gazpar2haws 0.2.0a20__py3-none-any.whl → 0.2.1__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 +104 -91
- gazpar2haws/bridge.py +32 -6
- gazpar2haws/config_utils.py +61 -56
- gazpar2haws/gazpar.py +280 -139
- gazpar2haws/haws.py +85 -32
- gazpar2haws/version.py +1 -1
- {gazpar2haws-0.2.0a20.dist-info → gazpar2haws-0.2.1.dist-info}/METADATA +39 -34
- gazpar2haws-0.2.1.dist-info/RECORD +11 -0
- gazpar2haws-0.2.0a20.dist-info/RECORD +0 -11
- {gazpar2haws-0.2.0a20.dist-info → gazpar2haws-0.2.1.dist-info}/LICENSE +0 -0
- {gazpar2haws-0.2.0a20.dist-info → gazpar2haws-0.2.1.dist-info}/WHEEL +0 -0
gazpar2haws/__main__.py
CHANGED
@@ -1,91 +1,104 @@
|
|
1
|
-
import
|
2
|
-
import
|
3
|
-
import logging
|
4
|
-
import traceback
|
5
|
-
|
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(
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
parser.add_argument(
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
config.
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
1
|
+
import argparse
|
2
|
+
import asyncio
|
3
|
+
import logging
|
4
|
+
import traceback
|
5
|
+
|
6
|
+
from gazpar2haws import __version__, config_utils
|
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(
|
16
|
+
prog="gazpar2haws",
|
17
|
+
description="Gateway that reads data history from the GrDF (French gas provider) meter and send it to Home Assistant using WebSocket interface.",
|
18
|
+
)
|
19
|
+
parser.add_argument(
|
20
|
+
"-v", "--version", action="version", version="Gazpar2HAWS version"
|
21
|
+
)
|
22
|
+
parser.add_argument(
|
23
|
+
"-c",
|
24
|
+
"--config",
|
25
|
+
required=False,
|
26
|
+
default="config/configuration.yaml",
|
27
|
+
help="Path to the configuration file",
|
28
|
+
)
|
29
|
+
parser.add_argument(
|
30
|
+
"-s",
|
31
|
+
"--secrets",
|
32
|
+
required=False,
|
33
|
+
default="config/secrets.yaml",
|
34
|
+
help="Path to the secret file",
|
35
|
+
)
|
36
|
+
|
37
|
+
args = parser.parse_args()
|
38
|
+
|
39
|
+
try:
|
40
|
+
# Load configuration files
|
41
|
+
config = config_utils.ConfigLoader(args.config, args.secrets)
|
42
|
+
config.load_secrets()
|
43
|
+
config.load_config()
|
44
|
+
|
45
|
+
print(f"Gazpar2HAWS version: {__version__}")
|
46
|
+
|
47
|
+
# 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")
|
52
|
+
|
53
|
+
# Convert logging level to integer
|
54
|
+
if logging_level.upper() == "DEBUG":
|
55
|
+
level = logging.DEBUG
|
56
|
+
elif logging_level.upper() == "INFO":
|
57
|
+
level = logging.INFO
|
58
|
+
elif logging_level.upper() == "WARNING":
|
59
|
+
level = logging.WARNING
|
60
|
+
elif logging_level.upper() == "ERROR":
|
61
|
+
level = logging.ERROR
|
62
|
+
elif logging_level.upper() == "CRITICAL":
|
63
|
+
level = logging.CRITICAL
|
64
|
+
else:
|
65
|
+
level = logging.INFO
|
66
|
+
|
67
|
+
logging.basicConfig(filename=logging_file, level=level, format=logging_format)
|
68
|
+
|
69
|
+
if logging_console:
|
70
|
+
# Add a console handler manually
|
71
|
+
console_handler = logging.StreamHandler()
|
72
|
+
console_handler.setLevel(level) # Set logging level for the console
|
73
|
+
console_handler.setFormatter(
|
74
|
+
logging.Formatter(logging_format)
|
75
|
+
) # Customize console format
|
76
|
+
|
77
|
+
# Get the root logger and add the console handler
|
78
|
+
logging.getLogger().addHandler(console_handler)
|
79
|
+
|
80
|
+
Logger.info(f"Starting Gazpar2HAWS version {__version__}")
|
81
|
+
|
82
|
+
# Log configuration
|
83
|
+
Logger.info(f"Configuration:\n{config.dumps()}")
|
84
|
+
|
85
|
+
# Start the bridge
|
86
|
+
bridge = Bridge(config)
|
87
|
+
await bridge.run()
|
88
|
+
|
89
|
+
Logger.info("Gazpar2HAWS stopped.")
|
90
|
+
|
91
|
+
return 0
|
92
|
+
|
93
|
+
except Exception: # pylint: disable=broad-except
|
94
|
+
errorMessage = (
|
95
|
+
f"An error occured while running Gazpar2HAWS: {traceback.format_exc()}"
|
96
|
+
)
|
97
|
+
Logger.error(errorMessage)
|
98
|
+
print(errorMessage)
|
99
|
+
return 1
|
100
|
+
|
101
|
+
|
102
|
+
# ----------------------------------
|
103
|
+
if __name__ == "__main__":
|
104
|
+
asyncio.run(main())
|
gazpar2haws/bridge.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
+
import asyncio
|
1
2
|
import logging
|
2
3
|
import signal
|
3
|
-
|
4
|
+
|
4
5
|
from gazpar2haws import config_utils
|
5
6
|
from gazpar2haws.gazpar import Gazpar
|
6
7
|
from gazpar2haws.haws import HomeAssistantWS
|
@@ -15,12 +16,30 @@ class Bridge:
|
|
15
16
|
def __init__(self, config: config_utils.ConfigLoader):
|
16
17
|
|
17
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")
|
18
21
|
self._grdf_scan_interval = int(config.get("grdf.scan_interval"))
|
19
22
|
|
20
|
-
# Home Assistant configuration
|
23
|
+
# Home Assistant configuration: host
|
24
|
+
if config.get("homeassistant.host") is None:
|
25
|
+
raise ValueError("Configuration parameter 'homeassistant.host' is missing")
|
21
26
|
ha_host = config.get("homeassistant.host")
|
27
|
+
|
28
|
+
# Home Assistant configuration: port
|
29
|
+
if config.get("homeassistant.port") is None:
|
30
|
+
raise ValueError("Configuration parameter 'homeassistant.port' is missing")
|
22
31
|
ha_port = config.get("homeassistant.port")
|
23
|
-
|
32
|
+
|
33
|
+
# Home Assistant configuration: endpoint
|
34
|
+
ha_endpoint = (
|
35
|
+
config.get("homeassistant.endpoint")
|
36
|
+
if config.get("homeassistant.endpoint")
|
37
|
+
else "/api/websocket"
|
38
|
+
)
|
39
|
+
|
40
|
+
# Home Assistant configuration: token
|
41
|
+
if config.get("homeassistant.token") is None:
|
42
|
+
raise ValueError("Configuration parameter 'homeassistant.token' is missing")
|
24
43
|
ha_token = config.get("homeassistant.token")
|
25
44
|
|
26
45
|
# Initialize Home Assistant
|
@@ -28,6 +47,9 @@ class Bridge:
|
|
28
47
|
|
29
48
|
# Initialize Gazpar
|
30
49
|
self._gazpar = []
|
50
|
+
|
51
|
+
if config.get("grdf.devices") is None:
|
52
|
+
raise ValueError("Configuration parameter 'grdf.devices' is missing")
|
31
53
|
for grdf_device_config in config.get("grdf.devices"):
|
32
54
|
self._gazpar.append(Gazpar(grdf_device_config, self._homeassistant))
|
33
55
|
|
@@ -40,7 +62,7 @@ class Bridge:
|
|
40
62
|
|
41
63
|
# ----------------------------------
|
42
64
|
# Graceful shutdown function
|
43
|
-
def handle_signal(self, signum,
|
65
|
+
def handle_signal(self, signum, _):
|
44
66
|
print(f"Signal {signum} received. Shutting down gracefully...")
|
45
67
|
Logger.info(f"Signal {signum} received. Shutting down gracefully...")
|
46
68
|
self._running = False
|
@@ -63,7 +85,9 @@ class Bridge:
|
|
63
85
|
for gazpar in self._gazpar:
|
64
86
|
Logger.info(f"Publishing data for device '{gazpar.name()}'...")
|
65
87
|
await gazpar.publish()
|
66
|
-
Logger.info(
|
88
|
+
Logger.info(
|
89
|
+
f"Device '{gazpar.name()}' data published to Home Assistant WS."
|
90
|
+
)
|
67
91
|
|
68
92
|
Logger.info("Gazpar data published to Home Assistant WS.")
|
69
93
|
|
@@ -71,7 +95,9 @@ class Bridge:
|
|
71
95
|
await self._homeassistant.disconnect()
|
72
96
|
|
73
97
|
# Wait before next scan
|
74
|
-
Logger.info(
|
98
|
+
Logger.info(
|
99
|
+
f"Waiting {self._grdf_scan_interval} minutes before next scan..."
|
100
|
+
)
|
75
101
|
|
76
102
|
# Check if the scan interval is 0 and leave the loop.
|
77
103
|
if self._grdf_scan_interval == 0:
|
gazpar2haws/config_utils.py
CHANGED
@@ -1,56 +1,61 @@
|
|
1
|
-
import
|
2
|
-
import
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
self.
|
10
|
-
self.
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
1
|
+
import os
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
import yaml
|
5
|
+
|
6
|
+
|
7
|
+
class ConfigLoader:
|
8
|
+
def __init__(self, config_file: str, secrets_file: str):
|
9
|
+
self.config_file = config_file
|
10
|
+
self.secrets_file = secrets_file
|
11
|
+
self.config = dict[str, Any]()
|
12
|
+
self.secrets = dict[str, Any]()
|
13
|
+
self.raw_config = None
|
14
|
+
|
15
|
+
def load_secrets(self):
|
16
|
+
"""Load the secrets file."""
|
17
|
+
if self.secrets_file is None:
|
18
|
+
return
|
19
|
+
if os.path.exists(self.secrets_file):
|
20
|
+
with open(self.secrets_file, "r", encoding="utf-8") as file:
|
21
|
+
self.secrets = yaml.safe_load(file)
|
22
|
+
else:
|
23
|
+
raise FileNotFoundError(f"Secrets file '{self.secrets_file}' not found.")
|
24
|
+
|
25
|
+
def load_config(self):
|
26
|
+
"""Load the main configuration file and resolve secrets."""
|
27
|
+
if os.path.exists(self.config_file):
|
28
|
+
with open(self.config_file, "r", encoding="utf-8") as file:
|
29
|
+
self.raw_config = yaml.safe_load(file)
|
30
|
+
self.config = self._resolve_secrets(self.raw_config)
|
31
|
+
else:
|
32
|
+
raise FileNotFoundError(
|
33
|
+
f"Configuration file '{self.config_file}' not found."
|
34
|
+
)
|
35
|
+
|
36
|
+
def _resolve_secrets(self, data):
|
37
|
+
"""Recursively resolve `!secret` keys in the configuration."""
|
38
|
+
if isinstance(data, dict):
|
39
|
+
return {key: self._resolve_secrets(value) for key, value in data.items()}
|
40
|
+
if isinstance(data, list):
|
41
|
+
return [self._resolve_secrets(item) for item in data]
|
42
|
+
if isinstance(data, str) and data.startswith("!secret"):
|
43
|
+
secret_key = data.split(" ", 1)[1]
|
44
|
+
if secret_key in self.secrets:
|
45
|
+
return self.secrets[secret_key]
|
46
|
+
raise KeyError(f"Secret key '{secret_key}' not found in secrets file.")
|
47
|
+
return data
|
48
|
+
|
49
|
+
def get(self, key, default=None):
|
50
|
+
"""Get a configuration value."""
|
51
|
+
keys = key.split(".")
|
52
|
+
value = self.config
|
53
|
+
try:
|
54
|
+
for k in keys:
|
55
|
+
value = value[k]
|
56
|
+
return value
|
57
|
+
except (KeyError, TypeError):
|
58
|
+
return default
|
59
|
+
|
60
|
+
def dumps(self) -> str:
|
61
|
+
return yaml.dump(self.raw_config)
|
gazpar2haws/gazpar.py
CHANGED
@@ -1,139 +1,280 @@
|
|
1
|
-
import
|
2
|
-
import traceback
|
3
|
-
import
|
4
|
-
import
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
self.
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
#
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
1
|
+
import logging
|
2
|
+
import traceback
|
3
|
+
from datetime import datetime, timedelta
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
import pygazpar # type: ignore
|
7
|
+
import pytz
|
8
|
+
|
9
|
+
from gazpar2haws.haws import HomeAssistantWS, HomeAssistantWSException
|
10
|
+
|
11
|
+
Logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
# ----------------------------------
|
15
|
+
class Gazpar:
|
16
|
+
|
17
|
+
# ----------------------------------
|
18
|
+
def __init__(self, config: dict[str, Any], homeassistant: HomeAssistantWS):
|
19
|
+
|
20
|
+
self._homeassistant = homeassistant
|
21
|
+
|
22
|
+
# GrDF configuration: name
|
23
|
+
if config.get("name") is None:
|
24
|
+
raise ValueError("Configuration parameter 'grdf.devices[].name' is missing")
|
25
|
+
self._name = config.get("name")
|
26
|
+
|
27
|
+
# GrDF configuration: data source
|
28
|
+
self._data_source = (
|
29
|
+
config.get("data_source") if config.get("data_source") else "json"
|
30
|
+
)
|
31
|
+
|
32
|
+
# GrDF configuration: username
|
33
|
+
if self._data_source != "test" and config.get("username") is None:
|
34
|
+
raise ValueError(
|
35
|
+
"Configuration parameter 'grdf.devices[].username' is missing"
|
36
|
+
)
|
37
|
+
self._username = config.get("username")
|
38
|
+
|
39
|
+
# GrDF configuration: password
|
40
|
+
if self._data_source != "test" and config.get("password") is None:
|
41
|
+
raise ValueError(
|
42
|
+
"Configuration parameter 'grdf.devices[].password' is missing"
|
43
|
+
)
|
44
|
+
self._password = config.get("password")
|
45
|
+
|
46
|
+
# GrDF configuration: pce_identifier
|
47
|
+
if self._data_source != "test" and config.get("pce_identifier") is None:
|
48
|
+
raise ValueError(
|
49
|
+
"Configuration parameter 'grdf.devices[].pce_identifier' is missing"
|
50
|
+
)
|
51
|
+
self._pce_identifier = str(config.get("pce_identifier"))
|
52
|
+
|
53
|
+
# GrDF configuration: tmp_dir
|
54
|
+
self._tmp_dir = config.get("tmp_dir") if config.get("tmp_dir") else "/tmp"
|
55
|
+
|
56
|
+
# GrDF configuration: last_days
|
57
|
+
if config.get("last_days") is None:
|
58
|
+
raise ValueError(
|
59
|
+
"Configuration parameter 'grdf.devices[].last_days' is missing"
|
60
|
+
)
|
61
|
+
self._last_days = int(str(config.get("last_days")))
|
62
|
+
|
63
|
+
# GrDF configuration: timezone
|
64
|
+
if config.get("timezone") is None:
|
65
|
+
raise ValueError(
|
66
|
+
"Configuration parameter 'grdf.devices[].timezone' is missing"
|
67
|
+
)
|
68
|
+
self._timezone = str(config.get("timezone"))
|
69
|
+
|
70
|
+
# GrDF configuration: reset
|
71
|
+
if config.get("reset") is None:
|
72
|
+
raise ValueError(
|
73
|
+
"Configuration parameter 'grdf.devices[].reset' is missing"
|
74
|
+
)
|
75
|
+
self._reset = bool(config.get("reset"))
|
76
|
+
|
77
|
+
# As of date: YYYY-MM-DD
|
78
|
+
as_of_date = config.get("as_of_date")
|
79
|
+
if self._data_source is not None and str(self._data_source).lower() == "test":
|
80
|
+
self._as_of_date = (
|
81
|
+
datetime.now(tz=pytz.timezone(self._timezone))
|
82
|
+
if as_of_date is None
|
83
|
+
else datetime.strptime(as_of_date, "%Y-%m-%d")
|
84
|
+
)
|
85
|
+
else:
|
86
|
+
self._as_of_date = datetime.now(tz=pytz.timezone(self._timezone))
|
87
|
+
|
88
|
+
# Set the timezone
|
89
|
+
timezone = pytz.timezone(self._timezone)
|
90
|
+
if self._as_of_date.tzinfo is None:
|
91
|
+
self._as_of_date = timezone.localize(self._as_of_date)
|
92
|
+
|
93
|
+
# ----------------------------------
|
94
|
+
def name(self):
|
95
|
+
return self._name
|
96
|
+
|
97
|
+
# ----------------------------------
|
98
|
+
# Publish Gaspar data to Home Assistant WS
|
99
|
+
async def publish(self):
|
100
|
+
|
101
|
+
# Volume and energy sensor names.
|
102
|
+
volume_sensor_name = f"sensor.{self._name}_volume"
|
103
|
+
energy_sensor_name = f"sensor.{self._name}_energy"
|
104
|
+
|
105
|
+
# Eventually reset the sensor in Home Assistant
|
106
|
+
if self._reset:
|
107
|
+
try:
|
108
|
+
await self._homeassistant.clear_statistics(
|
109
|
+
[volume_sensor_name, energy_sensor_name]
|
110
|
+
)
|
111
|
+
except Exception:
|
112
|
+
Logger.warning(
|
113
|
+
f"Error while resetting the sensor in Home Assistant: {traceback.format_exc()}"
|
114
|
+
)
|
115
|
+
raise
|
116
|
+
|
117
|
+
# Publish volume sensor
|
118
|
+
await self._publish_entity(
|
119
|
+
volume_sensor_name, pygazpar.PropertyName.VOLUME.value, "m³"
|
120
|
+
)
|
121
|
+
await self._publish_entity(
|
122
|
+
energy_sensor_name, pygazpar.PropertyName.ENERGY.value, "kWh"
|
123
|
+
)
|
124
|
+
|
125
|
+
# ----------------------------------
|
126
|
+
# Publish a sensor to Home Assistant
|
127
|
+
async def _publish_entity(
|
128
|
+
self, entity_id: str, property_name: str, unit_of_measurement: str
|
129
|
+
):
|
130
|
+
|
131
|
+
# Find last date, days and value of the entity.
|
132
|
+
last_date, last_days, last_value = await self._find_last_date_days_value(
|
133
|
+
entity_id
|
134
|
+
)
|
135
|
+
|
136
|
+
# Instantiate the right data source.
|
137
|
+
data_source = self._create_data_source()
|
138
|
+
|
139
|
+
# Initialize PyGazpar client
|
140
|
+
client = pygazpar.Client(data_source)
|
141
|
+
|
142
|
+
try:
|
143
|
+
data = client.loadSince(
|
144
|
+
pceIdentifier=self._pce_identifier,
|
145
|
+
lastNDays=last_days,
|
146
|
+
frequencies=[pygazpar.Frequency.DAILY],
|
147
|
+
)
|
148
|
+
except Exception: # pylint: disable=broad-except
|
149
|
+
Logger.warning(
|
150
|
+
f"Error while fetching data from GrDF: {traceback.format_exc()}"
|
151
|
+
)
|
152
|
+
data = {}
|
153
|
+
|
154
|
+
# Timezone
|
155
|
+
timezone = pytz.timezone(self._timezone)
|
156
|
+
|
157
|
+
# Compute and fill statistics.
|
158
|
+
daily = data.get(pygazpar.Frequency.DAILY.value)
|
159
|
+
statistics = []
|
160
|
+
total = last_value
|
161
|
+
for reading in daily:
|
162
|
+
# Parse date format DD/MM/YYYY into datetime.
|
163
|
+
date = datetime.strptime(
|
164
|
+
reading[pygazpar.PropertyName.TIME_PERIOD.value], "%d/%m/%Y"
|
165
|
+
)
|
166
|
+
|
167
|
+
# Set the timezone
|
168
|
+
date = timezone.localize(date)
|
169
|
+
|
170
|
+
# Skip all readings before the last statistic date.
|
171
|
+
if date <= last_date:
|
172
|
+
Logger.debug(f"Skip date: {date} <= {last_date}")
|
173
|
+
continue
|
174
|
+
|
175
|
+
# Compute the total volume and energy
|
176
|
+
if reading[property_name] is not None:
|
177
|
+
total += reading[property_name]
|
178
|
+
else:
|
179
|
+
Logger.warning(
|
180
|
+
f"Missing property {property_name} for date {date}. Skipping..."
|
181
|
+
)
|
182
|
+
continue
|
183
|
+
|
184
|
+
statistics.append({"start": date.isoformat(), "state": total, "sum": total})
|
185
|
+
|
186
|
+
# Publish statistics to Home Assistant
|
187
|
+
try:
|
188
|
+
await self._homeassistant.import_statistics(
|
189
|
+
entity_id, "recorder", "gazpar2haws", unit_of_measurement, statistics
|
190
|
+
)
|
191
|
+
except Exception:
|
192
|
+
Logger.warning(
|
193
|
+
f"Error while importing statistics to Home Assistant: {traceback.format_exc()}"
|
194
|
+
)
|
195
|
+
raise
|
196
|
+
|
197
|
+
# ----------------------------------
|
198
|
+
# Create the data source.
|
199
|
+
def _create_data_source(self) -> pygazpar.datasource.IDataSource:
|
200
|
+
|
201
|
+
if self._data_source is not None:
|
202
|
+
if str(self._data_source).lower() == "test":
|
203
|
+
return pygazpar.TestDataSource()
|
204
|
+
|
205
|
+
if str(self._data_source).lower() == "excel":
|
206
|
+
return pygazpar.ExcelWebDataSource(
|
207
|
+
username=self._username,
|
208
|
+
password=self._password,
|
209
|
+
tmpDirectory=self._tmp_dir,
|
210
|
+
)
|
211
|
+
|
212
|
+
return pygazpar.JsonWebDataSource(
|
213
|
+
username=self._username, password=self._password
|
214
|
+
)
|
215
|
+
|
216
|
+
# ----------------------------------
|
217
|
+
# Find last date, days and value of the entity.
|
218
|
+
async def _find_last_date_days_value(
|
219
|
+
self, entity_id: str
|
220
|
+
) -> tuple[datetime, int, float]:
|
221
|
+
|
222
|
+
# Check the existence of the sensor in Home Assistant
|
223
|
+
try:
|
224
|
+
exists_statistic_id = await self._homeassistant.exists_statistic_id(
|
225
|
+
entity_id, "sum"
|
226
|
+
)
|
227
|
+
except Exception:
|
228
|
+
Logger.warning(
|
229
|
+
f"Error while checking the existence of the sensor in Home Assistant: {traceback.format_exc()}"
|
230
|
+
)
|
231
|
+
raise
|
232
|
+
|
233
|
+
if exists_statistic_id:
|
234
|
+
# Get the last statistic from Home Assistant
|
235
|
+
try:
|
236
|
+
last_statistic = await self._homeassistant.get_last_statistic(
|
237
|
+
entity_id, self._as_of_date, self._last_days
|
238
|
+
)
|
239
|
+
except HomeAssistantWSException:
|
240
|
+
Logger.warning(
|
241
|
+
f"Error while fetching last statistics from Home Assistant: {traceback.format_exc()}"
|
242
|
+
)
|
243
|
+
|
244
|
+
if last_statistic:
|
245
|
+
# Extract the end date of the last statistics from the unix timestamp
|
246
|
+
last_date = datetime.fromtimestamp(
|
247
|
+
int(str(last_statistic.get("start"))) / 1000,
|
248
|
+
tz=pytz.timezone(self._timezone),
|
249
|
+
)
|
250
|
+
|
251
|
+
# Compute the number of days since the last statistics
|
252
|
+
last_days = (self._as_of_date - last_date).days
|
253
|
+
|
254
|
+
# Get the last meter value
|
255
|
+
last_value = float(str(last_statistic.get("sum")))
|
256
|
+
|
257
|
+
Logger.debug(
|
258
|
+
f"Last date: {last_date}, last days: {last_days}, last value: {last_value}"
|
259
|
+
)
|
260
|
+
|
261
|
+
return last_date, last_days, last_value
|
262
|
+
|
263
|
+
Logger.debug(f"No statistics found for the existing sensor {entity_id}.")
|
264
|
+
else:
|
265
|
+
Logger.debug(f"Sensor {entity_id} does not exist in Home Assistant.")
|
266
|
+
|
267
|
+
# If the sensor does not exist in Home Assistant, fetch the last days defined in the configuration
|
268
|
+
last_days = self._last_days
|
269
|
+
|
270
|
+
# Compute the corresponding last_date
|
271
|
+
last_date = self._as_of_date - timedelta(days=last_days)
|
272
|
+
|
273
|
+
# If no statistic, the last value is initialized to zero
|
274
|
+
last_value = 0
|
275
|
+
|
276
|
+
Logger.debug(
|
277
|
+
f"Last date: {last_date}, last days: {last_days}, last value: {last_value}"
|
278
|
+
)
|
279
|
+
|
280
|
+
return last_date, last_days, last_value
|
gazpar2haws/haws.py
CHANGED
@@ -1,13 +1,20 @@
|
|
1
|
-
import websockets
|
2
1
|
import json
|
3
|
-
from datetime import datetime, timedelta
|
4
2
|
import logging
|
3
|
+
from datetime import datetime, timedelta
|
4
|
+
|
5
|
+
import websockets
|
5
6
|
|
6
7
|
Logger = logging.getLogger(__name__)
|
7
8
|
|
8
9
|
|
10
|
+
# ----------------------------------
|
11
|
+
class HomeAssistantWSException(Exception):
|
12
|
+
pass
|
13
|
+
|
14
|
+
|
9
15
|
# ----------------------------------
|
10
16
|
class HomeAssistantWS:
|
17
|
+
# ----------------------------------
|
11
18
|
def __init__(self, host: str, port: str, endpoint: str, token: str):
|
12
19
|
self._host = host
|
13
20
|
self._port = port
|
@@ -24,7 +31,9 @@ class HomeAssistantWS:
|
|
24
31
|
ws_url = f"ws://{self._host}:{self._port}{self._endpoint}"
|
25
32
|
|
26
33
|
# Connect to the websocket
|
27
|
-
self._websocket = await websockets.connect(
|
34
|
+
self._websocket = await websockets.connect(
|
35
|
+
ws_url, additional_headers={"Authorization": f"Bearer {self._token}"}
|
36
|
+
)
|
28
37
|
|
29
38
|
# When a client connects to the server, the server sends out auth_required.
|
30
39
|
connect_response = await self._websocket.recv()
|
@@ -33,7 +42,7 @@ class HomeAssistantWS:
|
|
33
42
|
if connect_response_data.get("type") != "auth_required":
|
34
43
|
message = f"Authentication failed: auth_required not received {connect_response_data.get('messsage')}"
|
35
44
|
Logger.warning(message)
|
36
|
-
raise
|
45
|
+
raise HomeAssistantWSException(message)
|
37
46
|
|
38
47
|
# The first message from the client should be an auth message. You can authorize with an access token.
|
39
48
|
auth_message = {"type": "auth", "access_token": self._token}
|
@@ -46,7 +55,7 @@ class HomeAssistantWS:
|
|
46
55
|
if auth_response_data.get("type") == "auth_invalid":
|
47
56
|
message = f"Authentication failed: {auth_response_data.get('messsage')}"
|
48
57
|
Logger.warning(message)
|
49
|
-
raise
|
58
|
+
raise HomeAssistantWSException(message)
|
50
59
|
|
51
60
|
Logger.debug("Connected to Home Assistant")
|
52
61
|
|
@@ -60,10 +69,13 @@ class HomeAssistantWS:
|
|
60
69
|
Logger.debug("Disconnected from Home Assistant")
|
61
70
|
|
62
71
|
# ----------------------------------
|
63
|
-
async def send_message(self, message: dict) -> dict:
|
72
|
+
async def send_message(self, message: dict) -> dict | list[dict]:
|
64
73
|
|
65
74
|
Logger.debug("Sending a message...")
|
66
75
|
|
76
|
+
if self._websocket is None:
|
77
|
+
raise HomeAssistantWSException("Not connected to Home Assistant")
|
78
|
+
|
67
79
|
message["id"] = self._message_id
|
68
80
|
|
69
81
|
self._message_id += 1
|
@@ -77,40 +89,50 @@ class HomeAssistantWS:
|
|
77
89
|
Logger.debug("Received response")
|
78
90
|
|
79
91
|
if response_data.get("type") != "result":
|
80
|
-
raise
|
92
|
+
raise HomeAssistantWSException(f"Invalid response message: {response_data}")
|
81
93
|
|
82
94
|
if not response_data.get("success"):
|
83
|
-
raise
|
95
|
+
raise HomeAssistantWSException(
|
96
|
+
f"Request failed: {response_data.get('error')}"
|
97
|
+
)
|
84
98
|
|
85
99
|
return response_data.get("result")
|
86
100
|
|
87
101
|
# ----------------------------------
|
88
|
-
async def list_statistic_ids(self, statistic_type: str = None) -> list[
|
102
|
+
async def list_statistic_ids(self, statistic_type: str | None = None) -> list[dict]:
|
89
103
|
|
90
104
|
Logger.debug("Listing statistics IDs...")
|
91
105
|
|
92
106
|
# List statistics IDs message
|
93
|
-
list_statistic_ids_message = {
|
94
|
-
"type": "recorder/list_statistic_ids"
|
95
|
-
}
|
107
|
+
list_statistic_ids_message = {"type": "recorder/list_statistic_ids"}
|
96
108
|
|
97
109
|
if statistic_type is not None:
|
98
110
|
list_statistic_ids_message["statistic_type"] = statistic_type
|
99
111
|
|
100
112
|
response = await self.send_message(list_statistic_ids_message)
|
101
113
|
|
114
|
+
# Check response instance type
|
115
|
+
if not isinstance(response, list):
|
116
|
+
raise HomeAssistantWSException(
|
117
|
+
f"Invalid list_statistic_ids response type: got {type(response)} instead of list[dict]"
|
118
|
+
)
|
119
|
+
|
102
120
|
Logger.debug(f"Listed statistics IDs: {len(response)} ids")
|
103
121
|
|
104
122
|
return response
|
105
123
|
|
106
124
|
# ----------------------------------
|
107
|
-
async def exists_statistic_id(
|
125
|
+
async def exists_statistic_id(
|
126
|
+
self, entity_id: str, statistic_type: str | None = None
|
127
|
+
) -> bool:
|
108
128
|
|
109
129
|
Logger.debug(f"Checking if {entity_id} exists...")
|
110
130
|
|
111
131
|
statistic_ids = await self.list_statistic_ids(statistic_type)
|
112
132
|
|
113
|
-
entity_ids = [
|
133
|
+
entity_ids = [
|
134
|
+
statistic_id.get("statistic_id") for statistic_id in statistic_ids
|
135
|
+
]
|
114
136
|
|
115
137
|
exists_statistic = entity_id in entity_ids
|
116
138
|
|
@@ -119,9 +141,13 @@ class HomeAssistantWS:
|
|
119
141
|
return exists_statistic
|
120
142
|
|
121
143
|
# ----------------------------------
|
122
|
-
async def statistics_during_period(
|
144
|
+
async def statistics_during_period(
|
145
|
+
self, entity_ids: list[str], start_time: datetime, end_time: datetime
|
146
|
+
) -> dict:
|
123
147
|
|
124
|
-
Logger.debug(
|
148
|
+
Logger.debug(
|
149
|
+
f"Getting {entity_ids} statistics during period from {start_time} to {end_time}..."
|
150
|
+
)
|
125
151
|
|
126
152
|
# Subscribe to statistics
|
127
153
|
statistics_message = {
|
@@ -129,30 +155,55 @@ class HomeAssistantWS:
|
|
129
155
|
"start_time": start_time.isoformat(),
|
130
156
|
"end_time": end_time.isoformat(),
|
131
157
|
"statistic_ids": entity_ids,
|
132
|
-
"period": "day"
|
158
|
+
"period": "day",
|
133
159
|
}
|
134
160
|
|
135
161
|
response = await self.send_message(statistics_message)
|
136
162
|
|
137
|
-
|
163
|
+
# Check response instance type
|
164
|
+
if not isinstance(response, dict):
|
165
|
+
raise HomeAssistantWSException(
|
166
|
+
f"Invalid statistics_during_period response type: got {type(response)} instead of dict"
|
167
|
+
)
|
168
|
+
|
169
|
+
Logger.debug(
|
170
|
+
f"Received {entity_ids} statistics during period from {start_time} to {end_time}"
|
171
|
+
)
|
138
172
|
|
139
173
|
return response
|
140
174
|
|
141
175
|
# ----------------------------------
|
142
|
-
async def get_last_statistic(
|
176
|
+
async def get_last_statistic(
|
177
|
+
self, entity_id: str, as_of_date: datetime, depth_days: int
|
178
|
+
) -> dict:
|
143
179
|
|
144
180
|
Logger.debug(f"Getting last statistic for {entity_id}...")
|
145
181
|
|
146
|
-
statistics = await self.statistics_during_period(
|
182
|
+
statistics = await self.statistics_during_period(
|
183
|
+
[entity_id], as_of_date - timedelta(days=depth_days), as_of_date
|
184
|
+
)
|
185
|
+
|
186
|
+
if not statistics:
|
187
|
+
Logger.warning(f"No statistics found for {entity_id}.")
|
188
|
+
return {}
|
147
189
|
|
148
190
|
Logger.debug(f"Last statistic for {entity_id}: {statistics[entity_id][-1]}")
|
149
191
|
|
150
192
|
return statistics[entity_id][-1]
|
151
193
|
|
152
194
|
# ----------------------------------
|
153
|
-
async def import_statistics(
|
154
|
-
|
155
|
-
|
195
|
+
async def import_statistics(
|
196
|
+
self,
|
197
|
+
entity_id: str,
|
198
|
+
source: str,
|
199
|
+
name: str,
|
200
|
+
unit_of_measurement: str,
|
201
|
+
statistics: list[dict],
|
202
|
+
):
|
203
|
+
|
204
|
+
Logger.debug(
|
205
|
+
f"Importing {len(statistics)} statistics for {entity_id} from {source}..."
|
206
|
+
)
|
156
207
|
|
157
208
|
if len(statistics) == 0:
|
158
209
|
Logger.debug("No statistics to import")
|
@@ -162,19 +213,21 @@ class HomeAssistantWS:
|
|
162
213
|
import_statistics_message = {
|
163
214
|
"type": "recorder/import_statistics",
|
164
215
|
"metadata": {
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
216
|
+
"has_mean": False,
|
217
|
+
"has_sum": True,
|
218
|
+
"statistic_id": entity_id,
|
219
|
+
"source": source,
|
220
|
+
"name": name,
|
221
|
+
"unit_of_measurement": unit_of_measurement,
|
171
222
|
},
|
172
|
-
"stats": statistics
|
223
|
+
"stats": statistics,
|
173
224
|
}
|
174
225
|
|
175
226
|
await self.send_message(import_statistics_message)
|
176
227
|
|
177
|
-
Logger.debug(
|
228
|
+
Logger.debug(
|
229
|
+
f"Imported {len(statistics)} statistics for {entity_id} from {source}"
|
230
|
+
)
|
178
231
|
|
179
232
|
# ----------------------------------
|
180
233
|
async def clear_statistics(self, entity_ids: list[str]):
|
@@ -184,7 +237,7 @@ class HomeAssistantWS:
|
|
184
237
|
# Clear statistics message
|
185
238
|
clear_statistics_message = {
|
186
239
|
"type": "recorder/clear_statistics",
|
187
|
-
"statistic_ids": entity_ids
|
240
|
+
"statistic_ids": entity_ids,
|
188
241
|
}
|
189
242
|
|
190
243
|
await self.send_message(clear_statistics_message)
|
gazpar2haws/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: gazpar2haws
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.1
|
4
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
5
|
License: MIT License
|
6
6
|
|
@@ -52,7 +52,7 @@ Gazpar2HAWS can be installed in many ways.
|
|
52
52
|
|
53
53
|
### 1. Home Assistant Add-on
|
54
54
|
|
55
|
-
In the **Add-on store**, click **⋮ → Repositories**, fill in **`https://github.com/ssenart/gazpar2haws`** and click **Add → Close** or click the **Add repository** button below, click **Add → Close** (You might need to enter the **internal IP address** of your Home Assistant instance first).
|
55
|
+
In the **Add-on store**, click **⋮ → Repositories**, fill in **`https://github.com/ssenart/gazpar2haws`** and click **Add → Close** or click the **Add repository** button below, click **Add → Close** (You might need to enter the **internal IP address** of your Home Assistant instance first).
|
56
56
|
|
57
57
|
[](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Fssenart%2Fgazpar2haws)
|
58
58
|
|
@@ -67,11 +67,11 @@ The following steps permits to run a container from an existing image available
|
|
67
67
|
```yaml
|
68
68
|
services:
|
69
69
|
gazpar2haws:
|
70
|
-
image: ssenart/gazpar2haws:latest
|
70
|
+
image: ssenart/gazpar2haws:latest
|
71
71
|
container_name: gazpar2haws
|
72
72
|
restart: unless-stopped
|
73
73
|
network_mode: bridge
|
74
|
-
user: "1000:1000"
|
74
|
+
user: "1000:1000"
|
75
75
|
volumes:
|
76
76
|
- ./gazpar2haws/config:/app/config
|
77
77
|
- ./gazpar2haws/log:/app/log
|
@@ -86,6 +86,7 @@ services:
|
|
86
86
|
Edit the environment variable section according to your setup.
|
87
87
|
|
88
88
|
2. Run the container:
|
89
|
+
|
89
90
|
```sh
|
90
91
|
$ docker compose up -d
|
91
92
|
```
|
@@ -112,27 +113,33 @@ $ pip install gazpar2haws
|
|
112
113
|
The following steps permit to build the Docker image based on the local source files.
|
113
114
|
|
114
115
|
1. Clone the repo locally:
|
116
|
+
|
115
117
|
```sh
|
116
118
|
$ cd /path/to/my_install_folder/
|
117
119
|
|
118
120
|
$ git clone https://github.com/ssenart/gazpar2haws.git
|
119
121
|
```
|
122
|
+
|
120
123
|
2. Edit the docker-compose.yaml file by setting the environment variables corresponding to your GrDF account and Home Assistant setup:
|
121
124
|
|
122
125
|
```yaml
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
126
|
+
environment:
|
127
|
+
- GRDF_USERNAME=<GrDF account username>
|
128
|
+
- GRDF_PASSWORD=<GrDF account password>
|
129
|
+
- GRDF_PCE_IDENTIFIER=<GrDF PCE meter identifier>
|
130
|
+
- HOMEASSISTANT_HOST=<Home Assistant instance host name>
|
131
|
+
- HOMEASSISTANT_PORT=<Home Assistant instance port number>
|
132
|
+
- HOMEASSISTANT_TOKEN=<Home Assistant access token>
|
130
133
|
```
|
134
|
+
|
131
135
|
3. Build the image:
|
136
|
+
|
132
137
|
```sh
|
133
138
|
$ docker compose -f docker/docker-compose.yaml build
|
134
139
|
```
|
140
|
+
|
135
141
|
4. Run the container:
|
142
|
+
|
136
143
|
```sh
|
137
144
|
$ docker compose -f docker/docker-compose.yaml up -d
|
138
145
|
```
|
@@ -171,18 +178,18 @@ logging:
|
|
171
178
|
file: log/gazpar2haws.log
|
172
179
|
console: true
|
173
180
|
level: debug
|
174
|
-
format:
|
181
|
+
format: "%(asctime)s %(levelname)s [%(name)s] %(message)s"
|
175
182
|
|
176
183
|
grdf:
|
177
184
|
scan_interval: 0 # Number of minutes between each data retrieval (0 means no scan: a single data retrieval at startup, then stops).
|
178
185
|
devices:
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
+
- name: gazpar2haws # Name of the device in home assistant. It will be used as the entity_ids: sensor.${name}_volume and sensor.${name}_energy.
|
187
|
+
username: "!secret grdf.username"
|
188
|
+
password: "!secret grdf.password"
|
189
|
+
pce_identifier: "!secret grdf.pce_identifier"
|
190
|
+
timezone: Europe/Paris
|
191
|
+
last_days: 365 # Number of days of data to retrieve
|
192
|
+
reset: false # If true, the data will be reset before the first data retrieval
|
186
193
|
|
187
194
|
homeassistant:
|
188
195
|
host: "!secret homeassistant.host"
|
@@ -203,8 +210,9 @@ homeassistant.token: ${HA_TOKEN}
|
|
203
210
|
```
|
204
211
|
|
205
212
|
The history is uploaded on the entities with names:
|
206
|
-
|
207
|
-
- sensor.${name}
|
213
|
+
|
214
|
+
- sensor.${name}\_volume: Volume history in m³.
|
215
|
+
- sensor.${name}\_energy: Energy history in kWh.
|
208
216
|
|
209
217
|
`${name}` is 'gazpar2haws' defined in the above configuration file. It can be replaced by any other name.
|
210
218
|
|
@@ -212,16 +220,16 @@ The history is uploaded on the entities with names:
|
|
212
220
|
|
213
221
|
In a Docker environment, the configurations files are instantiated by replacing the environment variables below in the template files:
|
214
222
|
|
215
|
-
| Environment variable | Description
|
216
|
-
|
217
|
-
| GRDF_USERNAME
|
218
|
-
| GRDF_PASSWORD
|
219
|
-
| GRDF_PCE_IDENTIFIER | GrDF meter PCE identifier
|
220
|
-
| GRDF_SCAN_INTERVAL
|
221
|
-
| GRDF_LAST_DAYS
|
222
|
-
| HOMEASSISTANT_HOST
|
223
|
-
| HOMEASSISTANT_PORT
|
224
|
-
| HOMEASSISTANT_TOKEN | Home Assistant access token
|
223
|
+
| Environment variable | Description | Required | Default value |
|
224
|
+
| -------------------- | ----------------------------------------------------------------------------- | -------- | -------------- |
|
225
|
+
| GRDF_USERNAME | GrDF account user name | Yes | - |
|
226
|
+
| GRDF_PASSWORD | GrDF account password (avoid using special characters) | Yes | - |
|
227
|
+
| GRDF_PCE_IDENTIFIER | GrDF meter PCE identifier | Yes | - |
|
228
|
+
| GRDF_SCAN_INTERVAL | Period in minutes to refresh meter data (0 means one single refresh and stop) | No | 480 (8 hours) |
|
229
|
+
| GRDF_LAST_DAYS | Number of days of history data to retrieve | No | 1095 (3 years) |
|
230
|
+
| HOMEASSISTANT_HOST | Home Assistant instance host name | Yes | - |
|
231
|
+
| HOMEASSISTANT_PORT | Home Assistant instance port number | No | 8123 |
|
232
|
+
| HOMEASSISTANT_TOKEN | Home Assistant access token | Yes | - |
|
225
233
|
|
226
234
|
You can setup them directly in a docker-compose.yaml file (environment section) or from a Docker command line (-e option).
|
227
235
|
|
@@ -273,6 +281,3 @@ Please make sure to update tests as appropriate.
|
|
273
281
|
|
274
282
|
Gazpar2HAWS has been initiated for integration with [Home Assistant](https://www.home-assistant.io/) energy dashboard.
|
275
283
|
|
276
|
-
|
277
|
-
|
278
|
-
|
@@ -0,0 +1,11 @@
|
|
1
|
+
gazpar2haws/__init__.py,sha256=3MCDQdGGmT3FQMKaAB3mBJq7L75T_bJSdpDRjE-pado,58
|
2
|
+
gazpar2haws/__main__.py,sha256=KdhkowqJP6L8WTF9FNpTy4ZzDf1OPARm6mVtMclxGFk,3265
|
3
|
+
gazpar2haws/bridge.py,sha256=IDunENo7bQ9NDov76IEefAkMOen-6Wc_2NZcHGyk_XI,4601
|
4
|
+
gazpar2haws/config_utils.py,sha256=r8BvrA1r8huF-qjvQb0P1JYDLqzDleirLjOcj0xOyyU,2253
|
5
|
+
gazpar2haws/gazpar.py,sha256=29RfLxBaBCpoopOBpCX3xh681Ksm5pjZlLu9l3PqTAI,10321
|
6
|
+
gazpar2haws/haws.py,sha256=H--UWGECYq9JYlbU6IHLO2btaRXInwoVRZnHIXSA1ts,8169
|
7
|
+
gazpar2haws/version.py,sha256=9Iq5Jm63Ev7QquCjhDqa9_KAgHdKl9FJHynq8M6JNrY,83
|
8
|
+
gazpar2haws-0.2.1.dist-info/LICENSE,sha256=ajApZPyhVx8AU9wN7DXeRGhoWFqY2ylBZUa5GRhTmok,1073
|
9
|
+
gazpar2haws-0.2.1.dist-info/METADATA,sha256=Fbf6evtMKByRJCEWcE179YQHhmtsa4COSYluCIcGaKo,10050
|
10
|
+
gazpar2haws-0.2.1.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
|
11
|
+
gazpar2haws-0.2.1.dist-info/RECORD,,
|
@@ -1,11 +0,0 @@
|
|
1
|
-
gazpar2haws/__init__.py,sha256=3MCDQdGGmT3FQMKaAB3mBJq7L75T_bJSdpDRjE-pado,58
|
2
|
-
gazpar2haws/__main__.py,sha256=EMWGYVVfKEJySSvn8fmNfzFZWVjsPefUFyt4gTC506w,3162
|
3
|
-
gazpar2haws/bridge.py,sha256=plcXR8y6lH84OSHuUOogNcbM7uua24inoF9SSERKGHo,3539
|
4
|
-
gazpar2haws/config_utils.py,sha256=Q_-07kAIqvjjHG27tHLLnyaTAZcFVdt1iRzksz2wy1k,2067
|
5
|
-
gazpar2haws/gazpar.py,sha256=jXpOtqWW6fv6BQmVLoA0G7B93HjztY7MemvGnszBXPU,5615
|
6
|
-
gazpar2haws/haws.py,sha256=H0Qa01Qtsn3QdnGqIGkXE-Ympf7MSXkbFwAbzaMAodM,6895
|
7
|
-
gazpar2haws/version.py,sha256=tJINl5RAPtGkwDz8nWdcM1emyqLY2N2XfgsBHuofz5U,83
|
8
|
-
gazpar2haws-0.2.0a20.dist-info/LICENSE,sha256=ajApZPyhVx8AU9wN7DXeRGhoWFqY2ylBZUa5GRhTmok,1073
|
9
|
-
gazpar2haws-0.2.0a20.dist-info/METADATA,sha256=WsuiUmV_gYflm_ILcy4pIsuGWBn0rsPOWEb9UxVAIe0,9453
|
10
|
-
gazpar2haws-0.2.0a20.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
|
11
|
-
gazpar2haws-0.2.0a20.dist-info/RECORD,,
|
File without changes
|
File without changes
|