gazpar2haws 0.1.7__py3-none-any.whl → 0.1.8.dev5__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 +91 -91
- gazpar2haws/config_utils.py +56 -56
- gazpar2haws/gazpar.py +139 -139
- gazpar2haws/version.py +3 -3
- {gazpar2haws-0.1.7.dist-info → gazpar2haws-0.1.8.dev5.dist-info}/LICENSE +21 -21
- {gazpar2haws-0.1.7.dist-info → gazpar2haws-0.1.8.dev5.dist-info}/METADATA +30 -11
- gazpar2haws-0.1.8.dev5.dist-info/RECORD +11 -0
- {gazpar2haws-0.1.7.dist-info → gazpar2haws-0.1.8.dev5.dist-info}/WHEEL +1 -1
- CHANGELOG.md +0 -60
- gazpar2haws-0.1.7.dist-info/RECORD +0 -12
gazpar2haws/__main__.py
CHANGED
@@ -1,91 +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())
|
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/config_utils.py
CHANGED
@@ -1,56 +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)
|
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
CHANGED
@@ -1,139 +1,139 @@
|
|
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
|
-
# Volume and energy sensor names.
|
38
|
-
volume_sensor_name = f"sensor.{self._name}_volume"
|
39
|
-
energy_sensor_name = f"sensor.{self._name}_energy"
|
40
|
-
|
41
|
-
# Eventually reset the sensor in Home Assistant
|
42
|
-
if self._reset:
|
43
|
-
try:
|
44
|
-
await self._homeassistant.clear_statistics([volume_sensor_name, energy_sensor_name])
|
45
|
-
except Exception:
|
46
|
-
errorMessage = f"Error while resetting the sensor in Home Assistant: {traceback.format_exc()}"
|
47
|
-
Logger.warning(errorMessage)
|
48
|
-
raise Exception(errorMessage)
|
49
|
-
|
50
|
-
# Publish volume sensor
|
51
|
-
await self._publish_entity(volume_sensor_name, pygazpar.PropertyName.VOLUME.value, "m³")
|
52
|
-
await self._publish_entity(energy_sensor_name, pygazpar.PropertyName.ENERGY.value, "kWh")
|
53
|
-
|
54
|
-
# ----------------------------------
|
55
|
-
# Publish a sensor to Home Assistant
|
56
|
-
async def _publish_entity(self, entity_id: str, property_name: str, unit_of_measurement: str):
|
57
|
-
|
58
|
-
# Check the existence of the sensor in Home Assistant
|
59
|
-
try:
|
60
|
-
exists_statistic_id = await self._homeassistant.exists_statistic_id(entity_id, "sum")
|
61
|
-
except Exception:
|
62
|
-
errorMessage = f"Error while checking the existence of the sensor in Home Assistant: {traceback.format_exc()}"
|
63
|
-
Logger.warning(errorMessage)
|
64
|
-
raise Exception(errorMessage)
|
65
|
-
|
66
|
-
if exists_statistic_id:
|
67
|
-
# Get the last statistic from Home Assistant
|
68
|
-
try:
|
69
|
-
last_statistic = await self._homeassistant.get_last_statistic(entity_id)
|
70
|
-
except Exception:
|
71
|
-
errorMessage = f"Error while fetching last statistics from Home Assistant: {traceback.format_exc()}"
|
72
|
-
Logger.warning(errorMessage)
|
73
|
-
raise Exception(errorMessage)
|
74
|
-
|
75
|
-
# Extract the end date of the last statistics from the unix timestamp
|
76
|
-
last_date = datetime.fromtimestamp(last_statistic.get("start") / 1000, tz=pytz.timezone(self._timezone))
|
77
|
-
|
78
|
-
# Compute the number of days since the last statistics
|
79
|
-
last_days = (datetime.now(tz=pytz.timezone(self._timezone)) - last_date).days
|
80
|
-
|
81
|
-
# Get the last meter value
|
82
|
-
last_value = last_statistic.get("sum")
|
83
|
-
else:
|
84
|
-
# If the sensor does not exist in Home Assistant, fetch the last days defined in the configuration
|
85
|
-
last_days = self._last_days
|
86
|
-
|
87
|
-
# Compute the corresponding last_date
|
88
|
-
last_date = datetime.now(tz=pytz.timezone(self._timezone)) - timedelta(days=last_days)
|
89
|
-
|
90
|
-
# If no statistic, the last value is initialized to zero
|
91
|
-
last_value = 0
|
92
|
-
|
93
|
-
Logger.debug(f"Last date: {last_date}, last days: {last_days}, last value: {last_value}")
|
94
|
-
|
95
|
-
# Initialize PyGazpar client
|
96
|
-
client = pygazpar.Client(pygazpar.JsonWebDataSource(username=self._username, password=self._password))
|
97
|
-
|
98
|
-
try:
|
99
|
-
data = client.loadSince(pceIdentifier=self._pce_identifier, lastNDays=last_days, frequencies=[pygazpar.Frequency.DAILY])
|
100
|
-
except Exception:
|
101
|
-
errorMessage = f"Error while fetching data from GrDF: {traceback.format_exc()}"
|
102
|
-
Logger.warning(errorMessage)
|
103
|
-
data = {}
|
104
|
-
|
105
|
-
# Timezone
|
106
|
-
timezone = pytz.timezone(self._timezone)
|
107
|
-
|
108
|
-
# Compute and fill statistics.
|
109
|
-
daily = data.get(pygazpar.Frequency.DAILY.value)
|
110
|
-
statistics = []
|
111
|
-
total = last_value
|
112
|
-
for reading in daily:
|
113
|
-
# Parse date format DD/MM/YYYY into datetime.
|
114
|
-
date = datetime.strptime(reading[pygazpar.PropertyName.TIME_PERIOD.value], "%d/%m/%Y")
|
115
|
-
|
116
|
-
# Set the timezone
|
117
|
-
date = timezone.localize(date)
|
118
|
-
|
119
|
-
# Skip all readings before the last statistic date.
|
120
|
-
if date <= last_date:
|
121
|
-
Logger.debug(f"Skip date: {date} <= {last_date}")
|
122
|
-
continue
|
123
|
-
|
124
|
-
# Compute the total volume and energy
|
125
|
-
total += reading[property_name]
|
126
|
-
|
127
|
-
statistics.append({
|
128
|
-
"start": date.isoformat(),
|
129
|
-
"state": total,
|
130
|
-
"sum": total
|
131
|
-
})
|
132
|
-
|
133
|
-
# Publish statistics to Home Assistant
|
134
|
-
try:
|
135
|
-
await self._homeassistant.import_statistics(entity_id, "recorder", "gazpar2haws", unit_of_measurement, statistics)
|
136
|
-
except Exception:
|
137
|
-
errorMessage = f"Error while importing statistics to Home Assistant: {traceback.format_exc()}"
|
138
|
-
Logger.warning(errorMessage)
|
139
|
-
raise Exception(errorMessage)
|
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
|
+
# Volume and energy sensor names.
|
38
|
+
volume_sensor_name = f"sensor.{self._name}_volume"
|
39
|
+
energy_sensor_name = f"sensor.{self._name}_energy"
|
40
|
+
|
41
|
+
# Eventually reset the sensor in Home Assistant
|
42
|
+
if self._reset:
|
43
|
+
try:
|
44
|
+
await self._homeassistant.clear_statistics([volume_sensor_name, energy_sensor_name])
|
45
|
+
except Exception:
|
46
|
+
errorMessage = f"Error while resetting the sensor in Home Assistant: {traceback.format_exc()}"
|
47
|
+
Logger.warning(errorMessage)
|
48
|
+
raise Exception(errorMessage)
|
49
|
+
|
50
|
+
# Publish volume sensor
|
51
|
+
await self._publish_entity(volume_sensor_name, pygazpar.PropertyName.VOLUME.value, "m³")
|
52
|
+
await self._publish_entity(energy_sensor_name, pygazpar.PropertyName.ENERGY.value, "kWh")
|
53
|
+
|
54
|
+
# ----------------------------------
|
55
|
+
# Publish a sensor to Home Assistant
|
56
|
+
async def _publish_entity(self, entity_id: str, property_name: str, unit_of_measurement: str):
|
57
|
+
|
58
|
+
# Check the existence of the sensor in Home Assistant
|
59
|
+
try:
|
60
|
+
exists_statistic_id = await self._homeassistant.exists_statistic_id(entity_id, "sum")
|
61
|
+
except Exception:
|
62
|
+
errorMessage = f"Error while checking the existence of the sensor in Home Assistant: {traceback.format_exc()}"
|
63
|
+
Logger.warning(errorMessage)
|
64
|
+
raise Exception(errorMessage)
|
65
|
+
|
66
|
+
if exists_statistic_id:
|
67
|
+
# Get the last statistic from Home Assistant
|
68
|
+
try:
|
69
|
+
last_statistic = await self._homeassistant.get_last_statistic(entity_id)
|
70
|
+
except Exception:
|
71
|
+
errorMessage = f"Error while fetching last statistics from Home Assistant: {traceback.format_exc()}"
|
72
|
+
Logger.warning(errorMessage)
|
73
|
+
raise Exception(errorMessage)
|
74
|
+
|
75
|
+
# Extract the end date of the last statistics from the unix timestamp
|
76
|
+
last_date = datetime.fromtimestamp(last_statistic.get("start") / 1000, tz=pytz.timezone(self._timezone))
|
77
|
+
|
78
|
+
# Compute the number of days since the last statistics
|
79
|
+
last_days = (datetime.now(tz=pytz.timezone(self._timezone)) - last_date).days
|
80
|
+
|
81
|
+
# Get the last meter value
|
82
|
+
last_value = last_statistic.get("sum")
|
83
|
+
else:
|
84
|
+
# If the sensor does not exist in Home Assistant, fetch the last days defined in the configuration
|
85
|
+
last_days = self._last_days
|
86
|
+
|
87
|
+
# Compute the corresponding last_date
|
88
|
+
last_date = datetime.now(tz=pytz.timezone(self._timezone)) - timedelta(days=last_days)
|
89
|
+
|
90
|
+
# If no statistic, the last value is initialized to zero
|
91
|
+
last_value = 0
|
92
|
+
|
93
|
+
Logger.debug(f"Last date: {last_date}, last days: {last_days}, last value: {last_value}")
|
94
|
+
|
95
|
+
# Initialize PyGazpar client
|
96
|
+
client = pygazpar.Client(pygazpar.JsonWebDataSource(username=self._username, password=self._password))
|
97
|
+
|
98
|
+
try:
|
99
|
+
data = client.loadSince(pceIdentifier=self._pce_identifier, lastNDays=last_days, frequencies=[pygazpar.Frequency.DAILY])
|
100
|
+
except Exception:
|
101
|
+
errorMessage = f"Error while fetching data from GrDF: {traceback.format_exc()}"
|
102
|
+
Logger.warning(errorMessage)
|
103
|
+
data = {}
|
104
|
+
|
105
|
+
# Timezone
|
106
|
+
timezone = pytz.timezone(self._timezone)
|
107
|
+
|
108
|
+
# Compute and fill statistics.
|
109
|
+
daily = data.get(pygazpar.Frequency.DAILY.value)
|
110
|
+
statistics = []
|
111
|
+
total = last_value
|
112
|
+
for reading in daily:
|
113
|
+
# Parse date format DD/MM/YYYY into datetime.
|
114
|
+
date = datetime.strptime(reading[pygazpar.PropertyName.TIME_PERIOD.value], "%d/%m/%Y")
|
115
|
+
|
116
|
+
# Set the timezone
|
117
|
+
date = timezone.localize(date)
|
118
|
+
|
119
|
+
# Skip all readings before the last statistic date.
|
120
|
+
if date <= last_date:
|
121
|
+
Logger.debug(f"Skip date: {date} <= {last_date}")
|
122
|
+
continue
|
123
|
+
|
124
|
+
# Compute the total volume and energy
|
125
|
+
total += reading[property_name]
|
126
|
+
|
127
|
+
statistics.append({
|
128
|
+
"start": date.isoformat(),
|
129
|
+
"state": total,
|
130
|
+
"sum": total
|
131
|
+
})
|
132
|
+
|
133
|
+
# Publish statistics to Home Assistant
|
134
|
+
try:
|
135
|
+
await self._homeassistant.import_statistics(entity_id, "recorder", "gazpar2haws", unit_of_measurement, statistics)
|
136
|
+
except Exception:
|
137
|
+
errorMessage = f"Error while importing statistics to Home Assistant: {traceback.format_exc()}"
|
138
|
+
Logger.warning(errorMessage)
|
139
|
+
raise Exception(errorMessage)
|
gazpar2haws/version.py
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
import importlib.metadata
|
2
|
-
|
3
|
-
__version__ = importlib.metadata.version('gazpar2haws')
|
1
|
+
import importlib.metadata
|
2
|
+
|
3
|
+
__version__ = importlib.metadata.version('gazpar2haws')
|
@@ -1,21 +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.
|
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.
|
@@ -1,18 +1,37 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: gazpar2haws
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.8.dev5
|
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
|
-
License: MIT
|
5
|
+
License: MIT License
|
6
|
+
|
7
|
+
Copyright (c) 2024 Stéphane Senart
|
8
|
+
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
11
|
+
in the Software without restriction, including without limitation the rights
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
14
|
+
furnished to do so, subject to the following conditions:
|
15
|
+
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
17
|
+
copies or substantial portions of the Software.
|
18
|
+
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
25
|
+
SOFTWARE.
|
6
26
|
Author: Stéphane Senart
|
7
|
-
Requires-Python: >=3.
|
8
|
-
Classifier:
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
27
|
+
Requires-Python: >=3.9
|
28
|
+
Classifier: Programming Language :: Python :: 3.9
|
29
|
+
Classifier: Programming Language :: Python :: 3.10
|
30
|
+
Classifier: Programming Language :: Python :: 3.11
|
10
31
|
Classifier: Programming Language :: Python :: 3.12
|
11
|
-
|
12
|
-
Requires-Dist:
|
13
|
-
Requires-Dist:
|
14
|
-
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
15
|
-
Requires-Dist: websockets (>=14.1,<15.0)
|
32
|
+
Requires-Dist: pygazpar (>=1.2.7)
|
33
|
+
Requires-Dist: pyyaml (>=6.0.2)
|
34
|
+
Requires-Dist: websockets (>=14.1)
|
16
35
|
Description-Content-Type: text/markdown
|
17
36
|
|
18
37
|
# gazpar2haws
|
@@ -0,0 +1,11 @@
|
|
1
|
+
gazpar2haws/__init__.py,sha256=yzol8uZSBI7pIRGUmYJ6-vRBwkM4MI3IGf5cQpNsaFw,57
|
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.1.8.dev5.dist-info/LICENSE,sha256=ajApZPyhVx8AU9wN7DXeRGhoWFqY2ylBZUa5GRhTmok,1073
|
9
|
+
gazpar2haws-0.1.8.dev5.dist-info/METADATA,sha256=McJH2wquFssPNzHAx45s4nd6Ub5yWAb41ufnx_zB7uA,9455
|
10
|
+
gazpar2haws-0.1.8.dev5.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
|
11
|
+
gazpar2haws-0.1.8.dev5.dist-info/RECORD,,
|
CHANGELOG.md
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
# Changelog
|
2
|
-
|
3
|
-
All notable changes to this project will be documented in this file.
|
4
|
-
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
-
|
8
|
-
## [0.1.7] - 2025-01-05
|
9
|
-
|
10
|
-
### Fixed
|
11
|
-
|
12
|
-
[#18](https://github.com/ssenart/gazpar2haws/issues/18): Regression on DockerHub deployment.
|
13
|
-
|
14
|
-
## [0.1.6] - 2025-01-05
|
15
|
-
|
16
|
-
### Added
|
17
|
-
|
18
|
-
[#4](https://github.com/ssenart/gazpar2haws/issues/4): Deploy gazpar2haws as an HA add-on.
|
19
|
-
|
20
|
-
## [0.1.5] - 2025-01-04
|
21
|
-
|
22
|
-
### Added
|
23
|
-
|
24
|
-
[#15](https://github.com/ssenart/gazpar2haws/issues/15): Using HassIO, websocket endpoint is /core/websocket.
|
25
|
-
|
26
|
-
## [0.1.4] - 2025-01-04
|
27
|
-
|
28
|
-
### Fixed
|
29
|
-
|
30
|
-
[#13](https://github.com/ssenart/gazpar2haws/issues/13): Using HassIO, connection to the supervisor requires Authorization header.
|
31
|
-
|
32
|
-
## [0.1.3] - 2025-01-03
|
33
|
-
|
34
|
-
### Changed
|
35
|
-
|
36
|
-
[#11](https://github.com/ssenart/gazpar2haws/issues/11): Upgrade PyGazpar version to 1.2.6.
|
37
|
-
|
38
|
-
## [0.1.2] - 2024-12-30
|
39
|
-
|
40
|
-
### Added
|
41
|
-
|
42
|
-
[#2](https://github.com/ssenart/gazpar2haws/issues/2): DockerHub deployment.
|
43
|
-
|
44
|
-
### Fixed
|
45
|
-
|
46
|
-
[#9](https://github.com/ssenart/gazpar2haws/issues/9): Incorrect timezone info creates duplicate import.
|
47
|
-
|
48
|
-
[#6](https://github.com/ssenart/gazpar2haws/issues/6): The last meter value may be imported multiple times and cause the today value being wrong.
|
49
|
-
|
50
|
-
[#3](https://github.com/ssenart/gazpar2haws/issues/3): reset=false makes the meter import to restart from zero.
|
51
|
-
|
52
|
-
## [0.1.1] - 2024-12-22
|
53
|
-
|
54
|
-
### Added
|
55
|
-
|
56
|
-
[#1](https://github.com/ssenart/gazpar2haws/issues/1): Publish energy indicator in kWh.
|
57
|
-
|
58
|
-
## [0.1.0] - 2024-12-21
|
59
|
-
|
60
|
-
First version of the project.
|
@@ -1,12 +0,0 @@
|
|
1
|
-
CHANGELOG.md,sha256=VTHCw5lk7xiOq9TwQUWkePNYJq9d37n9NM0H9TAXwdI,1688
|
2
|
-
gazpar2haws/__init__.py,sha256=yzol8uZSBI7pIRGUmYJ6-vRBwkM4MI3IGf5cQpNsaFw,57
|
3
|
-
gazpar2haws/__main__.py,sha256=g8xk0x_kprBHKHLzgf9y9EY2_gKC9V3k4z0n-EDTd-I,3253
|
4
|
-
gazpar2haws/bridge.py,sha256=plcXR8y6lH84OSHuUOogNcbM7uua24inoF9SSERKGHo,3539
|
5
|
-
gazpar2haws/config_utils.py,sha256=D0lu-3KY-vLEyD2vDN05UABkMnpMJjqw1RuDJVrkGFs,2123
|
6
|
-
gazpar2haws/gazpar.py,sha256=8yrMzs9HfMSgq3HPeJs64SHWb5RWm7ZL3tgfzhlwHFQ,5754
|
7
|
-
gazpar2haws/haws.py,sha256=H0Qa01Qtsn3QdnGqIGkXE-Ympf7MSXkbFwAbzaMAodM,6895
|
8
|
-
gazpar2haws/version.py,sha256=ebdTNl4h0hNKmN3Gbs592VJsYbMmrkB47WyZMJevaQo,86
|
9
|
-
gazpar2haws-0.1.7.dist-info/LICENSE,sha256=G6JttcnlwcRHYzIcDflSGOVrHTtaP3BEegM2lH00xHw,1094
|
10
|
-
gazpar2haws-0.1.7.dist-info/METADATA,sha256=DCB5_09TfIhQMsEGI91e8aBmNzpujnB01sloo6ZEkqo,8274
|
11
|
-
gazpar2haws-0.1.7.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
12
|
-
gazpar2haws-0.1.7.dist-info/RECORD,,
|