Skip to content

Commit 5021af8

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

3 files changed

Lines changed: 184 additions & 2 deletions

File tree

examples/heatpump.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
E) Output from tank (hot)
28+
F) Thermostatic mixing valve (output is fixed at max 30 degrees)
29+
30+
The 5 point are measured.
31+
32+
The algorithm is quite simple:
33+
34+
Difference between A) and heat pump setpoint shows the loss in the connection pipes.
35+
Difference between D) and underfloor heating thermostats show if heat is required.
36+
37+
Remark: the return from the underfloor heating is connected to a thermostatic valve,
38+
on the output to the circulation pump. This allows to e.g. heat the tank to 50 degrees,
39+
while still circulating 30 degrees. Using the thermostatic valve, dramatically reduces
40+
the number of times the heat pump is started (when started it runs longer) and thus saving
41+
electricity.
42+
43+
The thermo meters are read via 1-wire protocol.
44+
45+
usage::
46+
47+
heatpump.py [-h]
48+
[--log {critical,error,warning,info,debug}]
49+
[--port <PORT>]
50+
51+
-h, --help
52+
show this help message and exit
53+
-l, --log {critical,error,warning,info,debug}
54+
set log level, default is info
55+
-p, --port PORT
56+
set port to listen on
57+
"""
58+
import argparse
59+
import asyncio
60+
61+
from pymodbus import Log, pymodbus_apply_logging_config
62+
from pymodbus.server import ModbusTcpServer
63+
from pymodbus.simulator import DataType, SimData, SimDevice
64+
from pymodbus.constants import ExcCodes
65+
66+
DEVICE_ID = 1
67+
68+
def get_commandline(cmdline: list[str] | None = None) -> argparse.Namespace:
69+
"""Read and check command line arguments."""
70+
parser = argparse.ArgumentParser(description="server_update")
71+
parser.add_argument(
72+
"-l",
73+
"--log",
74+
choices=["critical", "error", "warning", "info", "debug"],
75+
help="set log level, default is info",
76+
dest="log",
77+
default="info",
78+
type=str,
79+
)
80+
parser.add_argument(
81+
"-p",
82+
"--port",
83+
help="set listen port, default is 5020",
84+
dest="port",
85+
default=5020,
86+
type=str,
87+
)
88+
args = parser.parse_args(cmdline)
89+
pymodbus_apply_logging_config(args.log.upper())
90+
return args
91+
92+
93+
def setup_updating_server(cmdline=None):
94+
"""Run server setup."""
95+
args = get_commandline(cmdline=cmdline)
96+
97+
device = SimDevice(DEVICE_ID, simdata=[
98+
# bit 0, Heat pump requested active
99+
# bit 1, Alive signal (toggles every minute)
100+
# bit 2, Call signal (toggles with every call)
101+
# bit 3, Heat pump set to be active
102+
SimData(0, DataType.BITS, readonly=True),
103+
# 4 thermo meters A,B,C,D
104+
SimData(1, count=4, datatype=DataType.FLOAT32, readonly=True),
105+
# Setpoint to activate heat pump (output to underfloor pipes)
106+
# Setpoint to run heat pump at night (return to heat pump)
107+
SimData(9, count=2, datatype=DataType.FLOAT32),
108+
])
109+
server = ModbusTcpServer(
110+
device,
111+
address=("", args.port),
112+
context=device,
113+
)
114+
return server
115+
116+
117+
async def updating_task(server):
118+
"""Update values in server.
119+
120+
This task runs continuously beside the server
121+
It will increment some values each second.
122+
"""
123+
func_code = 3
124+
device_id = 0x01
125+
address = 0x10
126+
count = 6
127+
128+
# set values to zero
129+
values = await server.async_getValues(device_id, func_code, address, count=count)
130+
values = [0 for v in values]
131+
await server.async_setValues(device_id, func_code, address, values)
132+
133+
txt = (
134+
f"updating_task: started: initialised values: {values!s} at address {address!s}"
135+
)
136+
print(txt)
137+
Log.debug(txt)
138+
139+
# incrementing loop
140+
while True:
141+
await asyncio.sleep(1)
142+
143+
values = await server.async_getValues(device_id, func_code, address, count=count)
144+
values = [v + 1 for v in values]
145+
await server.async_setValues(device_id, func_code, address, values)
146+
147+
txt = f"updating_task: incremented values: {values!s} at address {address!s}"
148+
print(txt)
149+
Log.debug(txt)
150+
151+
async def catch_requests(
152+
function_code: int,
153+
start_address: int,
154+
address: int,
155+
count: int,
156+
current_registers: list[int],
157+
set_values: list[int] | list[bool] | None
158+
) -> :
159+
"""Run action."""
160+
161+
162+
async def run_updating_server(server):
163+
"""Start updating_task concurrently with the current task."""
164+
task = asyncio.create_task(updating_task(server))
165+
task.set_name("example updating task")
166+
await server.serve_forever() # start the server
167+
task.cancel()
168+
169+
170+
async def main(cmdline=None):
171+
"""Combine setup and run."""
172+
server = setup_updating_server(cmdline=cmdline)
173+
await run_updating_server(server)
174+
175+
176+
if __name__ == "__main__":
177+
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+
examples/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)