Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix bei Headerlänge / Neu: FunctionCall für Energiebilanz #4

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Replacement for vcontrold when using a python environment.
Python package zur Kommunikation mit Viessmann-Heizungen über die Optolink serielle Schnittstelle.
Geeignet um vcontrold zu ersetzen wenn ohnehin mit Python gearbeitet wird.

Neu:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vorschlag: Änderungshistorie nur in den Commit-Messages pflegen, mögliche Erweiterungen als github issues.

- FunctionCall zum Abfragen der Engergiebilanz ... dort scheint aber auch noch das Betriebstagebuch lesbar zu sein (Details: https://github.com/openv/openv/issues/480#issuecomment-703803129 )

Neuentwicklung basierend auf
- [SmartHomeNG plugin (Python)][SHNGpyPlugin]
- [vcontrold][vcontrold]
Expand All @@ -22,7 +25,10 @@ Einschränkungen/known issues:
- nur V200WO1C/P300 implementiert.

Beispielcode:
- testViessmann.py: führt einen Lesezugriff für alle definierten Kommandos durch.
- testViessmann.py: führt einen Lesezugriff für alle definierten Kommandos durch. Zum Scannen der Parameter im FunctionCall am Ende unittest.main() auskommentieren und bei viscanFuncionCall den Kommentar entfernen




[vcontrold]: https://github.com/openv (vcontrold)
[SHNGpyPlugin]: https://github.com/sisamiwe/myplugins/tree/master/viessmann (SmartHomeNG python Plugin)
Expand Down
135 changes: 80 additions & 55 deletions pyvcontrol/viCommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,88 +21,108 @@
import logging


class viCommandException(Exception):
pass
# Commandsets for different heatings
# YOU MUST CHANGE below in class viCommand the attribute "commandset" for your heating

# Predifined Commandsets for Heatings:

class viCommand(bytearray):
# the commands
# viCommand object value is a bytearray of addr and len
VITOCAL_WO1C = {
# All Parameters are tested and working on Vitocal 200S WO1C (Baujahr 2019)
# All Parameters are tested and working on Vitocal 333G (Baujahr 2014)

# TODO: statt 'write':False besser mode:rw/w verwenden
# ------ Statusinfos (read only) ------

commandset = {
# All Parameters are tested and working on Vitocal 200S WO1C (Baujahr 2019)
# Warmwasser: Warmwassertemperatur oben (0..95)
'Warmwassertemperatur': {'addr': '010d', 'len': 2, 'unit': 'IS10'},

# ------ Statusinfos (read only) ------
# Aussentemperatur (-40..70)
'Aussentemperatur': {'addr': '0101', 'len': 2, 'unit': 'IS10'},

# Warmwasser: Warmwassertemperatur oben (0..95)
'Warmwassertemperatur': {'addr': '010d', 'len': 2, 'unit': 'IS10', 'write': False},
# Heizkreis HK1: Vorlauftemperatur Sekundaer 1 (0..95)
'VorlauftempSek': {'addr': '0105', 'len': 2, 'unit': 'IS10'},

# Aussentemperatur (-40..70)
'Aussentemperatur': {'addr': '0101', 'len': 2, 'unit': 'IS10', 'write': False},
# Ruecklauftemperatur Sekundaer 1 (0..95)
'RuecklauftempSek': {'addr': '0106', 'len': 2, 'unit': 'IS10'},

# Heizkreis HK1: Vorlauftemperatur Sekundaer 1 (0..95)
'VorlauftempSek': {'addr': '0105', 'len': 2, 'unit': 'IS10', 'write': False},
# Sekundaerpumpe [%] (including one status byte)
'Sekundaerpumpe': {'addr': 'B421', 'len': 2, 'unit': 'IUNON'},

# Ruecklauftemperatur Sekundaer 1 (0..95)
'RuecklauftempSek': {'addr': '0106', 'len': 2, 'unit': 'IS10', 'write': False},
# Faktor Energiebilanz(1 = 0.1kWh, 10 = 1kWh, 100 = 10kWh)
'FaktorEnergiebilanz': {'addr': '163F', 'len': 1, 'unit': 'IUNON'},

# Sekundaerpumpe [%] (including one status byte)
'Sekundaerpumpe': {'addr': 'B421', 'len': 2, 'unit': 'IUNON', 'write': False},
# Heizwärme "Heizbetrieb", Verdichter 1
'Heizwaerme': {'addr': '1640', 'len': 4, 'unit': 'IUNON'},

# Faktor Energiebilanz(1 = 0.1kWh, 10 = 1kWh, 100 = 10kWh)
'FaktorEnergiebilanz': {'addr': '163F', 'len': 1, 'unit': 'IUNON', 'write': False},
# Elektroenergie "Heizbetrieb", Verdichter 1
'Heizenergie': {'addr': '1660', 'len': 4, 'unit': 'IUNON'},

# Heizwärme "Heizbetrieb", Verdichter 1
'Heizwaerme': {'addr': '1640', 'len': 4, 'unit': 'IUNON', 'write': False},
# Heizwärme "WW-Betrieb", Verdichter 1
'WWwaerme': {'addr': '1650', 'len': 4, 'unit': 'IUNON'},

# Elektroenergie "Heizbetrieb", Verdichter 1
'Heizenergie': {'addr': '1660', 'len': 4, 'unit': 'IUNON', 'write': False},
# Elektroenergie "WW-Betrieb", Verdichter 1
'WWenergie': {'addr': '1670', 'len': 4, 'unit': 'IUNON'},

# Heizwärme "WW-Betrieb", Verdichter 1
'WWwaerme': {'addr': '1650', 'len': 4, 'unit': 'IUNON', 'write': False},
# Verdichter [%] (including one status byte)
'Verdichter': {'addr': 'B423', 'len': 4, 'unit': 'IUNON'},

# Elektroenergie "WW-Betrieb", Verdichter 1
'WWenergie': {'addr': '1670', 'len': 4, 'unit': 'IUNON', 'write': False},
# Druck Sauggas [bar] (including one status byte) - Kühlmittel
'DruckSauggas': {'addr': 'B410', 'len': 3, 'unit': 'IS10'},

# Verdichter [%] (including one status byte)
'Verdichter': {'addr': 'B423', 'len': 4, 'unit': 'IUNON', 'write': False},
# Druck Heissgas [bar] (including one status byte)- Kühlmittel
'DruckHeissgas': {'addr': 'B411', 'len': 3, 'unit': 'IS10'},

# Druck Sauggas [bar] (including one status byte) - Kühlmittel
'DruckSauggas': {'addr': 'B410', 'len': 3, 'unit': 'IS10', 'write': False},
# Temperatur Sauggas [bar] (including one status byte)- Kühlmittel
'TempSauggas': {'addr': 'B409', 'len': 3, 'unit': 'IS10'},

# Druck Heissgas [bar] (including one status byte)- Kühlmittel
'DruckHeissgas': {'addr': 'B411', 'len': 3, 'unit': 'IS10', 'write': False},
# Temperatur Heissgas [bar] (including one status byte)- Kühlmittel
'TempHeissgas': {'addr': 'B40A', 'len': 3, 'unit': 'IS10'},

# Temperatur Sauggas [bar] (including one status byte)- Kühlmittel
'TempSauggas': {'addr': 'B409', 'len': 3, 'unit': 'IS10', 'write': False},
# Anlagentyp (muss 204D sein)
'Anlagentyp': {'addr': '00F8', 'len': 2, 'unit': 'DT'},

# Temperatur Heissgas [bar] (including one status byte)- Kühlmittel
'TempHeissgas': {'addr': 'B40A', 'len': 3, 'unit': 'IS10', 'write': False},
# --------- Menüebene -------

# Anlagentyp (muss 204D sein)
'Anlagentyp': {'addr': '00F8', 'len': 4, 'unit': 'DT', 'write': False},
# getManuell / setManuell -- 0 = normal, 1 = manueller Heizbetrieb, 2 = 1x Warmwasser auf Temp2
'WWeinmal': {'addr': 'B020', 'len': 1, 'unit': 'OO', 'write': True},

# --------- Menüebene -------
# Warmwassersolltemperatur (10..60 (95))
'SolltempWarmwasser': {'addr': '6000', 'len': 2, 'unit': 'IS10', 'write': True, 'min_value': 10,
'max_value': 60},

# getManuell / setManuell -- 0 = normal, 1 = manueller Heizbetrieb, 2 = 1x Warmwasser auf Temp2
'WWeinmal': {'addr': 'B020', 'len': 1, 'unit': 'OO', 'write': True},
# --------- Codierebene 2 ---------

# Warmwassersolltemperatur (10..60 (95))
'SolltempWarmwasser': {'addr': '6000', 'len': 2, 'unit': 'IS10', 'write': True, 'min_value': 10,
'max_value': 60},
# Hysterese Vorlauf ein: Verdichter schaltet im Heizbetrieb ein
'Hysterese_Vorlauf_ein': {'addr': '7304', 'len': 2, 'unit': 'IU10', 'write': True},

# --------- Codierebene 2 ---------
# Hysterese Vorlauf aus: Verdichter schaltet im Heizbetrieb ab
'Hysterese_Vorlauf_aus': {'addr': '7313', 'len': 2, 'unit': 'IU10', 'write': True},

# Hysterese Vorlauf ein: Verdichter schaltet im Heizbetrieb ein
'Hysterese_Vorlauf_ein': {'addr': '7304', 'len': 2, 'unit': 'IU10', 'write': True},

# Hysterese Vorlauf aus: Verdichter schaltet im Heizbetrieb ab
'Hysterese_Vorlauf_aus': {'addr': '7313', 'len': 2, 'unit': 'IU10', 'write': True}
# Funktion Call für Energiebilanz
'Energiebilanz': {'addr': 'B800', 'len': 16, 'unit': 'F_E', 'func': True}

# vo = viControl(port='/dev/optolink')
# vo.initComm()
# print( vo.execFunctionCall('Energiebilanz', 2,2).value )

}

class viCommandException(Exception):
pass


class viCommand(bytearray):
# the commands
# viCommand object value is a bytearray of addr and len

# TODO: statt 'write':False besser mode:rw/w verwenden -- verbessert: es gibt read, write oder func, wenn nichts vorhanden ist
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vorschlag: 'Hysterese_Vorlauf_aus': {'addr': '7313', 'len': 2, 'unit': 'IU10', 'accessmode': 'write'},
'Energiebilanz': {'addr': 'B800', 'len': 16, 'unit': 'F_E', 'accessmode': 'function'}

# ist es immer read, bei Write ist es read und Write und bei Func nur Func

# =============================================================
# CHANGE YOUR COMMANDSET HERE:
commandset = VITOCAL_WO1C
# =============================================================

}

def __init__(self, cmdname):
# FIXME: uniform naming of private and public properties
Expand All @@ -114,11 +134,16 @@ def __init__(self, cmdname):
self.__cmdcode__ = cs['addr']
self.__valuebytes__ = cs['len']
self.unit = cs['unit']
self.write = cs['write']
self.write = cs.get('write', False)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.accessmode = cs.get('accessmode')

self.function= cs.get('func', False)
self.cmdname = cmdname

# create bytearray representation
b = bytes.fromhex(self.__cmdcode__) + self.__valuebytes__.to_bytes(1, 'big')
b = bytes.fromhex(self.__cmdcode__)
if not self.function:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if self.accessmode != "function"

b = b + self.__valuebytes__.to_bytes(1, 'big')
# else: function call don't have length bytes!

super().__init__(b)

@classmethod
Expand Down
51 changes: 50 additions & 1 deletion pyvcontrol/viControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
# - execWriteCmd: execute write Command
# viSerial: Low-level interface

from ast import arg
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wo wird arg verwendet?

from pyvcontrol.viCommand import viCommand
from pyvcontrol.viTelegram import viTelegram
from pyvcontrol.viData import viData
Expand Down Expand Up @@ -65,12 +66,18 @@ def __del__(self):
# destructor, releases serial port
self.vs.disconnect()

# FIXME: die 3 exec-Methoden refaktorieren ... das sie sehr ähnlich sind

def execReadCmd(self, cmdname) -> viData:
# sends a read command and gets the response.
vc = viCommand(cmdname) # create command

if vc.function:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if vc.accessmode == 'function':

raise viControlException(f'command {cmdname} is not readable, because it is a function call')

vt = viTelegram(vc, 'read') # create read Telegram

logging.debug(f'Send telegram {vt.hex()}')
logging.debug(f'Send telegram {vt.hex(" ")}')
self.vs.send(vt) # send Telegram

# Check if sending was successfull
Expand Down Expand Up @@ -122,6 +129,48 @@ def execWriteCmd(self, cmdname, value) -> viData:

return viData.create(vt.vicmd.unit, vt.payload) # return viData object from payload

def execFunctionCall(self, cmdname, *function_args) -> viData:
# call function see: https://github.com/openv/openv/wiki/FunctionCalls-WO1C
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was wird über die function_args an die Anlage geschickt?
Eine kurze Beschreibung im Quellcode hier wäre hilfreich.

# not tested ... does not work with Vitocal333G

vc = viCommand(cmdname)
if not vc.function:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if vc.accessmode != 'function':

raise viControlException(f'command {cmdname} is not a function call')

# create viData object
vd = viData.create(vc.unit)
# create write Telegram
vt = viTelegram(vc, 'call', 'Request', bytearray((len(function_args),*function_args)))
# send Telegram
logging.debug(f'Send telegram {vt.hex(" ")}')
self.vs.send(vt)

# Check if sending was successfull
ack = self.vs.read(1)
logging.debug(f'Received {ack.hex()}')
if ack != ctrlcode['acknowledge']:
raise viControlException(f'Expected acknowledge byte, received {ack}')

# Receive response and evaluate data
header = self.vs.read(2) #read the header ... the read the length received in the header
logging.debug(f'Requested 2 (0x41 + length) bytes. Received telegram {header.hex()}')

l = viTelegram.checkStartByteAnGetLength(header)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vorschlag: viTelegramLength=viTelegram.length()

body = self.vs.read(l+1) #read the response (length + checksum)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l und 1 sehen nahezu gleich aus.


vr = header+body

# receive response
logging.debug(f'Requested {l+1} bytes. Received telegram {vr.hex()}')

self.vs.send(ctrlcode['acknowledge']) # send acknowledge

vt = viTelegram.frombytes(vr) # create response Telegram
if vt.tType == viTelegram.tTypes['error']:
raise viControlException('Function Call returned ERROR')

return viData.create(vt.vicmd.unit, vt.payload) # return viData object from payload

def initComm(self):
logging.debug('Init Communication to viControl....')
self.isInitialized = False
Expand Down
54 changes: 53 additions & 1 deletion pyvcontrol/viData.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##

from email import header
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wo wird header verwendet?

import logging
from collections import namedtuple
from struct import unpack

class viDataException(Exception):
def __init__(self,msg):
Expand Down Expand Up @@ -94,7 +97,7 @@ def create(cls,type,*args):
logging.debug(f'Data factory: request to produce Data type {type} with args {args}')
datatype_object={'BA':viDataBA, 'DT':viDataDT, 'IS10':viDataIS10,'IU10':viDataIU10,
'IU3600':viDataIU3600,'IUNON':viDataIUNON, 'RT':viDataRT, 'OO':viDataOO,
'ES':viDataES,
'ES':viDataES, 'F_E': viDataEnergy,
}
if type in datatype_object.keys():
return datatype_object[type](*args)
Expand Down Expand Up @@ -246,6 +249,7 @@ class viDataDT(viData):
0x2094: 'V200KW1, Protokoll: KW2',
0x209F: 'V200KO1B, Protokoll: P300, KW2',
0x204D: 'V200WO1C, Protokoll: P300',
0x204B: 'Vitocal 333G, Protokoll: P300',
0x20B8: 'V333MW1, Protokoll: ',
0x20A0: 'V100GC1, Protokoll: ',
0x20C2: 'VDensHO1, Protokoll: ',
Expand Down Expand Up @@ -417,6 +421,54 @@ def __fromraw__(self,value):
def value(self):
return self.OnOff[int.from_bytes(self, 'big')]


# holder type for Energy
Energy = namedtuple('Energy', 'byte1 day year week heating_energy heating_electrical_energy water_energy water_electrical_energy, cooling_energy cooling_electrical_energy total_energy total_eletrical_energy')

class viDataEnergy(viData):
#Energy-Type ... Return from Function-Call B800
# see https://github.com/openv/openv/issues/480#issuecomment-712235475
# see https://github.com/openv/openv/wiki/FunctionCalls-WO1C
unit={'code':'F_E','description': 'returns Named Tuple with enery Data','unit':''},

def __init__(self, value=b'\x00\x00',len=16):
#sets int representation based on input value
self.len=len #length in bytes
super().__init__(value)

def __fromtype__(self,value):
raise viDataException(f'Value not setable from Type')

@property
def value(self) -> Energy:
# decode the Result Record an return a named tuple
# see links above

# example 02 02 16 09 92 03 aa 00 99 00 2d 00 00 00 00 00
raw = unpack('<4B6H', self)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wofür steht '<4B6H'
Vorschlag: Variable mit passendem Namen verwenden die diesen String enthält (so was in der Art energy_byte_structure='<4B6H')


res = Energy(
raw[0],raw[1],2000+raw[2],raw[3],
raw[4]*0.1, raw[5]*0.1, #heating
raw[6]*0.1, raw[7]*0.1, #water
raw[8]*0.1, raw[9]*0.1, #cooling ???
raw[4]*0.1+raw[6]*0.1+raw[8]*0.1, raw[5]*0.1+raw[7]*0.1+raw[9]*0.1 #total
)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woher kommen die Konstanten?
Vorschlag: Für jede Konstante eine Variable definieren. Kostet zwar Platz ist aber verständlicher.


return res

@property
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diese Funktion gehört m.E. thematisch zu viTools, nicht zu viData, da sie nur in dem speziellen Kontext verwendet wird.
viTools verweist auf viData, viData wiederum auf viTools (quasi zirkuläre Abhängigkeit)

def valueScan(self):
# special property for scanning a function Call, see viTools
# for function call B800: it seems that the first for bytes are a header
# extract the header an try to decode the body as signed short
header = unpack('<4B', self[0:4])
body = unpack(f'<{len(self[4:])//2}h', self[4:])
bodyDiv10 = [x*0.1 for x in body]

return f"{self.hex(' ')} -- {header} - {body} / {bodyDiv10}"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vorschlag: Selbsterklärender Ausgabetext



systemschemes = {
'01': 'WW',
'02': 'HK + WW',
Expand Down
Loading