Skip to content

Commit d52b2b8

Browse files
committed
Initial upload of package files
1 parent c915abb commit d52b2b8

12 files changed

+2040
-0
lines changed

.gitignore

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
__debug_bin*
2+
.vscode
3+
.cache
4+
.DS_store
5+
*.gz
6+
*.py
7+
*.log
8+
*.json
9+
*.exe
10+
*.zip

README.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# sensonetEbus
2+
3+
sensonetEbus is a library that provides functions to read data from Vaillant heating systems, especially heat pumps, and to initiate certain routines on these systems.
4+
The communication works via ebus and mainly reads from the sensonet module (VR921). So you need a Vaillant heating system with a VR921 module, an ebus adapter (see https://adapter.ebusd.eu/v5-c6/) and the ebusd (see https://github.com/john30/ebusd).
5+
(Presumably the library also works with a Vaillant VR940f module instead of a VR921 module.)
6+
7+
## Features
8+
- Reading the system information of the heating system (current temperatures and setpoints for hotwater and heating zones, current power consumption)
9+
- Starting and stopping of hotwater boosts and of zone quick veto sessions
10+
- Starting and stopping of strategy based quick mode sessions
11+
12+
## Custom ebus message definition file 15.ctlv2.csv
13+
At the moment, some ebus message definitions needed for the initiation of a zone quick veto are missing in the "official" ebus configuration files under https://ebus.github.io/.
14+
It works, if you start the ebusd service with the config path https://ebus.github.io/next/. Or you can download the config files to a local path and substitute 15.ctlv2.csv by the file https://github.com/WulfgarW/sensonetEbus/ebusd-config-files/15.ctlv2.csv
15+
16+
## Getting Started
17+
18+
This project is still in a preliminary state.
19+

connection.go

+335
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
package sensonetEbus
2+
3+
import (
4+
"fmt"
5+
"time"
6+
)
7+
8+
// Connection is the SensonetEbus connection
9+
type Connection struct {
10+
logger Logger
11+
ebusdConn *EbusConnection
12+
currentQuickmode string
13+
quickmodeStarted time.Time
14+
quickmodeStopped time.Time
15+
onoff bool
16+
quickVetoSetPoint float32
17+
quickVetoExpiresAt string
18+
relData VaillantRelData
19+
}
20+
21+
// NewConnection creates a new Sensonet device connection.
22+
func NewConnection(ebusdAddress string, opts ...ConnOption) (*Connection, error) {
23+
conn := &Connection{}
24+
conn.currentQuickmode = ""
25+
conn.quickmodeStarted = time.Now()
26+
27+
for _, opt := range opts {
28+
opt(conn)
29+
}
30+
31+
var err error
32+
if conn.logger != nil {
33+
conn.ebusdConn, err = NewEbusConnection(ebusdAddress, WithConnLogger(conn.logger))
34+
} else {
35+
conn.ebusdConn, err = NewEbusConnection(ebusdAddress)
36+
}
37+
return conn, err
38+
}
39+
40+
func (c *Connection) debug(fmt string, arg ...any) {
41+
if c.logger != nil {
42+
c.logger.Printf(fmt, arg...)
43+
}
44+
}
45+
46+
func (c *Connection) GetCurrentQuickMode() string {
47+
return c.currentQuickmode
48+
}
49+
50+
func (c *Connection) GetSystem(refresh bool) (VaillantRelData, error) {
51+
err := c.ebusdConn.getSystem(&c.relData, refresh)
52+
c.refreshCurrentQuickMode()
53+
return c.relData, err
54+
}
55+
56+
func (c *Connection) CheckEbusdConfig() (string, error) {
57+
details, err := c.ebusdConn.checkEbusdConfig()
58+
return details, err
59+
}
60+
61+
func (c *Connection) StartZoneQuickVeto(zone int, setpoint float32, duration float32) error {
62+
if zone < 0 {
63+
zone = ZONEINDEX_DEFAULT
64+
} // if parameter "zone" is negative, then the default value is used
65+
if setpoint < 0.0 {
66+
setpoint = ZONEVETOSETPOINT_DEFAULT
67+
} // if parameter "setpoint" is negative, then the default value is used
68+
if duration < 0.0 {
69+
duration = ZONEVETODURATION_DEFAULT
70+
} // if parameter "duration" is negative, then the default value is used
71+
72+
zonePrefix := fmt.Sprintf("z%01d", zone)
73+
message := " -c " + c.ebusdConn.controllerForSFMode + " " + zonePrefix + EBUSDREAD_ZONE_QUICKVETOTEMP + fmt.Sprintf(" %2.1f", setpoint)
74+
err := c.ebusdConn.ebusdWrite(message)
75+
if err != nil {
76+
c.debug("could not start zone quick veto. Error: %s", err)
77+
return err
78+
}
79+
// Zone quick veto is started by writing a duration to the controler. A duration of 0.5 hours is set.
80+
message = " -c " + c.ebusdConn.controllerForSFMode + " " + zonePrefix + EBUSDREAD_ZONE_QUICKVETODURATION + fmt.Sprintf(" %2.1f", duration)
81+
err = c.ebusdConn.ebusdWrite(message)
82+
if err != nil {
83+
c.debug(fmt.Sprintf("could not start zone quick veto. Error: %s", err))
84+
return err
85+
}
86+
c.quickVetoSetPoint = setpoint
87+
c.quickVetoExpiresAt = (time.Now().Add(time.Duration(int64(duration*60) * int64(time.Minute)))).Format("15:04")
88+
c.ebusdConn.lastGetSystemAt = time.Time{} // reset the cache
89+
return err
90+
}
91+
92+
func (c *Connection) StopZoneQuickVeto(zone int) error {
93+
if zone < 0 {
94+
zone = ZONEINDEX_DEFAULT
95+
} // if parameter "zone" is negative, then the default value is used
96+
97+
zonePrefix := fmt.Sprintf("z%01d", zone)
98+
message := " -c " + c.ebusdConn.controllerForSFMode + " " + zonePrefix + EBUSDREAD_ZONE_SFMODE + " auto"
99+
err := c.ebusdConn.ebusdWrite(message)
100+
if err != nil {
101+
c.debug(fmt.Sprintf("could not stop zone quick veto. Error: %s", err))
102+
return err
103+
}
104+
c.quickVetoSetPoint = 0
105+
c.quickVetoExpiresAt = ""
106+
c.ebusdConn.lastGetSystemAt = time.Time{} // reset the cache
107+
return err
108+
}
109+
110+
func (c *Connection) StartHotWaterBoost() error {
111+
message := " -c " + c.ebusdConn.controllerForSFMode + " " + EBUSDREAD_HOTWATER_SFMODE + " load"
112+
err := c.ebusdConn.ebusdWrite(message)
113+
if err != nil {
114+
c.debug(fmt.Sprintf("could not start hotwater boost. Error: %s", err))
115+
}
116+
c.ebusdConn.lastGetSystemAt = time.Time{} // reset the cache
117+
return err
118+
}
119+
120+
func (c *Connection) StopHotWaterBoost() error {
121+
message := " -c " + c.ebusdConn.controllerForSFMode + " " + EBUSDREAD_HOTWATER_SFMODE + " auto"
122+
err := c.ebusdConn.ebusdWrite(message)
123+
if err != nil {
124+
c.debug(fmt.Sprintf("could not start hotwater boost. Error: %s", err))
125+
}
126+
c.ebusdConn.lastGetSystemAt = time.Time{} // reset the cache
127+
return err
128+
}
129+
130+
func (c *Connection) refreshCurrentQuickMode() {
131+
newQuickMode := ""
132+
if c.relData.Hotwater.HwcSFMode == HWC_SFMODE_BOOST {
133+
newQuickMode = QUICKMODE_HOTWATER
134+
}
135+
for _, zone := range c.relData.Zones {
136+
if zone.SFMode == ZONE_SFMODE_BOOST {
137+
newQuickMode = QUICKMODE_HEATING
138+
break
139+
}
140+
}
141+
if newQuickMode != c.currentQuickmode {
142+
if newQuickMode == "" {
143+
c.debug(fmt.Sprintf("Old quickmode: \"%s\" New quickmode: \"%s\"", c.currentQuickmode, newQuickMode))
144+
c.currentQuickmode = newQuickMode
145+
c.quickmodeStopped = time.Now()
146+
}
147+
if newQuickMode != "" {
148+
c.debug(fmt.Sprintf("Old quickmode: \"%s\" New quickmode: \"%s\"", c.currentQuickmode, newQuickMode))
149+
c.currentQuickmode = newQuickMode
150+
c.quickmodeStarted = time.Now()
151+
}
152+
}
153+
}
154+
155+
func (c *Connection) StartStrategybased(strategy int, heatingPar *HeatingParStruct) (string, error) {
156+
err := c.ebusdConn.getSystem(&c.relData, true)
157+
if err != nil {
158+
err = fmt.Errorf("could not read current status information in StartStrategybased(): %s", err)
159+
return "", err
160+
}
161+
// Extracting correct Zones element
162+
zoneData := GetZoneData(c.relData.Zones, heatingPar.ZoneIndex)
163+
164+
if c.currentQuickmode != "" {
165+
c.debug(fmt.Sprint("System is already in quick mode:", c.currentQuickmode))
166+
c.debug("Is there any need to change that?")
167+
c.debug(fmt.Sprint("Special Function of Dhw: ", c.relData.Hotwater.HwcSFMode))
168+
c.debug(fmt.Sprint("Special Function of Heating Zone: ", zoneData.SFMode))
169+
return QUICKMODE_ERROR_ALREADYON, err
170+
}
171+
172+
whichQuickMode := c.WhichQuickMode(strategy, heatingPar.ZoneIndex)
173+
c.debug(fmt.Sprint("whichQuickMode=", whichQuickMode))
174+
175+
switch whichQuickMode {
176+
case 1:
177+
err = c.StartHotWaterBoost()
178+
if err == nil {
179+
c.currentQuickmode = QUICKMODE_HOTWATER
180+
c.quickmodeStarted = time.Now()
181+
c.debug("Starting hotwater boost")
182+
}
183+
case 2:
184+
err = c.StartZoneQuickVeto(heatingPar.ZoneIndex, heatingPar.VetoSetpoint, heatingPar.VetoDuration)
185+
if err == nil {
186+
c.currentQuickmode = QUICKMODE_HEATING
187+
c.quickmodeStarted = time.Now()
188+
c.debug("Starting zone quick veto")
189+
}
190+
default:
191+
if c.currentQuickmode == QUICKMODE_HOTWATER {
192+
// if hotwater boost active, then stop it
193+
err = c.StopHotWaterBoost()
194+
if err == nil {
195+
c.debug("Stopping hotwater boost")
196+
}
197+
}
198+
if c.currentQuickmode == QUICKMODE_HEATING {
199+
// if zone quick veto active, then stop it
200+
err = c.StopZoneQuickVeto(heatingPar.ZoneIndex)
201+
if err == nil {
202+
c.debug("Stopping zone quick veto")
203+
}
204+
}
205+
c.currentQuickmode = QUICKMODE_NOTHING
206+
c.quickmodeStarted = time.Now()
207+
c.debug("Enable called but no quick mode possible. Starting idle mode")
208+
}
209+
210+
c.ebusdConn.lastGetSystemAt = time.Time{} // reset the cache
211+
return c.currentQuickmode, err
212+
}
213+
214+
func (c *Connection) StopStrategybased(heatingPar *HeatingParStruct) (string, error) {
215+
err := c.ebusdConn.getSystem(&c.relData, true)
216+
if err != nil {
217+
err = fmt.Errorf("could not read current status information in StopStrategybased(): %s", err)
218+
return "", err
219+
}
220+
// Extracting correct Zones element
221+
zoneData := GetZoneData(c.relData.Zones, heatingPar.ZoneIndex)
222+
223+
c.debug(fmt.Sprint("Operationg Mode of Dhw: ", c.relData.Hotwater.HwcSFMode))
224+
c.debug(fmt.Sprint("Operationg Mode of Heating: ", zoneData.SFMode))
225+
226+
switch c.currentQuickmode {
227+
case QUICKMODE_HOTWATER:
228+
err = c.StopHotWaterBoost()
229+
if err == nil {
230+
c.debug(fmt.Sprint("Stopping quick mode", c.currentQuickmode))
231+
}
232+
case QUICKMODE_HEATING:
233+
err = c.StopZoneQuickVeto(heatingPar.ZoneIndex)
234+
if err == nil {
235+
c.debug("Stopping zone quick veto")
236+
}
237+
case QUICKMODE_NOTHING:
238+
c.debug("Stopping idle quick mode")
239+
default:
240+
c.debug("Nothing to do, no quick mode active")
241+
}
242+
c.currentQuickmode = ""
243+
c.quickmodeStopped = time.Now()
244+
245+
c.ebusdConn.lastGetSystemAt = time.Time{} // reset the cache
246+
return c.currentQuickmode, err
247+
}
248+
249+
// This function checks the operation mode of heating and hotwater and the hotwater live temperature
250+
// and returns, which quick mode should be started, when StartStrategybased() is called
251+
func (c *Connection) WhichQuickMode(strategy, heatingZone int) int {
252+
/*err := c.ebusdConn.getSystem(&c.relData, false)
253+
if err != nil {
254+
err = fmt.Errorf("could not read current status information in WhichQuickMode(): %s", err)
255+
return 0, err
256+
}*/
257+
c.debug(fmt.Sprintf("Checking if hot water boost possible. Operation Mode = %s, temperature setpoint= %02.2f, live temperature= %02.2f",
258+
c.relData.Hotwater.HwcOpMode, c.relData.Hotwater.HwcTempDesired, c.relData.Hotwater.HwcStorageTemp))
259+
hotWaterBoostPossible := false
260+
// For strategy=STRATEGY_HOTWATER, a hotwater boost is possible when hotwater storage temperature is less than the temperature setpoint.
261+
// For other strategy values, a hotwater boost is possible when hotwater storage temperature is less than the temperature setpoint minus 5°C
262+
addOn := -5.0
263+
if strategy == STRATEGY_HOTWATER {
264+
addOn = 0.0
265+
}
266+
if c.relData.Hotwater.HwcStorageTemp < c.relData.Hotwater.HwcTempDesired+addOn &&
267+
c.relData.Hotwater.HwcOpMode == OPERATIONMODE_AUTO {
268+
hotWaterBoostPossible = true
269+
}
270+
271+
heatingQuickVetoPossible := false
272+
for _, z := range c.relData.Zones {
273+
if z.Index == heatingZone {
274+
c.debug(fmt.Sprintf("Checking if heating quick veto possible. Operation Mode = %s", z.OpMode))
275+
if z.OpMode == OPERATIONMODE_AUTO {
276+
heatingQuickVetoPossible = true
277+
}
278+
}
279+
}
280+
281+
whichQuickMode := 0
282+
switch strategy {
283+
case STRATEGY_HOTWATER:
284+
if hotWaterBoostPossible {
285+
whichQuickMode = 1
286+
} else {
287+
c.debug("Strategy = hotwater, but hotwater boost not possible")
288+
}
289+
case STRATEGY_HEATING:
290+
if heatingQuickVetoPossible {
291+
whichQuickMode = 2
292+
} else {
293+
c.debug("Strategy = heating, but heating quick veto not possible")
294+
}
295+
case STRATEGY_HOTWATER_THEN_HEATING:
296+
if hotWaterBoostPossible {
297+
whichQuickMode = 1
298+
} else {
299+
if heatingQuickVetoPossible {
300+
whichQuickMode = 2
301+
} else {
302+
c.debug("PV Use Strategy = hotwater_then_heating, but both not possible")
303+
}
304+
}
305+
}
306+
return whichQuickMode
307+
}
308+
309+
// Returns the energy data for systemId, deviceUuid and other given criteria
310+
/*func (c *Connection) GetEnergyData(systemId, deviceUuid, operationMode, energyType, resolution string, startDate, endDate time.Time) (EnergyData, error) {
311+
var energyData EnergyData
312+
v := url.Values{
313+
"resolution": {resolution},
314+
"operationMode": {operationMode},
315+
"energyType": {energyType},
316+
"startDate": {startDate.Format("2006-01-02T15:04:05-07:00")},
317+
"endDate": {endDate.Format("2006-01-02T15:04:05-07:00")},
318+
}
319+
320+
url := API_URL_BASE + fmt.Sprintf(ENERGY_URL, systemId, deviceUuid) + v.Encode()
321+
req, _ := http.NewRequest("GET", url, nil)
322+
if err := doJSON(c.client, req, &energyData); err != nil {
323+
return energyData, err
324+
}
325+
return energyData, nil
326+
}*/
327+
328+
// Returns the current power consumption for systemId
329+
func (c *Connection) GetSystemCurrentPower() (float64, error) {
330+
state, err := c.GetSystem(false)
331+
if err != nil {
332+
return -1.0, err
333+
}
334+
return state.Status.CurrentConsumedPower, err
335+
}

0 commit comments

Comments
 (0)