# -*- coding: latin-1 -*-
#
# YamahaSerial V0.0.1
# ================
# Written by Mark Clarkson, <mark@hollowhills.net>
# Public Domain
#
# Small trivial plugin to control Yamaha RX-V1000 AMP via RS-232
#
# Data from the AVR is turned into events. Description of
# commands and their replies can be found in the document
# "RS232C.PDF", which is available from the Yamaha.  The document
# was once available on the Yamaha website, but now is only available
# by request.
#
# Credit is due Oliver Wagner, <owagner@hometheatersoftware.com> who
# developed the DenonSerial plug-in for EG, and upon who'se code this
# plug-in is based.
# Credit is also due to Jens Samuelson who developed the Yamaha RX-V1500
# lua script for Girder 4.  I originally adapted Mr. Samuelson's code to
# control the RX-V1000 in Girder 4, and have adapted it again here for EG.
#
# Obviously, this code has been patched together from the work of others;
# it may not be perfect, but it works (please cut me some slack, I'm an
# engineer, not a programmer).
#
# I have not implemented all of the functionality included in the original
# code upon which this plug-in is based; I don't need it right now. Items
# not included, but that I may add sometime in the future:
#   Decode power-on response    - Now I only identify that power
#                               has been turned on
#   Tuner control               - I don't use the tuner, so have not included any
#                               of the tuner functions
#   Error Checking              - I have included no error checking/handling at all;
#                               neither for the script, not for the RX-V1000
#   Direct volume control       - Mr. Wagner's DenonSerial plug-in incorporates
#                               direct volume control and a fader.
#   String Hex Processing       - A better way to handle string hex --> int conversion
#
# Revision history:
# -----------------
# 0.0.1 - initial
# 0.0.2 - Martin Stafford - Added Command String decoding and support for x600 series

import eg

class PluginInfo(eg.PluginInfo):
    name = "Yamaha RX-Vxxxx Serial"
    author = "Mark Clarkson"
    version = "0.0.2"
    kind = "external"
    description = "Plugin to control Yamaha RX-Vxxxx receivers using RS232"

import wx
import thread
import time
import re
import types

from eg import SpinIntCtrl

debug = False
osd = True

