gazpar2haws 0.1.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.
CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2024-12-08
8
+
9
+ First version of the project.
@@ -0,0 +1 @@
1
+ from gazpar2haws.version import __version__ # noqa: F401
@@ -0,0 +1,91 @@
1
+ import asyncio
2
+ import argparse
3
+ import logging
4
+ import traceback
5
+ from gazpar2haws import config_utils
6
+ from gazpar2haws import __version__
7
+ from gazpar2haws.bridge import Bridge
8
+
9
+ Logger = logging.getLogger(__name__)
10
+
11
+
12
+ # ----------------------------------
13
+ async def main():
14
+ """Main function"""
15
+ parser = argparse.ArgumentParser(prog="gazpar2haws", description="Gateway that reads data history from the GrDF (French gas provider) meter and send it to Home Assistant using WebSocket interface.")
16
+ parser.add_argument("-v", "--version",
17
+ action="version",
18
+ version="Gazpar2HAWS version")
19
+ parser.add_argument("-c", "--config",
20
+ required=False,
21
+ default="config/configuration.yaml",
22
+ help="Path to the configuration file")
23
+ parser.add_argument("-s", "--secrets",
24
+ required=False,
25
+ default="config/secrets.yaml",
26
+ help="Path to the secret file")
27
+
28
+ args = parser.parse_args()
29
+
30
+ try:
31
+ # Load configuration files
32
+ config = config_utils.ConfigLoader(args.config, args.secrets)
33
+ config.load_secrets()
34
+ config.load_config()
35
+
36
+ print(f"Gazpar2HAWS version: {__version__}")
37
+
38
+ # Set up logging
39
+ logging_file = config.get("logging.file")
40
+ logging_console = bool(config.get("logging.console"))
41
+ logging_level = config.get("logging.level")
42
+ logging_format = config.get("logging.format")
43
+
44
+ # Convert logging level to integer
45
+ if logging_level.upper() == "DEBUG":
46
+ level = logging.DEBUG
47
+ elif logging_level.upper() == "INFO":
48
+ level = logging.INFO
49
+ elif logging_level.upper() == "WARNING":
50
+ level = logging.WARNING
51
+ elif logging_level.upper() == "ERROR":
52
+ level = logging.ERROR
53
+ elif logging_level.upper() == "CRITICAL":
54
+ level = logging.CRITICAL
55
+ else:
56
+ level = logging.INFO
57
+
58
+ logging.basicConfig(filename=logging_file, level=level, format=logging_format)
59
+
60
+ if logging_console:
61
+ # Add a console handler manually
62
+ console_handler = logging.StreamHandler()
63
+ console_handler.setLevel(level) # Set logging level for the console
64
+ console_handler.setFormatter(logging.Formatter(logging_format)) # Customize console format
65
+
66
+ # Get the root logger and add the console handler
67
+ logging.getLogger().addHandler(console_handler)
68
+
69
+ Logger.info(f"Starting Gazpar2HAWS version {__version__}")
70
+
71
+ # Log configuration
72
+ Logger.info(f"Configuration:\n{config.dumps()}")
73
+
74
+ # Start the bridge
75
+ bridge = Bridge(config)
76
+ await bridge.run()
77
+
78
+ Logger.info("Gazpar2HAWS stopped.")
79
+
80
+ return 0
81
+
82
+ except BaseException:
83
+ errorMessage = f"An error occured while running Gazpar2HAWS: {traceback.format_exc()}"
84
+ Logger.error(errorMessage)
85
+ print(errorMessage)
86
+ return 1
87
+
88
+
89
+ # ----------------------------------
90
+ if __name__ == '__main__':
91
+ asyncio.run(main())
gazpar2haws/bridge.py ADDED
@@ -0,0 +1,92 @@
1
+ import logging
2
+ import signal
3
+ import asyncio
4
+ from gazpar2haws import config_utils
5
+ from gazpar2haws.gazpar import Gazpar
6
+ from gazpar2haws.haws import HomeAssistantWS
7
+
8
+ Logger = logging.getLogger(__name__)
9
+
10
+
11
+ # ----------------------------------
12
+ class Bridge:
13
+
14
+ # ----------------------------------
15
+ def __init__(self, config: config_utils.ConfigLoader):
16
+
17
+ # GrDF scan interval (in seconds)
18
+ self._grdf_scan_interval = int(config.get("grdf.scan_interval"))
19
+
20
+ # Home Assistant configuration
21
+ ha_host = config.get("homeassistant.host")
22
+ ha_port = config.get("homeassistant.port")
23
+ ha_token = config.get("homeassistant.token")
24
+
25
+ # Initialize Home Assistant
26
+ self._homeassistant = HomeAssistantWS(ha_host, ha_port, ha_token)
27
+
28
+ # Initialize Gazpar
29
+ self._gazpar = []
30
+ for grdf_device_config in config.get("grdf.devices"):
31
+ self._gazpar.append(Gazpar(grdf_device_config, self._homeassistant))
32
+
33
+ # Set up signal handler
34
+ signal.signal(signal.SIGINT, self.handle_signal)
35
+ signal.signal(signal.SIGTERM, self.handle_signal)
36
+
37
+ # Initialize running flag
38
+ self._running = False
39
+
40
+ # ----------------------------------
41
+ # Graceful shutdown function
42
+ def handle_signal(self, signum, frame):
43
+ print(f"Signal {signum} received. Shutting down gracefully...")
44
+ Logger.info(f"Signal {signum} received. Shutting down gracefully...")
45
+ self._running = False
46
+
47
+ # ----------------------------------
48
+ async def run(self):
49
+
50
+ # Set running flag
51
+ self._running = True
52
+
53
+ try:
54
+ while self._running:
55
+
56
+ # Connect to Home Assistant
57
+ await self._homeassistant.connect()
58
+
59
+ # Publish Gazpar data to Home Assistant WS
60
+ Logger.info("Publishing Gazpar data to Home Assistant WS...")
61
+
62
+ for gazpar in self._gazpar:
63
+ Logger.info(f"Publishing data for device '{gazpar.name()}'...")
64
+ await gazpar.publish()
65
+ Logger.info(f"Device '{gazpar.name()}' data published to Home Assistant WS.")
66
+
67
+ Logger.info("Gazpar data published to Home Assistant WS.")
68
+
69
+ # Disconnect from Home Assistant
70
+ await self._homeassistant.disconnect()
71
+
72
+ # Wait before next scan
73
+ Logger.info(f"Waiting {self._grdf_scan_interval} minutes before next scan...")
74
+
75
+ # Check if the scan interval is 0 and leave the loop.
76
+ if self._grdf_scan_interval == 0:
77
+ break
78
+
79
+ await self._await_with_interrupt(self._grdf_scan_interval * 60, 5)
80
+ except KeyboardInterrupt:
81
+ print("Keyboard interrupt detected. Shutting down gracefully...")
82
+ Logger.info("Keyboard interrupt detected. Shutting down gracefully...")
83
+
84
+ # ----------------------------------
85
+ async def _await_with_interrupt(self, total_sleep_time: int, check_interval: int):
86
+ elapsed_time = 0
87
+ while elapsed_time < total_sleep_time:
88
+ await asyncio.sleep(check_interval)
89
+ elapsed_time += check_interval
90
+ # Check if an interrupt signal or external event requires breaking
91
+ if not self._running: # Assuming `running` is a global flag
92
+ break
@@ -0,0 +1,56 @@
1
+ import yaml
2
+ import os
3
+
4
+
5
+ class ConfigLoader:
6
+ def __init__(self, config_file="config.yaml", secrets_file="secrets.yaml"):
7
+ self.config_file = config_file
8
+ self.secrets_file = secrets_file
9
+ self.config = {}
10
+ self.secrets = {}
11
+
12
+ def load_secrets(self):
13
+ """Load the secrets file."""
14
+ if os.path.exists(self.secrets_file):
15
+ with open(self.secrets_file, 'r') as file:
16
+ self.secrets = yaml.safe_load(file)
17
+ else:
18
+ raise FileNotFoundError(f"Secrets file '{self.secrets_file}' not found.")
19
+
20
+ def load_config(self):
21
+ """Load the main configuration file and resolve secrets."""
22
+ if os.path.exists(self.config_file):
23
+ with open(self.config_file, 'r', encoding='utf-8') as file:
24
+ self.raw_config = yaml.safe_load(file)
25
+ self.config = self._resolve_secrets(self.raw_config)
26
+ else:
27
+ raise FileNotFoundError(f"Configuration file '{self.config_file}' not found.")
28
+
29
+ def _resolve_secrets(self, data):
30
+ """Recursively resolve `!secret` keys in the configuration."""
31
+ if isinstance(data, dict):
32
+ return {key: self._resolve_secrets(value) for key, value in data.items()}
33
+ elif isinstance(data, list):
34
+ return [self._resolve_secrets(item) for item in data]
35
+ elif isinstance(data, str) and data.startswith("!secret"):
36
+ secret_key = data.split(" ", 1)[1]
37
+ if secret_key in self.secrets:
38
+ return self.secrets[secret_key]
39
+ else:
40
+ raise KeyError(f"Secret key '{secret_key}' not found in secrets file.")
41
+ else:
42
+ return data
43
+
44
+ def get(self, key, default=None):
45
+ """Get a configuration value."""
46
+ keys = key.split(".")
47
+ value = self.config
48
+ try:
49
+ for k in keys:
50
+ value = value[k]
51
+ return value
52
+ except (KeyError, TypeError):
53
+ return default
54
+
55
+ def dumps(self) -> str:
56
+ return yaml.dump(self.raw_config)
gazpar2haws/gazpar.py ADDED
@@ -0,0 +1,114 @@
1
+ import pygazpar
2
+ import traceback
3
+ import logging
4
+ import pytz
5
+ from typing import Any
6
+ from datetime import datetime, timedelta
7
+ from gazpar2haws.haws import HomeAssistantWS
8
+
9
+ Logger = logging.getLogger(__name__)
10
+
11
+
12
+ # ----------------------------------
13
+ class Gazpar:
14
+
15
+ # ----------------------------------
16
+ def __init__(self, config: dict[str, Any], homeassistant: HomeAssistantWS):
17
+
18
+ self._homeassistant = homeassistant
19
+
20
+ # GrDF configuration
21
+ self._name = config.get("name")
22
+ self._username = config.get("username")
23
+ self._password = config.get("password")
24
+ self._pce_identifier = str(config.get("pce_identifier"))
25
+ self._last_days = int(config.get("last_days"))
26
+ self._timezone = config.get("timezone")
27
+ self._reset = bool(config.get("reset"))
28
+
29
+ # ----------------------------------
30
+ def name(self):
31
+ return self._name
32
+
33
+ # ----------------------------------
34
+ # Publish Gaspar data to Home Assistant WS
35
+ async def publish(self):
36
+
37
+ # Eventually reset the sensor in Home Assistant
38
+ if self._reset:
39
+ try:
40
+ await self._homeassistant.clear_statistics([f"sensor.{self._name}"])
41
+ except Exception:
42
+ errorMessage = f"Error while resetting the sensor in Home Assistant: {traceback.format_exc()}"
43
+ Logger.warning(errorMessage)
44
+ raise Exception(errorMessage)
45
+
46
+ # Check the existence of the sensor in Home Assistant
47
+ try:
48
+ exists_statistic_id = await self._homeassistant.exists_statistic_id(f"sensor.{self._name}", "sum")
49
+ except Exception:
50
+ errorMessage = f"Error while checking the existence of the sensor in Home Assistant: {traceback.format_exc()}"
51
+ Logger.warning(errorMessage)
52
+ raise Exception(errorMessage)
53
+
54
+ if exists_statistic_id:
55
+ # Get last statistics from GrDF
56
+ try:
57
+ last_statistics = await self._homeassistant.get_last_statistic(f"sensor.{self._name}")
58
+ except Exception:
59
+ errorMessage = f"Error while fetching last statistics from Home Assistant: {traceback.format_exc()}"
60
+ Logger.warning(errorMessage)
61
+ raise Exception(errorMessage)
62
+
63
+ # Extract the end date of the last statistics from the unix timestamp
64
+ last_date = datetime.fromtimestamp(last_statistics.get("end") / 1000)
65
+
66
+ # Compute the number of days since the last statistics
67
+ last_days = (datetime.now() - last_date).days
68
+ else:
69
+ # If the sensor does not exist in Home Assistant, fetch the last days defined in the configuration
70
+ last_days = self._last_days
71
+
72
+ # Compute the corresponding last_date
73
+ last_date = datetime.now() - timedelta(days=last_days)
74
+
75
+ # Initialize PyGazpar client
76
+ client = pygazpar.Client(pygazpar.JsonWebDataSource(username=self._username, password=self._password))
77
+
78
+ try:
79
+ data = client.loadSince(pceIdentifier=self._pce_identifier, lastNDays=last_days, frequencies=[pygazpar.Frequency.DAILY])
80
+ except Exception:
81
+ errorMessage = f"Error while fetching data from GrDF: {traceback.format_exc()}"
82
+ Logger.warning(errorMessage)
83
+ data = {}
84
+
85
+ # Timezone
86
+ timezone = pytz.timezone(self._timezone)
87
+
88
+ # Fill statistics.
89
+ daily = data.get(pygazpar.Frequency.DAILY.value)
90
+ statistics = []
91
+ for reading in daily:
92
+ # Parse date format DD/MM/YYYY into datetime.
93
+ date = datetime.strptime(reading[pygazpar.PropertyName.TIME_PERIOD.value], "%d/%m/%Y")
94
+
95
+ # Skip all readings before the last statistic date.
96
+ if date.date() < last_date.date():
97
+ continue
98
+
99
+ # Set the timezone
100
+ date = timezone.localize(date)
101
+
102
+ statistics.append({
103
+ "start": date.isoformat(),
104
+ "state": reading[pygazpar.PropertyName.END_INDEX.value],
105
+ "sum": reading[pygazpar.PropertyName.END_INDEX.value]
106
+ })
107
+
108
+ # Publish statistics to Home Assistant
109
+ try:
110
+ await self._homeassistant.import_statistics(f"sensor.{self._name}", "recorder", "gazpar2haws", "m³", statistics)
111
+ except Exception:
112
+ errorMessage = f"Error while importing statistics to Home Assistant: {traceback.format_exc()}"
113
+ Logger.warning(errorMessage)
114
+ raise Exception(errorMessage)
gazpar2haws/haws.py ADDED
@@ -0,0 +1,191 @@
1
+ import websockets
2
+ import json
3
+ from datetime import datetime, timedelta
4
+ import logging
5
+
6
+ Logger = logging.getLogger(__name__)
7
+
8
+
9
+ # ----------------------------------
10
+ class HomeAssistantWS:
11
+ def __init__(self, host, port, token):
12
+ self._host = host
13
+ self._port = port
14
+ self._token = token
15
+ self._websocket = None
16
+ self._message_id = 1
17
+
18
+ # ----------------------------------
19
+ async def connect(self):
20
+
21
+ Logger.debug(f"Connecting to Home Assistant at {self._host}:{self._port}")
22
+
23
+ ws_url = f"ws://{self._host}:{self._port}/api/websocket"
24
+
25
+ # Connect to the websocket
26
+ self._websocket = await websockets.connect(ws_url)
27
+
28
+ # When a client connects to the server, the server sends out auth_required.
29
+ connect_response = await self._websocket.recv()
30
+ connect_response_data = json.loads(connect_response)
31
+
32
+ if connect_response_data.get("type") != "auth_required":
33
+ message = f"Authentication failed: auth_required not received {connect_response_data.get('messsage')}"
34
+ Logger.warning(message)
35
+ raise Exception(message)
36
+
37
+ # The first message from the client should be an auth message. You can authorize with an access token.
38
+ auth_message = {"type": "auth", "access_token": self._token}
39
+ await self._websocket.send(json.dumps(auth_message))
40
+
41
+ # If the client supplies valid authentication, the authentication phase will complete by the server sending the auth_ok message.
42
+ auth_response = await self._websocket.recv()
43
+ auth_response_data = json.loads(auth_response)
44
+
45
+ if auth_response_data.get("type") == "auth_invalid":
46
+ message = f"Authentication failed: {auth_response_data.get('messsage')}"
47
+ Logger.warning(message)
48
+ raise Exception(message)
49
+
50
+ Logger.debug("Connected to Home Assistant")
51
+
52
+ # ----------------------------------
53
+ async def disconnect(self):
54
+
55
+ Logger.debug("Disconnecting from Home Assistant...")
56
+
57
+ await self._websocket.close()
58
+
59
+ Logger.debug("Disconnected from Home Assistant")
60
+
61
+ # ----------------------------------
62
+ async def send_message(self, message: dict) -> dict:
63
+
64
+ Logger.debug("Sending a message...")
65
+
66
+ message["id"] = self._message_id
67
+
68
+ self._message_id += 1
69
+
70
+ await self._websocket.send(json.dumps(message))
71
+
72
+ response = await self._websocket.recv()
73
+
74
+ response_data = json.loads(response)
75
+
76
+ Logger.debug(f"Received response: {response_data}")
77
+
78
+ if response_data.get("type") != "result":
79
+ raise Exception(f"Invalid response message: {response_data}")
80
+
81
+ if not response_data.get("success"):
82
+ raise Exception(f"Request failed: {response_data.get('error')}")
83
+
84
+ return response_data.get("result")
85
+
86
+ # ----------------------------------
87
+ async def list_statistic_ids(self, statistic_type: str = None) -> list[str]:
88
+
89
+ Logger.debug("Listing statistics IDs...")
90
+
91
+ # List statistics IDs message
92
+ list_statistic_ids_message = {
93
+ "type": "recorder/list_statistic_ids"
94
+ }
95
+
96
+ if statistic_type is not None:
97
+ list_statistic_ids_message["statistic_type"] = statistic_type
98
+
99
+ response = await self.send_message(list_statistic_ids_message)
100
+
101
+ Logger.debug(f"Listed statistics IDs: {len(response)} ids")
102
+
103
+ return response
104
+
105
+ # ----------------------------------
106
+ async def exists_statistic_id(self, entity_id: str, statistic_type: str = None) -> bool:
107
+
108
+ Logger.debug(f"Checking if {entity_id} exists...")
109
+
110
+ statistic_ids = await self.list_statistic_ids(statistic_type)
111
+
112
+ entity_ids = [statistic_id.get("statistic_id") for statistic_id in statistic_ids]
113
+
114
+ exists_statistic = entity_id in entity_ids
115
+
116
+ Logger.debug(f"{entity_id} exists: {exists_statistic}")
117
+
118
+ return exists_statistic
119
+
120
+ # ----------------------------------
121
+ async def statistics_during_period(self, entity_ids: list[str], start_time: datetime, end_time: datetime) -> dict:
122
+
123
+ Logger.debug(f"Getting {entity_ids} statistics during period from {start_time} to {end_time}...")
124
+
125
+ # Subscribe to statistics
126
+ statistics_message = {
127
+ "type": "recorder/statistics_during_period",
128
+ "start_time": start_time.isoformat(),
129
+ "end_time": end_time.isoformat(),
130
+ "statistic_ids": entity_ids,
131
+ "period": "day"
132
+ }
133
+
134
+ response = await self.send_message(statistics_message)
135
+
136
+ Logger.debug(f"Received {entity_ids} statistics during period from {start_time} to {end_time}")
137
+
138
+ return response
139
+
140
+ # ----------------------------------
141
+ async def get_last_statistic(self, entity_id: str) -> dict:
142
+
143
+ Logger.debug(f"Getting last statistic for {entity_id}...")
144
+
145
+ statistics = await self.statistics_during_period([entity_id], datetime.now() - timedelta(days=30), datetime.now())
146
+
147
+ Logger.debug(f"Last statistic for {entity_id}: {statistics[entity_id][-1]}")
148
+
149
+ return statistics[entity_id][-1]
150
+
151
+ # ----------------------------------
152
+ async def import_statistics(self, entity_id: str, source: str, name: str, unit_of_measurement: str, statistics: list[dict]):
153
+
154
+ Logger.debug(f"Importing {len(statistics)} statistics for {entity_id} from {source}...")
155
+
156
+ if len(statistics) == 0:
157
+ Logger.debug("No statistics to import")
158
+ return
159
+
160
+ # Import statistics message
161
+ import_statistics_message = {
162
+ "type": "recorder/import_statistics",
163
+ "metadata": {
164
+ "has_mean": False,
165
+ "has_sum": True,
166
+ "statistic_id": entity_id,
167
+ "source": source,
168
+ "name": name,
169
+ "unit_of_measurement": unit_of_measurement,
170
+ },
171
+ "stats": statistics
172
+ }
173
+
174
+ await self.send_message(import_statistics_message)
175
+
176
+ Logger.debug(f"Imported {len(statistics)} statistics for {entity_id} from {source}")
177
+
178
+ # ----------------------------------
179
+ async def clear_statistics(self, entity_ids: list[str]):
180
+
181
+ Logger.debug(f"Clearing {entity_ids} statistics...")
182
+
183
+ # Clear statistics message
184
+ clear_statistics_message = {
185
+ "type": "recorder/clear_statistics",
186
+ "statistic_ids": entity_ids
187
+ }
188
+
189
+ await self.send_message(clear_statistics_message)
190
+
191
+ Logger.debug(f"Cleared {entity_ids} statistics")
gazpar2haws/version.py ADDED
@@ -0,0 +1,3 @@
1
+ import importlib.metadata
2
+
3
+ __version__ = importlib.metadata.version('gazpar2haws')
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Stéphane Senart
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.1
2
+ Name: gazpar2haws
3
+ Version: 0.1.0
4
+ Summary: Gazpar2HAWS is a gateway that reads data history from the GrDF (French gas provider) meter and send it to Home Assistant using WebSocket interface
5
+ License: MIT
6
+ Author: Stéphane Senart
7
+ Requires-Python: >=3.12,<4.0
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Requires-Dist: pygazpar (==1.2.5)
13
+ Requires-Dist: pytest-asyncio (>=0.25.0,<0.26.0)
14
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
15
+ Requires-Dist: websockets (>=14.1,<15.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # gazpar2haws
19
+ Gazpar2HAWS is a gateway that reads data history from the GrDF (French gas provider) meter and send it to Home Assistant using WebSocket interface.
20
+
21
+ It is compatible with Home Assistant Energy Dashboard and permits to upload the history and keep it updated with the latest readings.
22
+
23
+ ## Installation
24
+
25
+ Gazpar2HAWS can be installed on any host as a standalone program.
26
+
27
+ ### 1. Using source files
28
+
29
+ The project requires [Poetry](https://python-poetry.org/) tool for dependency and package management.
30
+
31
+ ```sh
32
+ $ cd /path/to/my_install_folder/
33
+
34
+ $ git clone https://github.com/ssenart/gazpar2haws.git
35
+
36
+ $ cd gazpar2haws
37
+
38
+ $ poetry install
39
+
40
+ $ poetry shell
41
+
42
+ ```
43
+
44
+ ### 2. Using PIP package
45
+
46
+ ```sh
47
+ $ cd /path/to/my_install_folder/
48
+
49
+ $ mkdir gazpar2haws
50
+
51
+ $ cd gazpar2haws
52
+
53
+ $ python -m venv .venv
54
+
55
+ $ source .venv/bin/activate
56
+
57
+ $ pip install gazpar2haws
58
+
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ ### Command line
64
+
65
+ ```sh
66
+ $ python -m gazpar2haws --config /path/to/configuration.yaml --secrets /path/to/secrets.yaml
67
+ ```
68
+
69
+ ### Configuration file
70
+
71
+ The default configuration file is below.
72
+
73
+ ```yaml
74
+ logging:
75
+ file: log/gazpar2haws.log
76
+ console: true
77
+ level: debug
78
+ format: '%(asctime)s %(levelname)s [%(name)s] %(message)s'
79
+
80
+ grdf:
81
+ scan_interval: 0 # Number of minutes between each data retrieval (0 means no scan: a single data retrieval at startup, then stops).
82
+ devices:
83
+ - name: gazpar2haws
84
+ username: "!secret grdf.username"
85
+ password: "!secret grdf.password"
86
+ pce_identifier: "!secret grdf.pce_identifier"
87
+ timezone: Europe/Paris
88
+ last_days: 365 # Number of days of data to retrieve
89
+ reset: false # If true, the data will be reset before the first data retrieval
90
+
91
+ homeassistant:
92
+ host: "!secret homeassistant.host"
93
+ port: "!secret homeassistant.port"
94
+ token: "!secret homeassistant.token"
95
+ ```
96
+
97
+ The default secret file:
98
+
99
+ ```yaml
100
+ grdf.username: ${GRDF_USERNAME}
101
+ grdf.password: ${GRDF_PASSWORD}
102
+ grdf.pce_identifier: ${GRDF_PCE_IDENTIFIER}
103
+
104
+ homeassistant.host: ${HA_HOST}
105
+ homeassistant.port: ${HA_PORT}
106
+ homeassistant.token: ${HA_TOKEN}
107
+ ```
108
+
109
+ ## Contributing
110
+ Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
111
+
112
+ Please make sure to update tests as appropriate.
113
+
114
+ ## License
115
+ [MIT](https://choosealicense.com/licenses/mit/)
116
+
117
+ ## Project status
118
+ Gazpar2HAWS has been initiated for integration with [Home Assistant](https://www.home-assistant.io/) energy dashboard.
119
+
120
+
121
+
122
+
@@ -0,0 +1,12 @@
1
+ CHANGELOG.md,sha256=8JDG8V_DLBTwnVlZErwlK9PNI-xiwpU6vaqSKavYvKw,316
2
+ gazpar2haws/__init__.py,sha256=yzol8uZSBI7pIRGUmYJ6-vRBwkM4MI3IGf5cQpNsaFw,57
3
+ gazpar2haws/__main__.py,sha256=g8xk0x_kprBHKHLzgf9y9EY2_gKC9V3k4z0n-EDTd-I,3253
4
+ gazpar2haws/bridge.py,sha256=TiPmvRBxzgwOKKmWNXMOaP0pLg-Wls_SmiwHczNEM_4,3466
5
+ gazpar2haws/config_utils.py,sha256=D0lu-3KY-vLEyD2vDN05UABkMnpMJjqw1RuDJVrkGFs,2123
6
+ gazpar2haws/gazpar.py,sha256=4bEp0JDrLAFxbADSoRH76UD3OC8ls5GBhUcoBQ9WQxw,4663
7
+ gazpar2haws/haws.py,sha256=EXs5Tdla4u2BVtupCCLITyvdL_W288LrCxrTzskWSRY,6783
8
+ gazpar2haws/version.py,sha256=ebdTNl4h0hNKmN3Gbs592VJsYbMmrkB47WyZMJevaQo,86
9
+ gazpar2haws-0.1.0.dist-info/LICENSE,sha256=G6JttcnlwcRHYzIcDflSGOVrHTtaP3BEegM2lH00xHw,1094
10
+ gazpar2haws-0.1.0.dist-info/METADATA,sha256=H6EGtnQczzLQKLDwdPtAd2TwAzzVIi82AURA36Khwns,3099
11
+ gazpar2haws-0.1.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
12
+ gazpar2haws-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any