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