cmdList = (
    ('PowerAllOn', 'Power All On', '07a1d', None),
    ('PowerAllOff', 'Power All Standby', '07a1e', None),
    ('PowerZone1On', 'Power Zone 1 On', '07e7e', None),
    ('PowerZone1Off', 'Power Zone 1 Standby', '07e7f', None),
    ('PowerZone2On', 'Power Zone 2 On', '07eba', None),
    ('PowerZone2Off', 'Power Zone 2 Standby', '07ebb', None),
    
    ('ReadyCmd', 'Ready Command', '\x11000\x03', None),
    ('ResetCmd', 'Reset all RS232 controlled settings', '\x13\x7f\x7f\x7f\x03', None),
    
    ('System Commands', None, None, None),
    ('RequestTuningFreq','Request Tuning Frequency', '22000', None),
    ('RequestMainVol','Request Main Volume', '22001', None),
    ('RequestInputName','Request Input Name', '22003', None),
    ('RequestZone2InputName','Request Zone 2 Input Name', '22004', None),
    
    ('Volume Controls', None, None, None),
    ('Up', 'Master Volume Up', '07a1a', None),
    ('Down', 'Master Volume Down', '07a1b', None),
    ('MuteOn', 'Mute On', '07ea2', None),
    ('MuteOff', 'Mute Off', '07ea3', None),
    
    ('Speakers', None, None, None),
    ('SpeakersAOn', 'Speakers A On', '07eab', None),
    ('SpeakersAOff', 'Speakers A Off', '07eac', None),
    ('SpeakersBOn', 'Speakers B On', '07ead', None),
    ('SpeakersBOff', 'Speakers B Off', '07eae', None),

    ('Inputs/Routing', None, None, None),
    ('InputPhono', 'Input Phono', '07a14', None),
    ('InputCD', 'Input CD', '07a15', None),
    ('InputTuner', 'Input Tuner', '07a16', None),
    ('InputCDR', 'Input CD-R', '07a19', None),
    ('InputMDTape', 'Input MD/TAPE', '07ac9', None),
    ('InputDVD', 'Input DVD', '07ac1', None),
    ('InputDTVLD', 'Input DTV/LD', '07a54', None),
    ('InputCable', 'Input Cable/Sat', '07ac0', None),
    ('InputVCR1', 'Input VCR-1', '07a0f', None),
    ('InputVCR2DVR', 'Input VCR-2/DVR', '07a13', None),
    ('InputAux', 'Input Aux', '07a55', None),
    
    ('Input Mode', None, None, None),
    ('InputAuto', 'Auto Audio Signal Mode', '07ea6', None),
    ('InputDTS', 'DTS Audio Signal Mode', '07ea8', None),
    ('InputAnalog', 'Analog Audio Signal Mode', '07eaa', None),
    
    ('OSD', None, None, None),
    ('OSDOff', 'OSD Off', '07eb0', None),
    ('OSDShort', 'Short OSD', '07eb1', None),
    ('OSDFull', 'Full OSD', '07eb2', None),
    
    ('DSP', None, None, None),
    ('Straight', 'DSP Effect Off', '07ee0', None),
    ('HallMunich', 'Concert Hall Munich', '07ee1', None),
    ('HallVienna', 'Concert Hall Vienna', '07ee5', None),
    ('HallFrieburg', 'Concert Hall Frieburg', '07ee8', None),
    ('HallBottomLine', 'Concert Hall The Bottom Line', '07eec', None),
    ('HallRoxyTheatre', 'Concert Hall The Roxy Theatre', '07eed', None),
    ('Disco', 'Game', '07ee0', None),
    ('Game', 'Game', '07ee1', None),
    ('Stereo2ch', '2ch Stereo', '07ec0', None),
    ('Stereo7ch', '7ch Stereo', '07eef', None),
    ('PopRock', 'Pop/Rock', '07ef3', None),
    ('ClassicalOpera', 'Classical/Opera', '07ef5', None),
    ('MonoMovie', 'Mono Movie', '07ef7', None),
    ('TVSports', 'TV Sports', '07ef8', None),
    ('Spectacle', 'Spectacle', '07ef9', None),
    ('SciFi', 'Sci-Fi', '07efa', None),
    ('Adventure', 'Adventure', '07efb', None),
    ('General', 'General', '07efc', None),
    ('Standard', 'Standard', '07efd', None),
    ('Enhanced', 'Enhanced', '07efe', None),
    ('THXCinema', 'THX Cinema', '07ec2', None),
    ('THXMusic', 'THX Music', '07ec3', None),
    ('THXGame', 'THX Game', '07ec3', None)
)

