Skip to content

Commit b84d7a1

Browse files
committed
Add real life simulator example.
1 parent 5666da1 commit b84d7a1

3 files changed

Lines changed: 178 additions & 2 deletions

File tree

examples/contrib/heatpump.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#!/usr/bin/env python3
2+
"""Controlling a Daikin Altherma 3 heatpump, and presenting data in Home Assistant.
3+
4+
This app is used to control a Daikin heatpump, by reading the temperatures
5+
of deposit for the underfloor heating.
6+
7+
Based on the temperatures and the time of the day (electricity periods P1, P2 and P3) it
8+
is determined to start/stop the heat pump (Daikin Altherma3)
9+
10+
The data is made available in modbus and observed by a Home Assistant server, and presented
11+
to the home users.
12+
13+
Schematic of deposit:
14+
15+
+--------+
16+
A) --> -+ | F)
17+
| E) +--+- --> C)
18+
| | |
19+
B) <-- -+ | |
20+
| +--+- <-- D)
21+
+--------+
22+
23+
A) Intake from heat pump (hot)
24+
B) Return to heat pump (cold)
25+
C) Output to underfloor circulation pump (hot)
26+
D) return from underfloor pipes (cold)
27+
D) Output from tank (hot)
28+
F) Thermostatic mixing valve (output is fixed at max 30 degrees)
29+
30+
The 4 point are measured.
31+
32+
The algorithm is quite simple:
33+
34+
heat pump is activated when:
35+
- C) is less than a setpoint (typically 25 degrees)
36+
- Time in P3 (night) and B) is less than a setpoint (typically 45 degrees)
37+
38+
Difference between A) and heat pump setpoint shows the loss in the connection pipes.
39+
Difference between D) and underfloor heating thermostats show if heat is required.
40+
41+
Remark: the return from the underfloor heating is connected to a thermostatic valve,
42+
on the output to the circulation pump. This allows to e.g. heat the tank to 50 degrees,
43+
while still circulating 30 degrees. Using the thermostatic valve, dramatically reduces
44+
the number of times the heat pump is started (when started it runs longer) and thus saving
45+
electricity.
46+
47+
The thermo meters are read via 1-wire protocol.
48+
49+
usage::
50+
51+
heatpump.py [-h]
52+
[--log {critical,error,warning,info,debug}]
53+
[--port PORT]
54+
[--host HOST]
55+
56+
-h, --help
57+
show this help message and exit
58+
-l, --log {critical,error,warning,info,debug}
59+
set log level, default is info
60+
-p, --port PORT
61+
set port
62+
"""
63+
import argparse
64+
import asyncio
65+
66+
from pymodbus import Log, pymodbus_apply_logging_config
67+
from pymodbus.server import ModbusTcpServer
68+
from pymodbus.simulator import DataType, SimData, SimDevice
69+
70+
71+
def get_commandline(cmdline: list[str] | None = None):
72+
"""Read and check command line arguments."""
73+
parser = argparse.ArgumentParser(description="server_update")
74+
parser.add_argument(
75+
"-l",
76+
"--log",
77+
choices=["critical", "error", "warning", "info", "debug"],
78+
help="set log level, default is info",
79+
dest="log",
80+
default="info",
81+
type=str,
82+
)
83+
parser.add_argument(
84+
"-p",
85+
"--port",
86+
help="set port, default is 5020",
87+
dest="port",
88+
default=5020,
89+
type=str,
90+
)
91+
args = parser.parse_args(cmdline)
92+
pymodbus_apply_logging_config(args.log.upper())
93+
return args
94+
95+
96+
def setup_updating_server(cmdline=None):
97+
"""Run server setup."""
98+
args = get_commandline(cmdline=cmdline)
99+
100+
device = SimDevice(1, simdata=[
101+
# bit 0, Heat pump requested active
102+
# bit 1, Alive signals (toggles every minute)
103+
SimData(1, DataType.BITS, readonly=True),
104+
# 4 thermo meters A,B,C,D
105+
SimData(2, count=4, datatype=DataType.FLOAT32, readonly=True),
106+
# Setpoint to activate heat pump (output to underfloor pipes)
107+
# Setpoint to run heat pump at night (return to heat pump)
108+
SimData(10, count=2, datatype=DataType.FLOAT32),
109+
])
110+
server = ModbusTcpServer(
111+
device,
112+
address=("", args.port),
113+
context=device,
114+
)
115+
return server
116+
117+
118+
async def updating_task(server):
119+
"""Update values in server.
120+
121+
This task runs continuously beside the server
122+
It will increment some values each two seconds.
123+
124+
It should be noted that async_getValues and async_setValues are not safe
125+
against concurrent use.
126+
"""
127+
func_code = 3
128+
device_id = 0x01
129+
address = 0x10
130+
count = 6
131+
132+
# set values to zero
133+
values = await server.async_getValues(device_id, func_code, address, count=count)
134+
values = [0 for v in values]
135+
await server.async_setValues(device_id, func_code, address, values)
136+
137+
txt = (
138+
f"updating_task: started: initialised values: {values!s} at address {address!s}"
139+
)
140+
print(txt)
141+
Log.debug(txt)
142+
143+
# incrementing loop
144+
while True:
145+
await asyncio.sleep(2)
146+
147+
values = await server.async_getValues(device_id, func_code, address, count=count)
148+
values = [v + 1 for v in values]
149+
await server.async_setValues(device_id, func_code, address, values)
150+
151+
txt = f"updating_task: incremented values: {values!s} at address {address!s}"
152+
print(txt)
153+
Log.debug(txt)
154+
155+
156+
async def run_updating_server(server):
157+
"""Start updating_task concurrently with the current task."""
158+
task = asyncio.create_task(updating_task(server))
159+
task.set_name("example updating task")
160+
await server.serve_forever() # start the server
161+
task.cancel()
162+
163+
164+
async def main(cmdline=None):
165+
"""Combine setup and run."""
166+
server = setup_updating_server(cmdline=cmdline)
167+
await run_updating_server(server)
168+
169+
170+
if __name__ == "__main__":
171+
asyncio.run(main(), debug=True)

examples/server_updating.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
An example of an asynchronous server and
55
a task that runs continuously alongside the server and updates values.
66
7+
A real world example controlling a heatpump can be found at
8+
9+
example/contrib/heatpump.py
10+
711
usage::
812
913
server_updating.py [-h]

pymodbus/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@
66
__all__ = [
77
"ExceptionResponse",
88
"FramerType",
9+
"Log",
910
"ModbusDeviceIdentification",
1011
"ModbusException",
1112
"__version__",
1213
"__version_full__",
13-
"pymodbus_apply_logging_config"
14+
"pymodbus_apply_logging_config",
1415
]
1516

1617
from .exceptions import ModbusException
1718
from .framer import FramerType
18-
from .logging import pymodbus_apply_logging_config
19+
from .logging import Log, pymodbus_apply_logging_config
1920
from .pdu import ExceptionResponse
2021
from .pdu.device import ModbusDeviceIdentification
2122

0 commit comments

Comments
 (0)