juham-automation 0.0.12__py3-none-any.whl → 0.2.11__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.
- juham_automation/__init__.py +8 -6
- juham_automation/automation/__init__.py +6 -6
- juham_automation/automation/energybalancer.py +281 -0
- juham_automation/automation/energycostcalculator.py +112 -68
- juham_automation/automation/heatingoptimizer.py +971 -0
- juham_automation/automation/leakdetector.py +162 -0
- juham_automation/automation/watercirculator.py +1 -20
- juham_automation/japp.py +4 -2
- juham_automation/ts/__init__.py +3 -1
- juham_automation/ts/electricityprice_ts.py +1 -1
- juham_automation/ts/energybalancer_ts.py +73 -0
- juham_automation/ts/energycostcalculator_ts.py +3 -1
- juham_automation/ts/log_ts.py +19 -16
- juham_automation/ts/power_ts.py +17 -14
- juham_automation/ts/powermeter_ts.py +3 -5
- juham_automation/ts/powerplan_ts.py +37 -18
- juham_automation-0.2.11.dist-info/METADATA +198 -0
- juham_automation-0.2.11.dist-info/RECORD +24 -0
- {juham_automation-0.0.12.dist-info → juham_automation-0.2.11.dist-info}/WHEEL +1 -1
- {juham_automation-0.0.12.dist-info → juham_automation-0.2.11.dist-info}/entry_points.txt +3 -2
- juham_automation/automation/hotwateroptimizer.py +0 -567
- juham_automation/automation/powermeter_simulator.py +0 -139
- juham_automation/automation/spothintafi.py +0 -140
- juham_automation-0.0.12.dist-info/METADATA +0 -109
- juham_automation-0.0.12.dist-info/RECORD +0 -23
- {juham_automation-0.0.12.dist-info → juham_automation-0.2.11.dist-info/licenses}/LICENSE.rst +0 -0
- {juham_automation-0.0.12.dist-info → juham_automation-0.2.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Leak Detector based on Motion Detection and Water Meter Monitoring.
|
|
3
|
+
|
|
4
|
+
This class listens to water meter and motion sensor topics to detect potential water leaks.
|
|
5
|
+
If a leak is detected, the system triggers a leak alarm.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
from typing_extensions import override
|
|
11
|
+
from masterpiece.mqtt import MqttMsg
|
|
12
|
+
from juham_core.timeutils import timestamp
|
|
13
|
+
from juham_core import Juham
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LeakDetector(Juham):
|
|
17
|
+
"""
|
|
18
|
+
Water Leak Detector Class
|
|
19
|
+
|
|
20
|
+
Listens to water meter and motion sensor topics to identify potential water leaks.
|
|
21
|
+
If water consumption is detected without corresponding motion, or if water usage
|
|
22
|
+
remains constant for prolonged periods, a leak alarm is triggered.
|
|
23
|
+
|
|
24
|
+
Detection considers the time since the last motion detection and compares it to
|
|
25
|
+
the configured leak detection period, which is the maximum runtime of water-consuming
|
|
26
|
+
appliances.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
_LEAKDETECTOR: str = "leakdetector"
|
|
30
|
+
_LEAKDETECTOR_ATTRS: list[str] = [
|
|
31
|
+
"watermeter_topic",
|
|
32
|
+
"motion_topic",
|
|
33
|
+
"motion_last_detected_ts",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
watermeter_topic: str = "watermeter"
|
|
37
|
+
motion_topic: str = "motion"
|
|
38
|
+
leak_detection_period: float = (
|
|
39
|
+
3 * 3600.0
|
|
40
|
+
) # Maximum runtime for appliances, in seconds
|
|
41
|
+
location: str = "home"
|
|
42
|
+
conseq_zero_periods: int = (
|
|
43
|
+
60 # this many subsequent zero flow reports imply no leak
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def __init__(self, name: str = "leakdetector") -> None:
|
|
47
|
+
"""
|
|
48
|
+
Initialize the leak detector.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
name (str, optional): Name of the detector instance. Defaults to "leakdetector".
|
|
52
|
+
"""
|
|
53
|
+
super().__init__(name)
|
|
54
|
+
self.motion_last_detected_ts: float = timestamp()
|
|
55
|
+
self.watermeter_full_topic: str = self.make_topic_name(self.watermeter_topic)
|
|
56
|
+
self.motion_full_topic: str = self.make_topic_name(self.motion_topic)
|
|
57
|
+
self.leak_detected: bool = False
|
|
58
|
+
self.zero_usage_periods_count: int = 0
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Handle MQTT connection. Subscribe to water meter and motion topics.
|
|
64
|
+
"""
|
|
65
|
+
super().on_connect(client, userdata, flags, rc)
|
|
66
|
+
if rc == 0:
|
|
67
|
+
self.subscribe(self.watermeter_full_topic)
|
|
68
|
+
self.subscribe(self.motion_full_topic)
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Process incoming MQTT messages for water meter and motion topics.
|
|
74
|
+
"""
|
|
75
|
+
payload = json.loads(msg.payload.decode())
|
|
76
|
+
if msg.topic == self.watermeter_full_topic:
|
|
77
|
+
self.process_water_meter_data(payload)
|
|
78
|
+
elif msg.topic == self.motion_full_topic:
|
|
79
|
+
self.process_motion_data(payload)
|
|
80
|
+
else:
|
|
81
|
+
super().on_message(client, userdata, msg)
|
|
82
|
+
|
|
83
|
+
def detect_activity(self, current_ts: float) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Check if activity (motion) has been detected within the leak detection period.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
current_ts (float): Current timestamp.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
bool: True if activity detected within the period, False otherwise.
|
|
92
|
+
"""
|
|
93
|
+
elapsed_time = current_ts - self.motion_last_detected_ts
|
|
94
|
+
return elapsed_time < self.leak_detection_period
|
|
95
|
+
|
|
96
|
+
def publish_leak_status(self, current_ts: float, leak_suspected: bool) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Publish the leak detection status.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
current_ts (float): Current timestamp.
|
|
102
|
+
leak_suspected (bool): Whether a leak is suspected.
|
|
103
|
+
"""
|
|
104
|
+
status : dict[str, Any] = {
|
|
105
|
+
"location": self.location,
|
|
106
|
+
"sensor": self.name,
|
|
107
|
+
"leak_suspected": leak_suspected,
|
|
108
|
+
"ts": current_ts,
|
|
109
|
+
}
|
|
110
|
+
payload = json.dumps(status)
|
|
111
|
+
self.publish(self.watermeter_full_topic, payload, qos=1, retain=False)
|
|
112
|
+
|
|
113
|
+
def process_water_meter_data(self, data: dict[str, float]) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Handle water meter data and apply leak detection logic.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
data (dict): Water meter data containing flow rate and timestamp.
|
|
119
|
+
"""
|
|
120
|
+
if "active_lpm" in data and "ts" in data:
|
|
121
|
+
flow_rate = data["active_lpm"]
|
|
122
|
+
current_ts = data["ts"]
|
|
123
|
+
|
|
124
|
+
if flow_rate > 0.0:
|
|
125
|
+
if not self.detect_activity(current_ts):
|
|
126
|
+
if not self.leak_detected:
|
|
127
|
+
self.leak_detected = True
|
|
128
|
+
readable :str = datetime.fromtimestamp(self.motion_last_detected_ts).strftime("%Y-%m-%d %H:%M:%S")
|
|
129
|
+
self.warning(f"LEAK SUSPECT", f"Flow {flow_rate} lpm, last detected motion {readable}")
|
|
130
|
+
|
|
131
|
+
self.zero_usage_periods_count = 0
|
|
132
|
+
else:
|
|
133
|
+
self.zero_usage_periods_count += 1
|
|
134
|
+
if self.zero_usage_periods_count > self.conseq_zero_periods:
|
|
135
|
+
self.leak_detected = False
|
|
136
|
+
self.motion_last_detected_ts = current_ts
|
|
137
|
+
self.publish_leak_status(current_ts, self.leak_detected)
|
|
138
|
+
|
|
139
|
+
def process_motion_data(self, data: dict[str, float]) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Update the last detected motion timestamp.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
data (dict): Motion sensor data containing timestamp.
|
|
145
|
+
"""
|
|
146
|
+
if "motion" in data and data["motion"]:
|
|
147
|
+
self.motion_last_detected_ts = data["ts"]
|
|
148
|
+
|
|
149
|
+
@override
|
|
150
|
+
def to_dict(self) -> dict[str, Any]:
|
|
151
|
+
data = super().to_dict()
|
|
152
|
+
attributes = {attr: getattr(self, attr) for attr in self._LEAKDETECTOR_ATTRS}
|
|
153
|
+
data[self._LEAKDETECTOR] = attributes
|
|
154
|
+
return data
|
|
155
|
+
|
|
156
|
+
@override
|
|
157
|
+
def from_dict(self, data: dict[str, Any]) -> None:
|
|
158
|
+
super().from_dict(data)
|
|
159
|
+
if self._LEAKDETECTOR in data:
|
|
160
|
+
attributes = data[self._LEAKDETECTOR]
|
|
161
|
+
for attr in self._LEAKDETECTOR_ATTRS:
|
|
162
|
+
setattr(self, attr, attributes.get(attr, None))
|
|
@@ -33,7 +33,7 @@ class WaterCirculator(Juham):
|
|
|
33
33
|
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
|
-
uptime =
|
|
36
|
+
uptime = 1800 # half an hour
|
|
37
37
|
min_temperature = 37
|
|
38
38
|
|
|
39
39
|
def __init__(self, name: str, temperature_sensor: str) -> None:
|
|
@@ -84,9 +84,6 @@ class WaterCirculator(Juham):
|
|
|
84
84
|
|
|
85
85
|
self.water_temperature = m["temperature"]
|
|
86
86
|
self.water_temperature_updated = ts_utc_now
|
|
87
|
-
# self.info(
|
|
88
|
-
# f"Temperature of circulating water updated to {self.water_temperature} C"
|
|
89
|
-
# )
|
|
90
87
|
|
|
91
88
|
def on_motion_sensor(self, m: dict[str, dict[str, Any]], ts_utc_now: float) -> None:
|
|
92
89
|
"""Control the water cirulator bump.
|
|
@@ -103,14 +100,9 @@ class WaterCirculator(Juham):
|
|
|
103
100
|
motion: bool = bool(m["motion"])
|
|
104
101
|
|
|
105
102
|
if motion or vibration:
|
|
106
|
-
# self.debug(f"Life form detected in {sensor}")
|
|
107
|
-
# honey I'm home
|
|
108
103
|
if not self.current_motion:
|
|
109
104
|
if self.water_temperature > self.min_temperature:
|
|
110
105
|
self.publish_relay_state(0)
|
|
111
|
-
# self.debug(
|
|
112
|
-
# f"Circulator: motion detected but water warm already {self.water_temperature} > {self.min_temperature} C"
|
|
113
|
-
# )
|
|
114
106
|
else:
|
|
115
107
|
self.current_motion = True
|
|
116
108
|
self.relay_started_ts = ts_utc_now
|
|
@@ -122,10 +114,6 @@ class WaterCirculator(Juham):
|
|
|
122
114
|
else:
|
|
123
115
|
self.publish_relay_state(1)
|
|
124
116
|
self.relay_started_ts = ts_utc_now
|
|
125
|
-
# self.debug(
|
|
126
|
-
# f"Circulator pump has been running for {int(ts_utc_now - self.relay_started_ts)/60} minutes",
|
|
127
|
-
# " ",
|
|
128
|
-
# )
|
|
129
117
|
else:
|
|
130
118
|
if self.current_motion or not self.initialized:
|
|
131
119
|
elapsed: float = ts_utc_now - self.relay_started_ts
|
|
@@ -139,15 +127,8 @@ class WaterCirculator(Juham):
|
|
|
139
127
|
self.initialized = True
|
|
140
128
|
else:
|
|
141
129
|
self.publish_relay_state(1)
|
|
142
|
-
# self.debug(
|
|
143
|
-
# f"Circulator bump stop countdown {int(self.uptime - (ts_utc_now - self.relay_started_ts ))/60} min"
|
|
144
|
-
# )
|
|
145
130
|
else:
|
|
146
131
|
self.publish_relay_state(0)
|
|
147
|
-
# self.debug(
|
|
148
|
-
# f"Circulator bump off already, temperature {self.water_temperature} C",
|
|
149
|
-
# "",
|
|
150
|
-
# )
|
|
151
132
|
|
|
152
133
|
def publish_relay_state(self, state: int) -> None:
|
|
153
134
|
"""Publish power status.
|
juham_automation/japp.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from juham_automation.automation.energybalancer import EnergyBalancer
|
|
1
2
|
from masterpiece import Application
|
|
2
3
|
from juham_core import Juham
|
|
3
4
|
|
|
@@ -5,10 +6,10 @@ from .ts import ForecastTs
|
|
|
5
6
|
from .ts import PowerTs
|
|
6
7
|
from .ts import PowerPlanTs
|
|
7
8
|
from .ts import PowerMeterTs
|
|
9
|
+
from .ts import EnergyBalancerTs
|
|
8
10
|
from .ts import LogTs
|
|
9
11
|
from .ts import EnergyCostCalculatorTs
|
|
10
12
|
from .ts import ElectricityPriceTs
|
|
11
|
-
from .automation import SpotHintaFi
|
|
12
13
|
from .automation import EnergyCostCalculator
|
|
13
14
|
|
|
14
15
|
|
|
@@ -38,10 +39,11 @@ class JApp(Application):
|
|
|
38
39
|
self.add(PowerPlanTs())
|
|
39
40
|
self.add(PowerMeterTs())
|
|
40
41
|
self.add(LogTs())
|
|
41
|
-
self.add(SpotHintaFi())
|
|
42
42
|
self.add(EnergyCostCalculator())
|
|
43
43
|
self.add(EnergyCostCalculatorTs())
|
|
44
44
|
self.add(ElectricityPriceTs())
|
|
45
|
+
self.add(EnergyBalancer())
|
|
46
|
+
self.add(EnergyBalancerTs())
|
|
45
47
|
|
|
46
48
|
@classmethod
|
|
47
49
|
def register(cls) -> None:
|
juham_automation/ts/__init__.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Description
|
|
3
3
|
===========
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Time series recorders for Juha's Ultimate Home Automation classes.
|
|
6
6
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -13,6 +13,7 @@ from .powerplan_ts import PowerPlanTs
|
|
|
13
13
|
from .powermeter_ts import PowerMeterTs
|
|
14
14
|
from .electricityprice_ts import ElectricityPriceTs
|
|
15
15
|
from .forecast_ts import ForecastTs
|
|
16
|
+
from .energybalancer_ts import EnergyBalancerTs
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
"EnergyCostCalculatorTs",
|
|
@@ -22,4 +23,5 @@ __all__ = [
|
|
|
22
23
|
"PowerPlanTs",
|
|
23
24
|
"PowerMeterTs",
|
|
24
25
|
"ElectricityPriceTs",
|
|
26
|
+
"EnergyBalancerTs",
|
|
25
27
|
]
|
|
@@ -32,7 +32,7 @@ class ElectricityPriceTs(JuhamTs):
|
|
|
32
32
|
super().on_message(client, userdata, msg)
|
|
33
33
|
|
|
34
34
|
def on_spot(self, m: dict[Any, Any]) -> None:
|
|
35
|
-
"""Write
|
|
35
|
+
"""Write spot electricity prices to time series database.
|
|
36
36
|
|
|
37
37
|
Args:
|
|
38
38
|
m (dict): holding hourlys spot electricity prices
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing_extensions import override
|
|
4
|
+
|
|
5
|
+
from masterpiece.mqtt import MqttMsg
|
|
6
|
+
|
|
7
|
+
from juham_core import JuhamTs
|
|
8
|
+
from juham_core.timeutils import epoc2utc
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EnergyBalancerTs(JuhamTs):
|
|
12
|
+
"""Record energy balance data to time series database.
|
|
13
|
+
|
|
14
|
+
This class listens the "energybalance" MQTT topic and records the
|
|
15
|
+
messages to time series database.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, name: str = "energybalancer_ts") -> None:
|
|
19
|
+
"""Construct record object with the given name."""
|
|
20
|
+
|
|
21
|
+
super().__init__(name)
|
|
22
|
+
self.topic_in_status = self.make_topic_name("energybalance/status")
|
|
23
|
+
self.topic_in_diagnostics = self.make_topic_name("energybalance/diagnostics")
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
|
|
27
|
+
super().on_connect(client, userdata, flags, rc)
|
|
28
|
+
if rc == 0:
|
|
29
|
+
self.subscribe(self.topic_in_status)
|
|
30
|
+
self.subscribe(self.topic_in_diagnostics)
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
|
34
|
+
if msg.topic == self.topic_in_status:
|
|
35
|
+
self.on_status(json.loads(msg.payload.decode()))
|
|
36
|
+
elif msg.topic == self.topic_in_diagnostics:
|
|
37
|
+
self.on_diagnostics(json.loads(msg.payload.decode()))
|
|
38
|
+
else:
|
|
39
|
+
super().on_message(client, userdata, msg)
|
|
40
|
+
|
|
41
|
+
def on_status(self, m: dict[str, Any]) -> None:
|
|
42
|
+
"""Handle energybalance message.
|
|
43
|
+
Args:
|
|
44
|
+
m (dict[str, Any]): Message from energybalance topic.
|
|
45
|
+
"""
|
|
46
|
+
if not "Power" in m or not "Timestamp" in m:
|
|
47
|
+
self.error(f"INVALID STATUS msg {m}")
|
|
48
|
+
return
|
|
49
|
+
point = (
|
|
50
|
+
self.measurement("energybalance")
|
|
51
|
+
.tag("Unit", m["Unit"])
|
|
52
|
+
.field("Mode", m["Mode"])
|
|
53
|
+
.field("Power", float(m["Power"]))
|
|
54
|
+
.time(epoc2utc(m["Timestamp"]))
|
|
55
|
+
)
|
|
56
|
+
self.write(point)
|
|
57
|
+
|
|
58
|
+
def on_diagnostics(self, m: dict[str, Any]) -> None:
|
|
59
|
+
"""Handle energybalance diagnostics.
|
|
60
|
+
Args:
|
|
61
|
+
m (dict[str, Any]): Message from energybalance topic.
|
|
62
|
+
"""
|
|
63
|
+
if not "Timestamp" in m:
|
|
64
|
+
self.error(f"INVALID DIAGNOSTICS msg {m}")
|
|
65
|
+
return
|
|
66
|
+
point = (
|
|
67
|
+
self.measurement("energybalance")
|
|
68
|
+
.tag("EnergyBalancer", m["EnergyBalancer"])
|
|
69
|
+
.field("CurrentBalance", m["CurrentBalance"])
|
|
70
|
+
.field("NeededBalance", m["NeededBalance"])
|
|
71
|
+
.time(epoc2utc(m["Timestamp"]))
|
|
72
|
+
)
|
|
73
|
+
self.write(point)
|
|
@@ -26,10 +26,12 @@ class EnergyCostCalculatorTs(JuhamTs):
|
|
|
26
26
|
|
|
27
27
|
@override
|
|
28
28
|
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
|
29
|
-
super().on_message(client, userdata, msg)
|
|
30
29
|
if msg.topic == self.topic_net_energy_balance:
|
|
31
30
|
m = json.loads(msg.payload.decode())
|
|
32
31
|
self.record_powerconsumption(m)
|
|
32
|
+
else:
|
|
33
|
+
super().on_message(client, userdata, msg)
|
|
34
|
+
|
|
33
35
|
|
|
34
36
|
def record_powerconsumption(self, m: dict[str, Any]) -> None:
|
|
35
37
|
"""Record powerconsumption
|
juham_automation/ts/log_ts.py
CHANGED
|
@@ -39,19 +39,22 @@ class LogTs(JuhamTs):
|
|
|
39
39
|
|
|
40
40
|
@override
|
|
41
41
|
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
if msg.topic == self.topic_name:
|
|
43
|
+
m = json.loads(msg.payload.decode())
|
|
44
|
+
ts = epoc2utc(m["Timestamp"])
|
|
45
|
+
|
|
46
|
+
point = (
|
|
47
|
+
self.measurement("log")
|
|
48
|
+
.tag("class", m["Class"])
|
|
49
|
+
.field("source", m["Source"])
|
|
50
|
+
.field("msg", m["Msg"])
|
|
51
|
+
.field("details", m["Details"])
|
|
52
|
+
.field("Timestamp", m["Timestamp"])
|
|
53
|
+
.time(ts)
|
|
54
|
+
)
|
|
55
|
+
try:
|
|
56
|
+
self.write(point)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"ERROR: Cannot write log event {m['Msg']} {e}")
|
|
59
|
+
else:
|
|
60
|
+
super().on_message(client, userdata, msg)
|
juham_automation/ts/power_ts.py
CHANGED
|
@@ -33,17 +33,20 @@ class PowerTs(JuhamTs):
|
|
|
33
33
|
|
|
34
34
|
This method is called upon new arrived message.
|
|
35
35
|
"""
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
if msg.topic == self.topic_name:
|
|
37
|
+
m = json.loads(msg.payload.decode())
|
|
38
|
+
if not "Unit" in m:
|
|
39
|
+
return
|
|
40
|
+
unit = m["Unit"]
|
|
41
|
+
ts = m["Timestamp"]
|
|
42
|
+
state = m["State"]
|
|
43
|
+
point = (
|
|
44
|
+
self.measurement("power")
|
|
45
|
+
.tag("unit", unit)
|
|
46
|
+
.field("state", state)
|
|
47
|
+
.time(epoc2utc(ts))
|
|
48
|
+
)
|
|
49
|
+
self.write(point)
|
|
50
|
+
else:
|
|
51
|
+
super().on_message(client, userdata, msg)
|
|
52
|
+
|
|
@@ -26,11 +26,12 @@ class PowerMeterTs(JuhamTs):
|
|
|
26
26
|
|
|
27
27
|
@override
|
|
28
28
|
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
|
29
|
-
super().on_message(client, userdata, msg)
|
|
30
29
|
if msg.topic == self.power_topic:
|
|
31
30
|
m = json.loads(msg.payload.decode())
|
|
32
31
|
self.record_power(m)
|
|
33
|
-
|
|
32
|
+
else:
|
|
33
|
+
super().on_message(client, userdata, msg)
|
|
34
|
+
|
|
34
35
|
def record_power(self, em: dict[str, Any]) -> None:
|
|
35
36
|
"""Write from the power (energy) meter to the time
|
|
36
37
|
series database accordingly.
|
|
@@ -50,9 +51,6 @@ class PowerMeterTs(JuhamTs):
|
|
|
50
51
|
)
|
|
51
52
|
try:
|
|
52
53
|
self.write(point)
|
|
53
|
-
# self.debug(
|
|
54
|
-
# f"PowerMeter event recorded {epoc2utc(em['timestamp'])} - {em['real_a']} {em['real_b']} {em['real_c']}"
|
|
55
|
-
# )
|
|
56
54
|
except Exception as e:
|
|
57
55
|
self.error(f"Writing to influx failed {str(e)}")
|
|
58
56
|
|
|
@@ -25,21 +25,40 @@ class PowerPlanTs(JuhamTs):
|
|
|
25
25
|
|
|
26
26
|
@override
|
|
27
27
|
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
28
|
+
if msg.topic == self.powerplan_topic:
|
|
29
|
+
m = json.loads(msg.payload.decode())
|
|
30
|
+
schedule = m["Schedule"]
|
|
31
|
+
uoi = m["UOI"]
|
|
32
|
+
ts = m["Timestamp"]
|
|
33
|
+
|
|
34
|
+
# optional fields
|
|
35
|
+
tempForecast = m.get("NextDayTemperature")
|
|
36
|
+
solarForecast = m.get("NextDaySolarpower")
|
|
37
|
+
minTemp = m.get("MinTempLimit")
|
|
38
|
+
maxTemp = m.get("MaxTempLimit")
|
|
39
|
+
|
|
40
|
+
point = (
|
|
41
|
+
self.measurement("powerplan")
|
|
42
|
+
.tag("unit", m["Unit"])
|
|
43
|
+
.field("state", m["State"]) # 1 on, 0 off
|
|
44
|
+
.field("name", m["Unit"]) # e.g main_boiler
|
|
45
|
+
.field("type", "C") # C=consumption, S = supply
|
|
46
|
+
.field("power", 16.0) # kW
|
|
47
|
+
.field("Schedule", schedule) # figures of merit
|
|
48
|
+
.field("UOI", float(uoi)) # Utilitzation Optimizing Index
|
|
49
|
+
.time(epoc2utc(ts))
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Add optional fields only if they are present in the message
|
|
53
|
+
if minTemp is not None:
|
|
54
|
+
point = point.field("MinTemp", float(minTemp))
|
|
55
|
+
if maxTemp is not None:
|
|
56
|
+
point = point.field("MaxTemp", float(maxTemp))
|
|
57
|
+
if tempForecast is not None:
|
|
58
|
+
point = point.field("TempForecast", float(tempForecast))
|
|
59
|
+
if solarForecast is not None:
|
|
60
|
+
point = point.field("SolarForecast", float(solarForecast))
|
|
61
|
+
|
|
62
|
+
self.write(point)
|
|
63
|
+
else:
|
|
64
|
+
super().on_message(client, userdata, msg)
|