class YamahaRXV(eg.PluginClass):
    canMultiLoad = True

    def __init__(self):
        self.serial = None
        group = self
        
        self.CurrentVolume = {
            'holder':0
        }
        
        self.ReceiverVersion = {
            'R0191':'RXV-1600',
            'R0192':'HTR-5990',
            'R0193':'RXV-2600'
        }
        
        # a list of events with a prefix and then the position of additional characters
        # for the event from the configuration string
        self.ConfigurationCommand = {
            '10':(31), #Playback
            '11':(32), #Frequency
            '12':(33), #ES/EX
            '13':(34), #THR/Bypass
            '14':(35), #RED DTS
            '16':(43), #DTS 96/24
            '20':(8), #Power
            '21':(10,9), #Input
            '22':(40,11), #InputMode
            '23':(12), #Mute
            '24':(13), #Zone2Input
            '25':(14), #Zone2Mute
            '26':(15,16), #Volume
            '27':(17,18), #Zone2 Volume
            '28':(19,20), #Program
            '29':(25), #Tuner Page
            '2A':(26), #Tuner Preset
            '2C':(24), #Sleep
            '2D':(22), #Ext Sur
            '2B':(23), #OSD
            '2E':(29), #SpeakerA
            '2F':(30), #SpeakerB
            '35':(37), #Tuner Band
            '34':(36), #Headphone
            '3A':(42), #DC1 Trigger Control
            '3B':(44), #DC2 Trigger Control
            '3C':(45), #DC2 Trigger Control
            '3D':(46), #SP B Set
            '3E':(47), #Zone 2 Amp
            'B2':(123), #Fan Control Mode
            'B3':(124), #Speaker Impedance
            'A0':(127), #Zone 3 Input
            'A1':(128), #Zone 3 Mute
            'A2':(129,130), #Zone 3 Volume 
            'BC':(136), # TV Format
            'BD':(144) # Wake on RS232
        }

        self.TypList = {
            '0':'RS232',
            '1':'IR',
            '2':'Keys',
            '3':'System',
            '4':'Encoder'
        }
        
        self.GRDList = {
            '0':'NoGuard',
            '1':'GuardBySystem',
            '2':'GuardBySetting'
        }
                
        self.EventList = {
            '00':{
                'content':'NoGuard',
                '00':'Ok',
                '01':'Busy',
                '02':'PowerOff'
                },
            '01':{
                'content':'Warning',
                '00':'OverCurrent',
                '01':'DCDetect',
                '02':'PowerTrouble',
                '03':'OverHeat'
                },
            '10':{
                'content':'Playback',
                '00':'ExternalDecoder',
                '01':'Analog',
                '02':'PCM',
                '03':'DD',
                '04':'DD20',
                '05':'DDKaraoke',
                '06':'DD61',
                '07':'DTS',
                '08':'DTSES',
                '09':'Digital'
                },
            '11':{
                'content':'Fs',
                '00':'Analog',
                '01':'32kHz',
                '02':'441kHz',
                '03':'48kHz',
                '04':'64kHz',
                '05':'882kHz',
                '06':'96kHz',
                '07':'Unknown'
                },
            '12':{
                'content':'61ES',
                '00':'On',
                '01':'Off'
                },
            '13':{
                'content':'ThrBypass',
                '00':'Normal',
                '01':'Bypass'
                },
            '14':{
                'content':'REDDTS',
                '00':'Release',
                '01':'Wait'
                },
            '15':{
                'content':'Tuner',
                '00':'NotTuned',
                '01':'Tuned'
                },
            '16':{
                'content':'DTS9624',
                '00':'Off',
                '01':'On'
                },
            '20':{
                'content':'Power',
                '00':'AllOff',
                '01':'AllOn',
                '02':'Zone1OnZone2Off',
                '03':'Zone1OffZone2On'
                },
            '21':{
                'content':'Input',
                '00':'Phono',
                '01':'CD',
                '02':'Tuner',
                '03':'CDR',
                '04':'MDTape',
                '05':'DVD',
                '06':'DTV',
                '07':'Cable',
                '08':'Sat',
                '09':'VCR1',
                '0A':'DVR/VCR2',
                '0B':'VCR3/DVR',
                '0C':'V-Aux',
                '0E':'XM',
                '10':'Phono.MultiCH',
                '11':'CD.MultiCH',
                '12':'Tuner.MultiCH',
                '13':'CDR.MultiCH',
                '14':'MDTape.MultiCH',
                '15':'DVD.MultiCH',
                '16':'DTV.MultiCH',
                '07':'Cable.MultiCH',
                '08':'Sat.MultiCH',
                '09':'VCR1.MultiCH',
                '0A':'DVR/VCR2.MultiCH',
                '0B':'VCR3/DVR.MultiCH',
                '0C':'V-Aux.MultiCH',
                '0E':'XM.MultiCH'
                },
            '22':{
                'content':'InputMode',
                '00':'Auto',
                '03':'Coax/Optical',
                '04':'Analog',
                '05':'AnalogOnly',
                '08':'HDMI',
                '10':'DTS',
                '13':'CoaxOptical.DTS',
                '14':'Analog.DTS',
                '15':'AnalogOnly.DTS',
                '18':'HDMI.DTS',
                '20':'AAC',
                '23':'CoaxOptical.AAC',
                '24':'Analog.AAC',
                '25':'AnalogOnly.AAC',
                '28':'HDMI.AAC'
                },
            '23':{
                'content':'Mute',
                 '00':'Off',
                 '01':'On'
                },
            '24':{
                'content':'Zone2Input',
                '00':'Phono',
                '01':'CD',
                '02':'Tuner',
                '03':'CDR',
                '04':'MDTape',
                '05':'DVD',
                '06':'DTV',
                '07':'Cable',
                '08':'Sat',
                '09':'VCR1',
                '0A':'DVR/VCR2',
                '0B':'VCR3/DVR',
                '0C':'V-Aux',
                '0E':'XM'
                },
            '25':{
                'content':'Zone2Mute',
                '00':'Off',
                '01':'On'
                },
            '26':{
                'content':'Volume'
                },
            '27':{
                'content':'Volume.Zone2'
                },
            '28':{
                'content':'DSP',
                '00':'Hall1',
                '05':'Hall2',
                '08':'Church',
                '0D':'Jazz',
                '10':'Rock',
                '14':'Stadium',
                '17':'7chStereo',
                '18':'Disco',
                '19':'Game',
                '1A':'6chStereo',
                '1D':'TVSports',
                '20':'MonoMovie',
                '24':'70mmSpectacle',
                '25':'70mmSciFi',
                '28':'70mmAdventure',
                '29':'70mmGeneral',
                '2C':'Normal',
                '2D':'Enhanced',
                '30':'PLIIMovie',
                '31':'PLIIMusic',
                '32':'Neo6Movie',
                '33':'Neo6Music',
                '34':'2chStereo',
                '35':'2chDirectStereo',
                '36':'THXCinema',
                '37':'THXMusic',
                '3C':'THXGame',
                '80':'Straight.Hall1',
                '85':'Straight.Hall2',
                '88':'Straight.Church',
                '8D':'Straight.Jazz',
                '90':'Straight.Rock',
                '94':'Straight.Stadium',
                '97':'Straight.7chStereo',
                '98':'Straight.Disco',
                '99':'Straight.Game',
                '9A':'Straight.6chStereo',
                '9D':'Straight.TVSports',
                'A0':'Straight.MonoMovie',
                'A4':'Straight.70mmSpectacle',
                'A5':'Straight.70mmSciFi',
                'A8':'Straight.70mmAdventure',
                'A9':'Straight.70mmGeneral',
                'AC':'Straight.Normal',
                'AD':'Straight.Enhanced',
                'B0':'Straight.PLIIMovie',
                'B1':'Straight.PLIIMusic',
                'B2':'Straight.Neo6Movie',
                'B3':'Straight.Neo6Music',
                'B4':'Straight.2chStereo',
                'B5':'Straight.2chDirectStereo',
                'B6':'Straight.THXCinema',
                'B7':'Straight.THXMusic',
                'BC':'Straight.THXGame'
                },
            '29':{
                'content':'TunerPage',
                '00':'A',
                '01':'B',
                '02':'C',
                '03':'D',
                '04':'E'
                },
            '2A':{
                'content':'TunerPreset',
                '00':'1',
                '01':'2',
                '02':'3',
                '03':'4',
                '04':'5',
                '05':'6',
                '06':'7',
                '07':'8'
                },
            '2B':{
                'content':'OSD',
                '00':'Full',
                '01':'Short',
                '02':'Off'
                },
            '2C':{
                'content':'Sleep',
                '00':'120',
                '01':'90',
                '02':'60',
                '03':'30',
                '04':'Off'
                },
            '2D':{
                'content':'EXTDSur',
                '00':'Off',
                '01':'EXES',
                '02':'DiscreteOn',
                '03':'Auto',
                '04':'EX',
                '05':'PLIIxMovie',
                '06':'PLIIxMusic'
                },
            '2E':{
                'content':'SpkrRelayA',
                '00':'Off',
                '01':'On'
                },
            '2F':{
                'content':'SpkrRelayB',
                '00':'Off',
                '01':'On'
                },
            '30':{
                'content':'HomeBank',
                '00':'Main',
                '01':'A',
                '02':'B',
                '03':'C'
                },
            '31':{
                'content':'HomePreset',
                '00':'A',
                '01':'B',
                '02':'C'
                },
            '32':{
                'content':'VolumeBank',
                '00':'Main',
                '01':'A',
                '02':'B',
                '03':'C'
                },
            '33':{
                'content':'VolumePreset',
                '00':'A',
                '01':'B',
                '02':'C'
                },
            '34':{
                'content':'Headphone',
                '00':'Off',
                '01':'On'
                },
            '35':{
                'content':'FMAM',
                '00':'FM',
                '01':'AM'
                },
            '36':{
                'content':'DC1TriggerOutput',
                '00':'Off',
                '01':'On'
                },
            '3A':{
                'content':'DC1TriggerControl',
                '00':'AllZone',
                '01':'Zone1',
                '02':'Zone2',
                '03':'Zone3'
                },
            '3B':{
                'content':'DC2TriggerControl',
                '00':'AllZone',
                '01':'Zone1',
                '02':'Zone2',
                '03':'Zone3'
                },
            '3C':{
                'content':'DC2TriggerOutput',
                '00':'Off',
                '01':'On'
                },
            '3D':{
                'content':'SPBSet',
                '00':'Zone1',
                '01':'Zone2'
                },
            '3E':{
                'content':'Zone2Amp',
                '00':'External',
                '01':'Internal.Surround',
                '02':'Internal.Presence',
                '03':'Internal.Both'
                },
            'A0':{
                'content':'Zone3Input',
                '00':'Phono',
                '01':'CD',
                '02':'Tuner',
                '03':'CDR',
                '04':'MDTape',
                '05':'DVD',
                '06':'DTVLD',
                '07':'Cable',
                '09':'VCR1',
                '0A':'DVR/VCR2',
                '0B':'VAux',
                '0C':'V-Aux',
                '0E':'XM'
                },
            'A1':{
                'content':'Zone3Mute',
                '00':'Off',
                '01':'On'
                },
            'A2':{
                'content':'Volume.Zone3'
                },
            'A5':{
                'content':'MuteType',
                '00':'Full',
                '01':'-20db'
                },
            'B2':{
                'content':'FanControl',
                '00':'Auto',
                '01':'Cont'
                },
            'B3':{
                'content':'SpeakerImpedance',
                '00':'8ohm',
                '01':'6ohm'
                },
            'BC':{
                'content':'TVFormat',
                '00':'PAL',
                '01':'NTSC'
                },
            'BD':{
                'content':'WakeOnRS232',
                '00':'No',
                '01':'Yes'
                }
            }

        def createWriter(cmd):
            def write(self):
                if not('\x11' in cmd or '\x12' in cmd or '\x13' in cmd or '\x03' in cmd or '\x02' in cmd):
                    self.plugin.serial.write('\x02')
                if debug: print("SerialOut:"+cmd)
                self.plugin.serial.write(cmd)
                self.plugin.serial.write('\x03')
            return write
                        
        for cmd_name, cmd_text, cmd_cmd, cmd_rangespec in cmdList:
            if cmd_text is None:
                # New subgroup, or back up
                if cmd_name is None:
                    group = self
                else:
                    group = self.AddGroup(cmd_name)
            else:
                # Argumentless command
                class Handler:
                    name = cmd_name
                    description = cmd_text
                    __call__ = createWriter(cmd_cmd)
                Handler.__name__ = cmd_name
                group.AddAction(Handler)

    def hexToDec(self, hexstring):
        hexconv = {'0':0,'1':1, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9, 'A':10, 'B':11, 'C':12, 'D':13, 'E':14, 'F':15}
        result = hexconv[hexstring[0]]*16+hexconv[hexstring[1]]
        return result

    # Serial port reader
    def reader(self):
        line=""
        configre=re.compile('(.)(.....)(.)(..)(.*)(..)')
        parmre=re.compile('(.)(.)(.)(....)')
        while self.readerkiller is False:
            ch=self.serial.read()
            if ch=='\x03':
                if debug: print("SerialIn:"+line)
                
                # Configuration Command
                if line[0]=='\x12':
                    m=configre.match(line)
                    if m is None:
                        print("Invalid Configuration Command: " + line)
                    else:
                        # Report the receiver version
                        if m.group(2) in self.ReceiverVersion:
                            print("Yamaha " + self.ReceiverVersion[m.group(2)] + "(" + m.group(3) + ") ")
                        else:
                            print("Unknown Yamaha receiver: " + m.group(2))
                        # Loop through the known (supported) ConfigurationCommand locations, build StatusReport codes and report them
                        # The first two characters of the status code is defined in self.ConfigurationCommand
                        # With the remaining characters pulled from positions in the ConfigurationCommand string. 
                        for command, locations in self.ConfigurationCommand.iteritems():
                            data="00"
                            if type(locations) == types.IntType:
                                if locations <= len(m.group(5)):
                                    data += m.group(5)[locations]
                            else:
                                for location in locations:
                                    if locations <= len(m.group(5)):
                                        data += m.group(5)[location]
                            self.statusReport(command + data[len(data)-2:])
                        
                # Status Report
                elif line[0]=='\x02':
                    m=parmre.match(line)
                    if m is None:
                        print("Invalid Status Report: " + line)
                    else:
                        self.statusReport(m.group(4))
                        
                line=""
            else:
                line+=ch
                
    def statusReport(self, code):
        reportre=re.compile('(..)(..)')
        m=reportre.match(code)
        if m is not None:
            if m.group(1) in self.EventList:
                command = self.EventList[m.group(1)]
                #Process volume separately
                if command['content']=='Volume' or command['content']=='Volume.Zone2' or command['content']=='Volume.Zone3':
                    self.processVolume(command['content'], m.group(2))
                    return
                else:
                    if m.group(2) in command:
                        self.TriggerEvent(command['content'] + '.' + command[m.group(2)])
                        return
        print('Unrecognised Status Report: '+code)
            
    #Process volume from hex string to -dB
    def processVolume(self, zone, volstring):
        step = 0.5
        volume = self.hexToDec(volstring)*step - 99.5
        
        if zone in self.CurrentVolume:
            volumeChange = ''
            if volume < self.CurrentVolume[zone]: volumeChange = 'Down'
            elif volume > self.CurrentVolume[zone]: volumeChange = 'Up'
            self.TriggerEvent(zone + '.' + volumeChange)         
        
        self.CurrentVolume[zone] = volume
        self.TriggerEvent(zone + '.' + str(volume))
        if osd: eg.plugins.EventGhost.ShowOSD(zone + ': ' + str(volume), None, (255, 255, 255), (0, 0, 0), 1, (0, 0), 0, 3.0)         

    def __start__(self, port):
        try:
            self.serial = eg.SerialPort(port)
        except:
            raise eg.Exception("Can't open serial port.")
        self.serial.baudrate = 9600
        self.serial.timeout = 30.0
        self.serial.setDTR(1)
        self.serial.setRTS(1)
        self.readerkiller = False
        thread.start_new_thread(self.reader,());
        # Send a ready command to initiate status dump
        #self.serial.write('\x11' + '000' + '\x03');
        
    def __stop__(self):
        self.readerkiller = True
        if self.serial is not None:
            self.serial.close()
            self.serial = None
            
    def Configure(self, port=0):
        dialog = eg.ConfigurationDialog(self)
        ports = eg.SerialPort.GetAllPorts()
        choices = []
        for portnum in ports:
            choices.append("COM%d" % (portnum + 1))
        portCtrl = wx.Choice(dialog, -1, choices=choices)
        portCtrl.SetSelection(port)
        
        flags = wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL
        mySizer = wx.FlexGridSizer(4, 2, 5, 5)
        mySizer.Add(wx.StaticText(dialog, -1, 'Port:'), 0, flags)
        mySizer.Add(portCtrl, 0, wx.EXPAND)

        dialog.sizer.Add(mySizer)
        
        if dialog.AffirmedShowModal():
            return (
                ports[portCtrl.GetSelection()],
            )
                    
