Hallo zusammen,
ich habe versucht aus mehreren Plugins (Comfoair, Russound, und andere) zu lernen und ein Plugin für meinen TEAC AG980 Audio Receiver zu erstellen.
Das ist meist per copy und paste erfolgt, aber einiges habe ich auf selbst erstellt.
Das RS232 Protokoll des Teac habe ich noch mal angehängt.
Mein größtes Problem ist die Statusabfrage des Teac und die entsprechende Umsetzung dessen auf die Items. Im RS232 Protokol ist eigentlich die IR-Fernbedienung abgebildet, und nicht eine direkte Steuerung des Geräts.
Eine Status wird nicht aktiv vom Gerät verschickt, sondern muss abgefragt werden.
Wenn ich nun den Status Abfrage werden die Statusbytes ausgewertet und entsprechend die Items gesetzt. Jetzt hat sich das Item evtl. geändert (weil z.B. über die Fernbedienung etwas umgeschaltet wurde) und es wird ein update_item durchgeführt. Die Statusabfrage, die ja auch durch update_item getriggert ist, ist aber noch gar nicht fertig und somit wartet der eine Befehl auf den anderen. Wie kann ich sowas verhindern bzw. umgehen?
Die Initalisierung des ersten Status habe ich hinbekommen, bin mir aber nicht sicher ob der Scheduler wirklich nach dem ersten Lauf beendet wird.
Das habe ich vom Confoair Plugin, aber noch nicht genau erfasst wie es funktioniert.
Die Lautstärke kann ich nur "hoch" und "runter" tasten. Im Status des Teac habe ich aber einen Nummerischen Wert der Lautstärke. Der wird aber im entsprechenden Item nicht aktualisiert, dafür müßte ich den Status wieder abfragen. Da bin ich aber wieder bei dem Problem, der Schleife siehe oben Statusabfrage. Wenn das gelöst ist könnte man über Logik nachdenken um z.B. direkte Werte anzuspringen und die durch "hoch" bzw. "runter" anzusteuern. Gehört diese Logic dann extern oder mit ins Plugin?
./etc/plugin.conf:
./plugins/ag980/__init__.py
./item/ag980.conf
./plugins/ag980/Readme.md
als Readme.txt angehängt
Das ist (m)ein erster Versuch, hoffe nicht kompletter Mist, den ich da zusammengebaut habe.
Auf jeden Fall kann ich damit per USB-RS232 oder per Moxa (in meinem Fall ist es ein XPORT) den Teac steuern.
Wenn Ihr jetzt noch etwas helfen könntet wirds vielleicht ganz gut nutzbar (hoffe ich).
@mknx: Einen Git Account habe ich erstellt, jetzt muss ich noch ne Erklärung finden wie ich das hochlade...
Danke an mknx für smarthome.py und die Ersteller der anderen Plugins als Vorlagenlieferanten!
ich habe versucht aus mehreren Plugins (Comfoair, Russound, und andere) zu lernen und ein Plugin für meinen TEAC AG980 Audio Receiver zu erstellen.
Das ist meist per copy und paste erfolgt, aber einiges habe ich auf selbst erstellt.
Das RS232 Protokoll des Teac habe ich noch mal angehängt.
Mein größtes Problem ist die Statusabfrage des Teac und die entsprechende Umsetzung dessen auf die Items. Im RS232 Protokol ist eigentlich die IR-Fernbedienung abgebildet, und nicht eine direkte Steuerung des Geräts.
Eine Status wird nicht aktiv vom Gerät verschickt, sondern muss abgefragt werden.
Wenn ich nun den Status Abfrage werden die Statusbytes ausgewertet und entsprechend die Items gesetzt. Jetzt hat sich das Item evtl. geändert (weil z.B. über die Fernbedienung etwas umgeschaltet wurde) und es wird ein update_item durchgeführt. Die Statusabfrage, die ja auch durch update_item getriggert ist, ist aber noch gar nicht fertig und somit wartet der eine Befehl auf den anderen. Wie kann ich sowas verhindern bzw. umgehen?
Die Initalisierung des ersten Status habe ich hinbekommen, bin mir aber nicht sicher ob der Scheduler wirklich nach dem ersten Lauf beendet wird.
Das habe ich vom Confoair Plugin, aber noch nicht genau erfasst wie es funktioniert.
Die Lautstärke kann ich nur "hoch" und "runter" tasten. Im Status des Teac habe ich aber einen Nummerischen Wert der Lautstärke. Der wird aber im entsprechenden Item nicht aktualisiert, dafür müßte ich den Status wieder abfragen. Da bin ich aber wieder bei dem Problem, der Schleife siehe oben Statusabfrage. Wenn das gelöst ist könnte man über Logik nachdenken um z.B. direkte Werte anzuspringen und die durch "hoch" bzw. "runter" anzusteuern. Gehört diese Logic dann extern oder mit ins Plugin?
./etc/plugin.conf:
Code:
[AG980] class_name = ag980 class_path = plugins.ag980 host = 192.168.35.15 port = 7777 #serialport = /dev/ttyUSB0 # Enable this if you want to use a serial connection
Code:
#!/usr/bin/env python3 # vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab ######################################################################### # Copyright 2013 Frank Plass ######################################################################### # This file is part of SmartHome.py. http://mknx.github.io/smarthome/ # # SmartHome.py is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # SmartHome.py is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with SmartHome.py. If not, see <http://www.gnu.org/licenses/>. ######################################################################### import logging import socket import time import serial #import re import threading logger = logging.getLogger('ag980') CONST_MAP_COMMANDS = [ 'status', 'display', 'speakeroff', 'speakera', 'speakerb', 'speakerc', 'speakerd', 'main', 'mainvolstat', 'mainmute', 'mainvolumeup', 'mainvolumedown', 'maininput', 'room2', 'r2volstat', 'r2mute', 'r2volumeup', 'r2volumedown', 'r2input' ] CONST_SEND_COMMANDS = { 'on' : 0x01, 'off' : 0x02, 'maintuner' : 0x03, 'maincd' : 0x04, 'mainaux' : 0x05, 'mainphono' : 0x06, 'maindvd' : 0x07, 'maintape' : 0x08, 'mainvolumeup' : 0x0f, 'mainvolumedown' : 0x10, 'mainmute' : 0x11, 'tone' : 0x12, 'countup' : 0x13, 'countdown' : 0x14, 'speakera' : 0x1d, 'speakerb' : 0x1e, 'speakerc' : 0x1f, 'speakerd' : 0x20, 'speakeroff' : 0x21, 'room2' : 0x3f, 'r2volumeup' : 0x41, 'r2volumedown' : 0x42, 'r2mute' : 0x43, 'r2tuner' : 0x44, 'r2cd' : 0x45, 'r2aux' : 0x46, 'r2phone' : 0x47, 'r2dvd' : 0x48, 'r2tape' : 0x49, 'status' : 0x53 } class ag980(): def __init__(self, smarthome, host=None, port=0, serialport=None, debug=False): self.connected = False self._sh = smarthome self._params = {} self._lock = threading.Lock() self._host = host self._port = int(port) self._serialport = serialport self._connection_attempts = 0 self._connection_errorlog = 60 smarthome.connections.monitor(self) self._packetstart = 0x8173 #System ID des AG980 self._initread = False def connect(self): if self._serialport is not None: self.connect_serial() else: self.connect_tcp() def connect_tcp(self): self._lock.acquire() try: self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.settimeout(2) self._sock.connect((self._host, self._port)) except Exception as e: self._connection_attempts -= 1 if self._connection_attempts <= 0: self.log_err('could not connect to {}:{}: {}'.format(self._host, self._port, e)) self._connection_attempts = self._connection_errorlog self._lock.release() return else: self.connected = True self.log_info('connected to {}:{}'.format(self._host, self._port)) self._connection_attempts = 0 self._lock.release() def connect_serial(self): self._lock.acquire() try: self._serialconnection = serial.Serial( self._serialport, 9600, serial.EIGHTBITS, serial.PARITY_NONE, serial.STOPBITS_ONE, timeout=2) except Exception as e: self._connection_attempts -= 1 if self._connection_attempts <= 0: logger.err('could not connect to {}: {}'.format(self._serialport, e)) self._connection_attempts = self._connection_errorlog self._lock.release() return else: self.connected = True self.log_info('connected to {}'.format(self._serialport)) self._connection_attempts = 0 self._lock.release() def disconnect(self): if self._serialport is not None: self.disconnect_serial() else: self.disconnect_tcp() def disconnect_tcp(self): self.connected = False try: self._sock.shutdown(socket.SHUT_RDWR) except: pass try: self._sock.close() except: pass def disconnect_serial(self): self.connected = False try: self._serialconnection.close() self._serialconnection = None except: pass def send_bytes(self, packet): if self._serialport is not None: self.send_bytes_serial(packet) else: self.send_bytes_tcp(packet) def send_bytes_tcp(self, packet): self._sock.sendall(packet) def send_bytes_serial(self, packet): self._serialconnection.write(packet) def read_bytes(self, length): if self._serialport is not None: return self.read_bytes_serial(length) else: return self.read_bytes_tcp(length) def read_bytes_tcp(self, length): return self._sock.recv(length) def read_bytes_serial(self, length): return self._serialconnection.read(length) def run(self): # if you want to create child threads, do not make them daemon = True! # They will not shutdown properly. (It's a python bug) self.alive = True self._sh.scheduler.add('ag980-init', self._getfirststatus, prio=5, cycle=600, offset=2) maxloops = 20 loops = 0 while self.alive and not self._initread and loops < maxloops: # wait for init read to finish time.sleep(0.5) loops += 1 self._sh.scheduler.remove('ag980-init') def stop(self): self._sh.scheduler.remove('ag980-init') self.alive = False self.disconnect() def parse_item(self, item): if 'ag980_cmd' in item.conf: commandname = item.conf['ag980_cmd'] if commandname in CONST_MAP_COMMANDS: self.log_debug('Item {} writes by using command \'{}\''.format(item, commandname)) self._params[commandname] = {'cmd': item.id(), 'item': item} #self.log_debug('Parameterliste: {} '.format(self._params)) return self.update_item else: self.log_err('Item {} contains invalid command \'{}\'!'.format(item, commandname)) return None else: return None def parse_logic(self, logic): pass def update_item(self, item, caller=None, source=None, dest=None): if caller != 'ag980': cmd = item.conf['ag980_cmd'] value = item() self.log_debug('Update command: "{}" with value "{}"'.format(cmd,value)) if self._initread: if cmd == 'status': self.send_command(cmd, value) elif cmd == 'speakeroff': self.send_command(cmd, value) elif cmd == 'speakera': self.send_command(cmd, value) elif cmd == 'speakerb': self.send_command(cmd, value) elif cmd == 'speakerc': self.send_command(cmd, value) elif cmd == 'speakerd': self.send_command(cmd, value) # Main Zone elif cmd == 'main': self.send_command('on' if value else 'off', value) elif cmd == 'mainmute': self.send_command(cmd, value) elif cmd == 'mainvolumeup': self.send_command(cmd, value) elif cmd == 'mainvolumedown': self.send_command(cmd, value) elif cmd == 'maininput': # restrict possible Inputs value == self._restrictinput(value) if value == 2: self.send_command('maintuner', value) elif value == 4: self.send_command('maincd', value) elif value == 5: self.send_command('mainaux', value) elif value == 6: self.send_command('mainphono', value) elif value == 7: self.send_command('maindvd', value) elif value == 8: self.send_command('maintape', value) elif cmd == 'room2': self.send_command('room2', value) elif cmd == 'r2mute': self.send_command(cmd, value) elif cmd == 'r2volumeup': self.send_command(cmd, value) elif cmd == 'r2volumedown': self.send_command(cmd, value) elif cmd == 'r2input': # restrict possible Inputs value == self._restrictinput(value) if value == 2: self.send_command('r2tuner', value) elif value == 4: self.send_command('r2cd', value) elif value == 5: self.send_command('r2aux', value) elif value == 6: self.send_command('r2phone', value) elif value == 7: self.send_command('r2dvd', value) elif value == 8: self.send_command('r2tape', value) def send_command(self, commandname, value=None): try: self.log_debug('Got a new send job: Command "{}"'.format(commandname)) # Get command hexcode commandcode = CONST_SEND_COMMANDS[commandname] packet = bytearray() packet.extend(self.int2bytes(self._packetstart, 2)) packet.extend(self.int2bytes(commandcode, 1)) #self.log_debug('packet: "{}"'.format(packet)) checksum = self.calc_checksum(packet) packet.extend(self.int2bytes(checksum, 1)) # Use a lock to allow only one sender at a time self._lock.acquire() if not self.connected: raise Exception("No connection to ag980.") try: self.log_info('Preparing command {} with hexcode \'{}\' to be sent.'.format(commandname, self.bytes2hexstring(packet))) self.send_bytes(packet) self.log_info('Successfully sent packet: {}'.format(self.bytes2hexstring(packet))) except Exception as e: raise Exception('Exception while sending: {}'.format(e)) #if send status request, get the response if commandname == 'status': packet = bytearray() firstpartlen = 3 while self.alive and len(packet) < firstpartlen: try: bytestoreceive = firstpartlen - len(packet) self.log_debug('Trying to receive {} bytes for the first part of the response.'.format(bytestoreceive)) chunk = self.read_bytes(bytestoreceive) self.log_debug('Received chunk of response: {}'.format(self.bytes2hexstring(chunk))) packet.extend(chunk) except socket.timeout: raise Exception("error receiving first part of packet: timeout") except Exception as e: raise Exception("error receiving first part of packet: {}".format(e)) #packet length fix at 20 bytes packetlen = 20 # Try to receive the second part of the packet while self.alive and len(packet) < packetlen: try: #Receive next chunk bytestoreceive = packetlen - len(packet) self.log_debug('Trying to receive {} bytes for the second part of the response.'.format(bytestoreceive)) chunk = self.read_bytes(bytestoreceive) self.log_debug('Received chunk of response: {}'.format(self.bytes2hexstring(chunk))) packet.extend(chunk) except socket.timeout: raise Exception("error receiving second part of packet: timeout") except Exception as e: raise Exception("error receiving second part of packet: {}".format(e)) # Parse response self.parse_response(packet) except Exception as e: self.disconnect() self.log_err("send_command failed: {}".format(e)) finally: # At the end, release the lock self._lock.release() def parse_response(self, response): resph = self.bytes2int(response) self.log_info('Successfully received response: {}'.format(self.bytes2hexstring(response))) # Remove System ID and checksum to get the data rawdatabytes = response[2:-1] # Received Checksum receivedchecksum = response[-1] #self.log_debug('Corresponding read command: {}, decoded data: {} (raw: {})'.format(self.bytes2hexstring(commandcodebytes), self.bytes2hexstring(databytes), self.bytes2hexstring(rawdatabytes))) # Validate checksum checksum = self.calc_checksum(rawdatabytes) if (receivedchecksum != checksum): self.log_err('Calculated checksum of {} does not match received checksum of {}! Ignoring reponse.'.format(self.byte2hexstring(checksum), self.byte2hexstring(receivedchecksum))) return # self.log_debug('--') textstring = response[3:12] # self.log_debug('Successfully received Textstring: "{}"'.format(textstring.decode("utf-8"))) self._params['display']['item'](textstring.decode("utf-8")) systemstatus = bin(response[13])[2:].zfill(8) # self.log_debug('Successfully received systemstatus: "{}"'.format(systemstatus)) # self.log_debug('Successfully received System Power Status: "{}"'.format(systemstatus[7])) self._params['main']['item'](self._str2bool(systemstatus[7])) # self.log_debug('Successfully received System Request Error: "{}"'.format(systemstatus[6])) # self.log_debug('Successfully received R2 Status: "{}"'.format(systemstatus[5])) self._params['room2']['item'](self._str2bool(systemstatus[5])) # self.log_debug('Successfully received Main Mute: "{}"'.format(systemstatus[4])) self._params['mainmute']['item'](self._str2bool(systemstatus[4])) # self.log_debug('Successfully received R2 Mute: "{}"'.format(systemstatus[3])) self._params['r2mute']['item'](self._str2bool(systemstatus[3])) # self.log_debug('Successfully received Sleep Mode: "{}"'.format(systemstatus[2])) # self.log_debug('Successfully received Cinema EQ: "{}"'.format(systemstatus[1])) # self.log_debug('Successfully received Tone defeat: "{}"'.format(systemstatus[0])) # self.log_debug('--') maininput = response[14] self.log_debug('Successfully received maininput "{}"'.format(maininput)) self._params['maininput']['item'](maininput) r2input = response[15] self._params['r2input']['item'](r2input) mainvolume = response[16] self._params['mainvolstat']['item'](mainvolume) r2volume = response[17] self._params['r2volstat']['item'](r2volume) speakerstatus = bin(response[18])[2:].zfill(8) # self.log_debug('Successfully received r2input: "{}"'.format(self.byte2hexstring(r2input))) # self.log_debug('--') # self.log_debug('Successfully received mainvolume: "{}"'.format(self.byte2hexstring(mainvolume))) # self.log_debug('Successfully received r2volume: "{}"'.format(self.byte2hexstring(r2volume))) self.log_debug('--') self.log_debug('Successfully received Speakerstatusbyte: "{}"'.format(speakerstatus)) #self.log_debug('Successfully received Speakerstatus A: "{}"'.format(self._str2bool(speakerstatus[7]))) #self.log_debug('Test: "{}"'.format(self._params['speakera']['item'])) self._params['speakera']['item'](self._str2bool(speakerstatus[7])) self.log_debug('Successfully received Speakerstatus B: "{}"'.format(self._str2bool(speakerstatus[6]))) #self.log_debug('Test: "{}"'.format(self._params['speakerb']['item'])) self._params['speakerb']['item'](self._str2bool(speakerstatus[6])) self._params['speakerc']['item'](self._str2bool(speakerstatus[5])) self._params['speakerd']['item'](self._str2bool(speakerstatus[4])) def _str2bool(self, var): if var == '1': return True else: return False def _getfirststatus(self): try: # Do the init read commands if self.connected: self.log_info('Starting initial read commands.') self.send_command('status', None ) finally: self._initread = True def _restrictinput(self, val): if val == 0 or val == 1: return 2 elif val == 3: return 2 elif val > 8: return 8 return val def calc_checksum(self, packetpart): return (sum(packetpart)) % 256 def log_debug(self, text): logger.debug('AG980: {}'.format(text)) def log_info(self, text): logger.info('AG980: {}'.format(text)) def log_err(self, text): logger.error('AG980: {}'.format(text)) def int2bytes(self, value, length): # Limit value to the passed byte length value = value % (2 ** (length * 8)) return value.to_bytes(length, byteorder='big') def bytes2int(self, bytesvalue): return int.from_bytes(bytesvalue, byteorder='big', signed=False) def bytes2hexstring(self, bytesvalue): return ":".join("{:02x}".format(c) for c in bytesvalue) def byte2hexstring(self, bytevalue): return "{:02x}".format(bytevalue)
Code:
['teac'] [['status']] name = Statusabfrage ag980_cmd = status type = bool knx_dpt = 1 knx_send = 6/0/0 knx_listen = 6/0/0 enforce_updates = on [['display']] name = Text Display ag980_cmd = display type = str knx_dpt = 16.001 knx_send = 6/0/1 knx_reply = 6/0/1 [['speaker']] name = Lautsprecher alle Aus ag980_cmd = speakeroff type = bool knx_dpt = 1 knx_send = 6/0/10 knx_reply = 6/0/10 enforce_updates = on [[['speakerA']]] name = Lautsprecher A ag980_cmd = speakera type = bool knx_dpt = 1 knx_send = 6/0/11 knx_listen = 6/0/11 [[['speakerB']]] name = Lautsprecher B ag980_cmd = speakerb type = bool knx_dpt = 1 knx_send = 6/0/12 knx_listen = 6/0/12 [[['speakerC']]] name = Lautsprecher C ag980_cmd = speakerc type = bool knx_dpt = 1 knx_send = 6/0/13 knx_listen = 6/0/13 [[['speakerD']]] name = Lautsprecher D ag980_cmd = speakerd type = bool knx_dpt = 1 knx_send = 6/0/14 knx_listen = 6/0/14 [['main']] name = Haupzone [[['an']]] name = Main Ein- Ausschalten ag980_cmd = main type = bool knx_dpt = 1 knx_send = 6/0/20 knx_listen = 6/0/20 [[['mainvolstatus']]] name = Main Volume Status ag980_cmd = mainvolstat type = num knx_dpt = 5 knx_send = 6/0/21 knx_reply = 6/0/21 [[['mainmute']]] name = Main Stumm AN/AUS ag980_cmd = mainmute type = bool knx_dpt = 1 knx_send = 6/0/22 knx_listen = 6/0/22 [[['volumeup']]] name = Main Volume up ag980_cmd = mainvolumeup type = bool knx_dpt = 1 knx_send = 6/0/23 knx_listen = 6/0/23 enforce_updates = on [[['volumedown']]] name = Main Volume down ag980_cmd = mainvolumedown type = bool knx_dpt = 1 knx_send = 6/0/24 knx_listen = 6/0/24 enforce_updates = on [[['quelle']]] name = Main Quelle ag980_cmd = maininput type = num knx_dpt = 5 knx_send = 6/0/25 knx_listen = 6/0/25 enforce_updates = on [['room2']] name = Raum 2 [[['an']]] name = Raum2 Ein- Ausschalten ag980_cmd = room2 type = bool knx_dpt = 1 knx_send = 6/0/30 knx_listen = 6/0/30 [[['r2volstatus']]] name = Raum2 Volume Status ag980_cmd = r2volstat type = num knx_dpt = 5 knx_send = 6/0/31 knx_listen = 6/0/31 [[['r2mute']]] name = Raum2 Stumm AN/AUS ag980_cmd = r2mute type = bool knx_dpt = 1 knx_send = 6/0/32 knx_listen = 6/0/32 [[['r2volup']]] name = Raum2 Volume up ag980_cmd = r2volumeup type = bool knx_dpt = 1 knx_send = 6/0/33 knx_listen = 6/0/33 enforce_updates = on [[['r2voldown']]] name = Raum2 Volume down ag980_cmd = r2volumedown type = bool knx_dpt = 1 knx_send = 6/0/34 knx_listen = 6/0/34 enforce_updates = on [[['quelle']]] name = Raum2 Quelle ag980_cmd = r2input type = num knx_dpt = 5 knx_send = 6/0/35 knx_listen = 6/0/35
als Readme.txt angehängt
Das ist (m)ein erster Versuch, hoffe nicht kompletter Mist, den ich da zusammengebaut habe.
Auf jeden Fall kann ich damit per USB-RS232 oder per Moxa (in meinem Fall ist es ein XPORT) den Teac steuern.
Wenn Ihr jetzt noch etwas helfen könntet wirds vielleicht ganz gut nutzbar (hoffe ich).
@mknx: Einen Git Account habe ich erstellt, jetzt muss ich noch ne Erklärung finden wie ich das hochlade...
Danke an mknx für smarthome.py und die Ersteller der anderen Plugins als Vorlagenlieferanten!
Kommentar