コード例 #1
0
class Kodi(SmartPlugin):
    '''
    Main class of the Plugin. Does all plugin specific stuff and provides
    the update functions for the items
    '''
    
    PLUGIN_VERSION = '1.3c.0'
    ALLOW_MULTIINSTANCE = True
    
    # list of all possible input actions for Kodi
    _possible_input_actions = [
        'left', 'right', 'up', 'down', 'pageup', 'pagedown', 'select', 'highlight',
        'parentdir', 'parentfolder', 'back', 'menu', 'previousmenu', 'info',
        'pause', 'stop', 'skipnext', 'skipprevious', 'fullscreen', 'aspectratio',
        'stepforward', 'stepback', 'bigstepforward', 'bigstepback',
        'chapterorbigstepforward', 'chapterorbigstepback', 'osd', 'showsubtitles',
        'nextsubtitle', 'cyclesubtitle', 'playerdebug', 'codecinfo', 'playerprocessinfo',
        'nextpicture', 'previouspicture', 'zoomout', 'zoomin', 'playlist', 'queue',
        'zoomnormal', 'zoomlevel1', 'zoomlevel2', 'zoomlevel3', 'zoomlevel4',
        'zoomlevel5', 'zoomlevel6', 'zoomlevel7', 'zoomlevel8', 'zoomlevel9',
        'nextcalibration', 'resetcalibration', 'analogmove', 'analogmovex',
        'analogmovey', 'rotate', 'rotateccw', 'close', 'subtitledelayminus',
        'subtitledelay', 'subtitledelayplus', 'audiodelayminus', 'audiodelay',
        'audiodelayplus', 'subtitleshiftup', 'subtitleshiftdown', 'subtitlealign',
        'audionextlanguage', 'verticalshiftup', 'verticalshiftdown', 'nextresolution',
        'audiotoggledigital', 'number0', 'number1', 'number2', 'number3', 'number4',
        'number5', 'number6', 'number7', 'number8', 'number9', 'smallstepback',
        'fastforward', 'rewind', 'play', 'playpause', 'switchplayer', 'delete', 'copy',
        'move', 'screenshot', 'rename', 'togglewatched', 'scanitem', 'reloadkeymaps',
        'volumeup', 'volumedown', 'mute', 'backspace', 'scrollup', 'scrolldown',
        'analogfastforward', 'analogrewind', 'moveitemup', 'moveitemdown', 'contextmenu',
        'shift', 'symbols', 'cursorleft', 'cursorright', 'showtime', 'analogseekforward',
        'analogseekback', 'showpreset', 'nextpreset', 'previouspreset', 'lockpreset', 'randompreset',
        'increasevisrating', 'decreasevisrating', 'showvideomenu', 'enter', 'increaserating',
        'decreaserating', 'setrating', 'togglefullscreen', 'nextscene', 'previousscene', 'nextletter',
        'prevletter', 'jumpsms2', 'jumpsms3', 'jumpsms4', 'jumpsms5', 'jumpsms6', 'jumpsms7', 'jumpsms8',
        'jumpsms9', 'filter', 'filterclear', 'filtersms2', 'filtersms3', 'filtersms4', 'filtersms5',
        'filtersms6', 'filtersms7', 'filtersms8', 'filtersms9', 'firstpage', 'lastpage', 'guiprofile',
        'red', 'green', 'yellow', 'blue', 'increasepar', 'decreasepar', 'volampup', 'volampdown',
        'volumeamplification', 'createbookmark', 'createepisodebookmark', 'settingsreset',
        'settingslevelchange', 'stereomode', 'nextstereomode', 'previousstereomode',
        'togglestereomode', 'stereomodetomono', 'channelup', 'channeldown', 'previouschannelgroup',
        'nextchannelgroup', 'playpvr', 'playpvrtv', 'playpvrradio', 'record', 'togglecommskip',
        'showtimerrule', 'leftclick', 'rightclick', 'middleclick', 'doubleclick', 'longclick',
        'wheelup', 'wheeldown', 'mousedrag', 'mousemove', 'tap', 'longpress', 'pangesture',
        'zoomgesture', 'rotategesture', 'swipeleft', 'swiperight', 'swipeup', 'swipedown', 'error', 'noop']

    _get_items = ['volume', 'mute', 'title', 'media', 'state', 'favorites']
    
    _set_items = {'volume': dict(method='Application.SetVolume', params=dict(volume='ITEM_VALUE')),
                  'mute'  : dict(method='Application.SetMute', params=dict(mute='ITEM_VALUE')),
                  'input' : dict(method='Input.ExecuteAction', params=dict(action='ITEM_VALUE')),
                  'on_off': dict(method='System.Shutdown', params=None)}
    
    def __init__(self, sh, *args, **kwargs):
        '''
        Initalizes the plugin.
        '''
        # init logger
        self.logger = logging.getLogger(__name__)
        self.logger.info('Init Kodi Plugin')
        self.host = self.get_parameter_value('host')
        self.port = self.get_parameter_value('port')
        self.kodi_tcp_connection = Tcp_client(host=self.host,
                                              port=self.port,
                                              name='KodiTCPConnection',
                                              autoreconnect=False,
                                              connect_retries=5,
                                              connect_cycle=5)
        self.kodi_tcp_connection.set_callbacks(connected=None,
                                               data_received=self.received_data,
                                               disconnected=self.on_disconnect)
        self.kodi_server_alive = False
#         self.terminator = 0
#         self.balance(b'{', b'}')
        self.message_id = 1
        self.response_id = None
        self.cmd_lock = threading.Lock()
        self.reply_lock = threading.Condition()
        self.reply = None
        self.registered_items = {key: [] for key in set(list(Kodi._set_items.keys()) + Kodi._get_items)}

    def run(self):
        '''
        Run method for the plugin
        '''        
        self.logger.debug('Plugin \'{}\': run method called'.format(self.get_shortname()))
        self.connect_to_kodi()
        self.alive = True

    def stop(self):
        '''
        Stop method for the plugin
        '''
        self.logger.debug('Plugin \'{}\': stop method called'.format(self.get_shortname()))
        self.kodi_tcp_connection.close()
        self.kodi_server_alive = False
        self.alive = False
    
    def on_connect(self):
        '''
        This method is called on a succesful connect to Kodi        
        On a connect first check if the JSON-RPC API is available.
        If this is the case initialize all items with values extracted from Kodi
        '''
        # check if API is available
        result = self.send_kodi_rpc(method='JSONRPC.Ping')
        if result is None or result['result'] != 'pong':
            self.logger.error('Kodi JSON-RPC API not available on {}:{}'.format(self.host, self.port))
            self.stop()
        else:
            # API available -> init items
            #
            # get volume and mute state
            result = self.send_kodi_rpc(method='Application.GetProperties',
                                        params=dict(properties=['volume', 'muted']))['result']
            for elem in self.registered_items['mute']:
                elem(result['muted'], caller='Kodi')
            for elem in self.registered_items['volume']:
                elem(result['volume'], caller='Kodi')
            # get the list of favorites
            result = self.send_kodi_rpc(method='Favourites.GetFavourites',
                                        params=dict(properties=['window', 'path', 'thumbnail', 'windowparameter']))['result']
            item_dict = dict()                                    
            if result['favourites'] is not None:
                item_dict = {elem['title']: elem for elem in result['favourites']}
            for elem in self.registered_items['favorites']:
                elem(item_dict, caller='Kodi')        
            # parse active player (if present)
            self._get_player_info()
    
    def on_disconnect(self):
        ''' function called when TCP connection to Kodi is disconnected '''
        self.logger.debug('Received disconnect from Kodi server')
        self.kodi_server_alive = False
        for elem in self.registered_items['on_off']:
            elem(self.kodi_server_alive, caller='Kodi')
    
    def connect_to_kodi(self):
        '''
        try to establish a new connection to Kodi
        
        While this method is called during the start-up phase of the plugin,
        it can also be used to establish a connection to the Kodi server if the
        plugin was initialized before the server went up.
        '''
        self.kodi_tcp_connection.connect()
        # we allow for 2 seconds to connect
        time.sleep(2)
        if not self.kodi_tcp_connection.connected():
            # no connection could be established, Kodi may be offline
            self.logger.info('Could not establish a connection to Kodi Server')
            self.kodi_server_alive = False
        else:
            self.kodi_server_alive = True
            self.on_connect()            
        for elem in self.registered_items['on_off']:
            elem(self.kodi_server_alive, caller='Kodi')

    def parse_item(self, item):
        '''
        Method for parsing Kodi items.
        If the item carries the kodi_item field, this item is registered to the plugin.
        :param item:    The item to process.
        :return:        The item update method to be triggered if the kodi_item is in the set item dict.
        '''
        if self.has_iattr(item.conf, 'kodi_item'):
            kodi_item = self.get_iattr_value(item.conf, 'kodi_item')
            self.logger.debug('Plugin \'%s\', instance \'%s\': registering item: %s',
                              self.get_shortname(),
                              self.get_instance_name(),
                              item)            
            if kodi_item in self.registered_items:
                self.registered_items[kodi_item].append(item)
            else:
                self.logger.warning('I do not know the kodi_item \'%s\', skipping!', kodi_item)
            if kodi_item in Kodi._set_items:
                return self.update_item

    def parse_logic(self, logic):
        '''
        Default plugin parse_logic method
        '''
        pass

    def update_item(self, item, caller=None, source=None, dest=None):
        '''
        Callback method for sending values to Kodi when a registered item has changed

        :param item: item to be updated towards the plugin
        :param caller: if given it represents the callers name
        :param source: if given it represents the source
        :param dest: if given it represents the dest
        '''
        item_value = item()
        if item_value:
            if caller != 'Kodi' and self.has_iattr(item.conf, 'kodi_item'):
                # update item was triggered from something else then this plugin -> send to Kodi
                kodi_item = self.get_iattr_value(item.conf, 'kodi_item')                
                
                if kodi_item == 'on_off' and item():
                    # handle the on_off item as special case:                    
                    # if item is on, try to establish a connection to Kodi
                    self.connect_to_kodi()
                    # if item is off send shutdown command to Kodi. This is
                    # handled in the standard block below though
                elif kodi_item in Kodi._set_items:
                    if kodi_item == 'input' and item() not in self._possible_input_actions:
                        self.logger.error('The action \'%s\' for the kodi_item \'input\' is not allowed, skipping!', item_value)
                    else:
                        self.logger.debug('Plugin \'%s\': update_item ws called with item \'%s\' from caller \'%s\', source \'%s\' and dest \'%s\'',
                                          self.get_shortname(), item, caller, source, dest)
                        method = self._set_items[kodi_item]['method']
                        params = self._set_items[kodi_item]['params']
                        if params is not None:
                            # copy so we don't interfer with the class variable
                            params = params.copy()
                            # replace the wild card ITEM_VALUE with the item's value
                            for key, value in params.items():
                                if value == 'ITEM_VALUE':
                                    params[key] = item_value
                        self.send_kodi_rpc(method, params, wait=False)
                else:
                    self.logger.info('kodi_item \'%s\' not in send_keys, skipping!', kodi_item)
    
    def notify(self, title, message, image=None, display_time=10000):
        '''
        Send a notification to Kodi to be displayed on the screen
        
        :param title: the title of the message
        :param message: the message itself
        :param image: an optional image to be displayed alongside the message
        :param display_time: how long the message is displayed in milli seconds
        '''
        params = dict(title=title, message=message, displaytime=display_time)
        if image is not None:
            params['image'] = image
        self.send_kodi_rpc(method='GUI.ShowNotification', params=params)

    def send_kodi_rpc(self, method, params=None, message_id=None, wait=True):
        '''
        Send a JSON RPC to Kodi.
        
        The  JSON string is extracted from the supplied method and the given parameters.        
        :param method: the Kodi method to be triggered
        :param params: parameters dictionary
        :param message_id: the message ID to be used. If none, use the internal counter
        :param wait: whether to wait for the reply from Kodi or send off the RPC asynchronously
                     If wait is True, this method returns a dictionary parsed from the JSON
                     response from Kodi
        '''
        reply = None
        if self.kodi_server_alive:
            self.cmd_lock.acquire()
            self.reply = None
            if message_id is None:
                self.message_id += 1
                message_id = self.message_id
                if message_id > 99:
                    self.message_id = 0
            self.response_id = message_id
            if params is not None:
                data = {'jsonrpc': '2.0', 'id': message_id, 'method': method, 'params': params}
            else:
                data = {'jsonrpc': '2.0', 'id': message_id, 'method': method}
            self.reply_lock.acquire()
            self.logger.debug('Kodi sending: {0}'.format(json.dumps(data, separators=(',', ':'))))
            self.kodi_tcp_connection.send((json.dumps(data, separators=(',', ':')) + '\r\n').encode())
            if wait:
                self.reply_lock.wait(2)
            self.reply_lock.release()
            reply = self.reply
            self.reply = None
            self.cmd_lock.release()
        else:
            self.logger.debug('JSON-RPC command requested without an established connection to Kodi.')
        return reply

    def received_data(self, connection, data):
        '''
        This method is called whenever data is received from the connection to
        Kodi.
        '''
        event = json.loads(data)
        self.logger.debug('Kodi receiving: {0}'.format(event))
        if 'id' in event:
            if event['id'] == self.response_id:
                self.response_id = None
                self.reply = event
            self.reply_lock.acquire()
            self.reply_lock.notify()
            self.reply_lock.release()
            return
        if 'method' in event:
            if event['method'] == 'Player.OnPause':
                for elem in self.registered_items['state']:
                    elem('Pause', caller='Kodi')
            elif event['method'] == 'Player.OnStop':
                for elem in self.registered_items['state']:
                    elem('Stopped Player', caller='Kodi')
                for elem in self.registered_items['media']:
                    elem('', caller='Kodi')
                for elem in self.registered_items['title']:
                    elem('', caller='Kodi')
            elif event['method'] == 'GUI.OnScreensaverActivated':
                for elem in self.registered_items['state']:
                    elem('Screensaver', caller='Kodi')
            if event['method'] in ['Player.OnPlay']:
                # use a different thread for event handling
                self.get_sh().trigger('kodi-player-start', self._get_player_info, 'Kodi')
            elif event['method'] in ['Application.OnVolumeChanged']:
                for elem in self.registered_items['mute']:
                    elem(event['params']['data']['muted'], caller='Kodi')
                for elem in self.registered_items['volume']:
                    elem(event['params']['data']['volume'], caller='Kodi')
    
    def _send_player_command(self, kodi_item):
        '''
        This method should only be called from the update item method in
        a new thread in order to handle Play/Pause and Stop commands to
        the active Kodi players
        '''
        # get the currently active players
        result = self.send_kodi_rpc(method='Player.GetActivePlayers')
        result = result['result']
        if len(result) == 0:
            self.logger.warning('Kodi: no active player found, skipping request!')
        else:
            if len(result) > 1:
                self.logger.info('Kodi: there is more than one active player. Sending request to each player!')
            for player in result:
                player_id = player['playerid']
                self.send_kodi_rpc(method=self._set_items[kodi_item]['method'],
                                   params=dict(playerid=player_id),
                                   wait=False)

    def _get_player_info(self):
        '''
        Extract information from Kodi regarding the active player and save it
        to the respective items
        '''
        result = self.send_kodi_rpc(method='Player.GetActivePlayers')['result']
        if len(result) == 0:
            self.logger.info('Kodi: no active player found.')
            for elem in self.registered_items['title']:
                elem('', caller='Kodi')
            for elem in self.registered_items['media']:
                elem('', caller='Kodi')
            for elem in self.registered_items['state']:
                elem('No Active Player', caller='Kodi')
            return
        playerid = result[0]['playerid']
        typ = result[0]['type']
        for elem in self.registered_items['state']:
            elem('Playing', caller='Kodi')
        if typ == 'video':
            result = self.send_kodi_rpc(method='Player.GetItem',
                                        params=dict(properties=['title'], playerid=playerid),
                                        message_id='VideoGetItem')['result']
            title = result['item']['title']
            typ = result['item']['type']
            if not title and 'label' in result['item']:
                title = result['item']['label']
            for elem in self.registered_items['media']:
                elem(typ.capitalize(), caller='Kodi')
        elif typ == 'audio':
            for elem in self.registered_items['media']:
                elem('Audio', caller='Kodi')                
            result = self.send_kodi_rpc(method='Player.GetItem',
                                        params=dict(properties=['title', 'artist'], playerid=playerid),
                                        message_id='AudioGetItem')['result']
            if len(result['item']['artist']) == 0:
                artist = 'unknown'
            else:
                artist = result['item']['artist'][0]
            title = artist + ' - ' + result['item']['title']
        elif typ == 'picture':
            for elem in self.registered_items['media']:
                elem('Picture', caller='Kodi')
            title = ''
        else:
            self.logger.warning('Kodi: Unknown type: {0}'.format(typ))
            return
        for elem in self.registered_items['title']:
            elem(title, caller='Kodi')
コード例 #2
0
class Kodi(SmartPlugin):
    '''
    Main class of the Plugin. Does all plugin specific stuff and provides
    the update functions for the items
    '''

    PLUGIN_VERSION = '1.5.0'
    ALLOW_MULTIINSTANCE = True

    # list of all possible input actions for Kodi except player specific actions
    _possible_input_actions = [
        'left', 'right', 'up', 'down', 'pageup', 'pagedown', 'select',
        'highlight', 'parentdir', 'parentfolder', 'back', 'menu',
        'previousmenu', 'osd', 'playlist', 'queue', 'nextcalibration',
        'resetcalibration', 'close', 'fullscreen', 'number0', 'number1',
        'number2', 'number3', 'number4', 'number5', 'number6', 'number7',
        'number8', 'number9', 'play', 'playpause', 'switchplayer', 'delete',
        'copy', 'moveitemup', 'moveitemdown', 'contextmenu', 'move',
        'screenshot', 'rename', 'togglewatched', 'scanitem', 'reloadkeymaps',
        'volumeup', 'volumedown', 'mute', 'backspace', 'scrollup',
        'scrolldown', 'shift', 'symbols', 'cursorleft', 'cursorright',
        'showpreset', 'nextpreset', 'previouspreset', 'lockpreset',
        'randompreset', 'increasevisrating', 'decreasevisrating',
        'showvideomenu', 'enter', 'increaserating', 'decreaserating',
        'setrating', 'togglefullscreen', 'nextletter', 'prevletter', 'filter',
        'filterclear', 'filtersms2', 'filtersms3', 'filtersms4', 'filtersms5',
        'filtersms6', 'filtersms7', 'filtersms8', 'filtersms9', 'firstpage',
        'lastpage', 'guiprofile', 'red', 'green', 'yellow', 'blue',
        'increasepar', 'decreasepar', 'volampup', 'volampdown',
        'volumeamplification', 'createbookmark', 'createepisodebookmark',
        'settingsreset', 'settingslevelchange', 'channelup', 'channeldown',
        'previouschannelgroup', 'nextchannelgroup', 'playpvr', 'playpvrtv',
        'playpvrradio', 'record', 'togglecommskip', 'showtimerrule',
        'leftclick', 'rightclick', 'middleclick', 'doubleclick', 'longclick',
        'wheelup', 'wheeldown', 'mousedrag', 'mousemove', 'tap', 'longpress',
        'pangesture', 'zoomgesture', 'rotategesture', 'swipeleft',
        'swiperight', 'swipeup', 'swipedown', 'error', 'noop'
    ]

    _possible_player_actions = [
        'pause', 'stop', 'skipnext', 'skipprevious', 'aspectratio',
        'stepforward', 'stepback', 'bigstepforward', 'bigstepback',
        'chapterorbigstepforward', 'chapterorbigstepback', 'showsubtitles',
        'nextsubtitle', 'cyclesubtitle', 'playerdebug', 'codecinfo',
        'playerprocessinfo', 'nextpicture', 'previouspicture', 'zoomout',
        'zoomin', 'zoomnormal', 'zoomlevel1', 'zoomlevel2', 'zoomlevel3',
        'zoomlevel4', 'zoomlevel5', 'zoomlevel6', 'zoomlevel7', 'zoomlevel8',
        'zoomlevel9', 'analogmove', 'analogmovex', 'analogmovey', 'rotate',
        'rotateccw', 'subtitledelayminus', 'subtitledelay',
        'subtitledelayplus', 'audiodelayminus', 'audiodelay', 'audiodelayplus',
        'subtitleshiftup', 'subtitleshiftdown', 'subtitlealign',
        'audionextlanguage', 'verticalshiftup', 'verticalshiftdown',
        'nextresolution', 'audiotoggledigital', 'smallstepback', 'fastforward',
        'rewind', 'analogfastforward', 'analogrewind', 'showtime',
        'analogseekforward', 'analogseekback', 'nextscene', 'previousscene',
        'jumpsms2', 'jumpsms3', 'jumpsms4', 'jumpsms5', 'jumpsms6', 'jumpsms7',
        'jumpsms8', 'jumpsms9', 'stereomode', 'nextstereomode',
        'previousstereomode', 'togglestereomode', 'stereomodetomono'
    ]

    _get_items = ['volume', 'mute', 'title', 'media', 'state', 'favourites']

    _set_items = {
        'volume':
        dict(method='Application.SetVolume', params=dict(volume='ITEM_VALUE')),
        'mute':
        dict(method='Application.SetMute', params=dict(mute='ITEM_VALUE')),
        'input':
        dict(method='Input.ExecuteAction', params=dict(action='ITEM_VALUE')),
        'on_off':
        dict(method='System.Shutdown', params=None),
        'home':
        dict(method='Input.Home', params=None),
        'player':
        dict(method='Player.GetActivePlayers', params=None)
    }

    _player_items = {
        'audiostream':
        dict(method='Player.SetAudioStream', params=dict(stream='ITEM_VALUE')),
        'subtitle':
        dict(method='Player.SetSubtitle',
             params=dict(subtitle='ITEM_VALUE[0]', enable='ITEM_VALUE[1]')),
        'seek':
        dict(method='Player.Seek', params=dict(value='ITEM_VALUE')),
        'speed':
        dict(method='Player.SetSpeed', params=dict(speed='ITEM_VALUE'))
    }

    _macro = {
        'resume': {
            "play":
            dict(method='Input.ExecuteAction', params=dict(action='play')),
            "wait":
            1,
            "resume":
            dict(method='Input.ExecuteAction', params=dict(action='select'))
        },
        'beginning': {
            "play":
            dict(method='Input.ExecuteAction', params=dict(action='play')),
            "wait":
            1,
            "down":
            dict(method='Input.ExecuteAction', params=dict(action='down')),
            "select":
            dict(method='Input.ExecuteAction', params=dict(action='select'))
        }
    }

    _initcommands = {
        "ping": {
            "method": "JSONRPC.Ping"
        },
        "getvolume": {
            "method": 'Application.GetProperties',
            "params": dict(properties=['volume', 'muted'])
        },
        "favourites": {
            "method":
            'Favourites.GetFavourites',
            "params":
            dict(properties=['window', 'path', 'thumbnail', 'windowparameter'])
        },
        "player": {
            "method": "Player.GetActivePlayers"
        }
    }

    def __init__(self, sh, *args, **kwargs):
        '''
        Initalizes the plugin.
        '''
        # init logger
        self.logger = logging.getLogger(__name__)
        self.logger.info('Init Kodi Plugin')
        self.host = self.get_parameter_value('host')
        self.port = self.get_parameter_value('port')
        self.autoreconnect = self.get_parameter_value('autoreconnect')
        self.connect_retries = self.get_parameter_value('connect_retries')
        self.connect_cycle = self.get_parameter_value('connect_cycle')
        self.send_retries = self.get_parameter_value('send_retries')
        self.kodi_tcp_connection = Tcp_client(
            host=self.host,
            port=self.port,
            name='KodiTCPConnection',
            autoreconnect=self.autoreconnect,
            connect_retries=self.connect_retries,
            connect_cycle=self.connect_cycle)
        self.kodi_tcp_connection.set_callbacks(
            connected=self.on_connect,
            data_received=self.received_data,
            disconnected=self.on_disconnect)
        self.kodi_server_alive = False
        #         self.terminator = 0
        #         self.balance(b'{', b'}')
        self.message_id = 1
        self.response_id = None
        self.sendingcommand = None
        self.senderrors = {}
        self.cmd_lock = threading.Lock()
        self.reply_lock = threading.Condition()
        self.reply = None
        self.activeplayers = []
        self.sendcommands = []
        self.registered_items = {
            key: []
            for key in set(
                list(Kodi._set_items.keys()) + ['macro'] + Kodi._get_items +
                list(Kodi._player_items.keys()))
        }

    def run(self):
        '''
        Run method for the plugin
        '''
        self.logger.debug('Plugin \'{}\': run method called'.format(
            self.get_shortname()))
        self.connect_to_kodi('run')
        self.alive = True

    def stop(self):
        '''
        Stop method for the plugin
        '''
        self.logger.debug('Plugin \'{}\': stop method called'.format(
            self.get_shortname()))
        self.kodi_tcp_connection.close()
        self.kodi_server_alive = False
        self.alive = False

    def on_connect(self, by=None):
        '''
        This method is called on a succesful connect to Kodi
        On a connect first check if the JSON-RPC API is available.
        If this is the case initialize all items with values extracted from Kodi
        '''
        # check if API is available
        self.kodi_server_alive = True
        if isinstance(by, (dict, Tcp_client)):
            by = 'TCP_Connect'
        self.logger.debug(
            "Kodi running onconnect started by {}. Connection: {}. Selfcommands {}"
            .format(by, self.kodi_server_alive, self.sendcommands))
        if len(self.sendcommands) == 0:
            for command in self._initcommands:
                self.logger.debug("Sending command after connect: {}".format(
                    self._initcommands.get(command)))
                self.send_kodi_rpc(
                    method=self._initcommands.get(command).get('method'),
                    params=self._initcommands.get(command).get('params'),
                    wait=False)

    def on_disconnect(self, obj=None):
        ''' function called when TCP connection to Kodi is disconnected '''
        self.logger.debug('Received disconnect from Kodi')
        self.kodi_server_alive = False
        for elem in self.registered_items['on_off']:
            elem(self.kodi_server_alive, caller='Kodi')

    def connect_to_kodi(self, by):
        '''
        try to establish a new connection to Kodi

        While this method is called during the start-up phase of the plugin,
        it can also be used to establish a connection to the Kodi server if the
        plugin was initialized before the server went up.
        '''
        self.logger.debug("Kodi connection initialized by {}".format(by))
        if not self.kodi_tcp_connection.connected():
            self.kodi_tcp_connection.connect()
            # we allow for 2 seconds to connect
            time.sleep(2)
        if not self.kodi_tcp_connection.connected():
            # no connection could be established, Kodi may be offline
            self.logger.info('Could not establish a connection to Kodi Server')
            self.kodi_server_alive = False
        else:
            self.kodi_server_alive = True
            #self.on_connect(by)
        for elem in self.registered_items['on_off']:
            elem(self.kodi_server_alive, caller='Kodi')

    def parse_item(self, item):
        '''
        Method for parsing Kodi items.
        If the item carries the kodi_item field, this item is registered to the plugin.
        :param item:    The item to process.
        :return:        The item update method to be triggered if the kodi_item is in the set item dict.
        '''
        if self.has_iattr(item.conf, 'kodi_item'):
            kodi_item = self.get_iattr_value(item.conf, 'kodi_item')
            self.logger.debug('Registering item: {}'.format(item))
            if kodi_item in self.registered_items:
                self.registered_items[kodi_item].append(item)
            else:
                self.logger.warning(
                    'I do not know the kodi_item {}, skipping!'.format(
                        kodi_item))
            if kodi_item in Kodi._set_items or kodi_item == 'macro' or kodi_item in Kodi._player_items:
                return self.update_item

    def parse_logic(self, logic):
        '''
        Default plugin parse_logic method
        '''
        pass

    def update_item(self, item, caller=None, source=None, dest=None):
        '''
        Callback method for sending values to Kodi when a registered item has changed

        :param item: item to be updated towards the plugin
        :param caller: if given it represents the callers name
        :param source: if given it represents the source
        :param dest: if given it represents the dest
        '''
        item_value = item()
        if item_value is not None and caller != 'Kodi' and self.has_iattr(
                item.conf, 'kodi_item'):
            # update item was triggered from something else then this plugin -> send to Kodi
            kodi_item = self.get_iattr_value(item.conf, 'kodi_item')
            self.logger.debug("Updating item {} using kodi command {}".format(
                item, kodi_item))

            if kodi_item == 'on_off' and item():
                # handle the on_off item as special case:
                # if item is on, try to establish a connection to Kodi
                self.connect_to_kodi('update')
                # if item is off send shutdown command to Kodi. This is
                # handled in the standard block below though
            elif kodi_item == 'macro' and item() in self._macro:
                macro = item()
                for command in self._macro.get(macro):
                    if command == "wait":
                        waittime = int(self._macro.get(macro).get(command))
                        self.logger.debug(
                            "Macro waiting for {} second(s)".format(waittime))
                        time.sleep(waittime)
                    else:
                        method = self._macro.get(macro).get(command).get(
                            'method')
                        params = self._macro.get(macro).get(command).get(
                            'params')
                        self.logger.debug(
                            "Command - Method: {}, Params: {}".format(
                                method, params))
                        self.send_kodi_rpc(method=method,
                                           params=params,
                                           wait=False)
            elif kodi_item in Kodi._set_items:
                if kodi_item == 'player':
                    for elem in self.registered_items['player']:
                        elem(0, caller='Kodi')
                if kodi_item == 'input' and item(
                ) not in self._possible_input_actions + self._possible_player_actions:
                    self.logger.error(
                        "The action {} for the kodi_item 'input' is not allowed, skipping"
                        .format(item_value))
                else:
                    self.logger.debug(
                        "update_item was called with item {} from caller {}, source {} and dest {}"
                        .format(item, caller, source, dest))
                    method = self._set_items[kodi_item]['method']
                    params = self._set_items[kodi_item]['params']
                    if params is not None:
                        # copy so we don't interfer with the class variable
                        params = params.copy()
                        # replace the wild card ITEM_VALUE with the item's value
                        for key, value in params.items():
                            if value == 'ITEM_VALUE':
                                params[key] = item_value
                    if item() in self._possible_input_actions:
                        self.send_kodi_rpc(method=method,
                                           params=params,
                                           wait=False)
                    elif item() in self._possible_player_actions:
                        self._send_player_command(method, params, kodi_item)
            elif kodi_item in Kodi._player_items:
                self.logger.debug(
                    'Plugin \'%s\': update_item was called with item \'%s\' from caller \'%s\', source \'%s\' and dest \'%s\'',
                    self.get_shortname(), item, caller, source, dest)
                method = self._player_items[kodi_item]['method']
                params = self._player_items[kodi_item]['params'] or {}
                if params is not None:
                    # copy so we don't interfer with the class variable
                    params = params.copy()
                    # replace the wild card ITEM_VALUE with the item's value
                    for key, value in params.items():
                        if value == 'ITEM_VALUE':
                            params[key] = item_value
                    self._send_player_command(method, params, kodi_item)
            else:
                self.logger.info(
                    'kodi_item \'%s\' not in send_keys, skipping!', kodi_item)

    def notify(self, title, message, image=None, display_time=10000):
        '''
        Send a notification to Kodi to be displayed on the screen

        :param title: the title of the message
        :param message: the message itself
        :param image: an optional image to be displayed alongside the message
        :param display_time: how long the message is displayed in milli seconds
        '''
        params = dict(title=title, message=message, displaytime=display_time)
        if image is not None:
            params['image'] = image
        self.send_kodi_rpc(method='GUI.ShowNotification', params=params)

    def send_kodi_rpc(self, method, params=None, message_id=None, wait=True):
        '''
        Send a JSON RPC to Kodi.

        The  JSON string is extracted from the supplied method and the given parameters.
        :param method: the Kodi method to be triggered
        :param params: parameters dictionary
        :param message_id: the message ID to be used. If none, use the internal counter
        :param wait: whether to wait for the reply from Kodi or send off the RPC asynchronously
                     If wait is True, this method returns a dictionary parsed from the JSON
                     response from Kodi
        '''
        reply = None
        self.logger.debug("Sending method {}. Alive: {}".format(
            method, self.kodi_server_alive))
        if self.kodi_server_alive:
            self.cmd_lock.acquire()
            self.logger.debug("Command lock acquired")
            self.reply = None
            '''
            if message_id is None:
                self.message_id += 1
                message_id = self.message_id
                if message_id > 99:
                    self.message_id = 0
                message_id = "{}_{}".format(method, message_id)
            '''
            message_id = method
            self.logger.debug('Sendcommands while sending: {0}'.format(
                self.sendcommands))
            self.response_id = message_id
            if params is not None:
                data = {
                    'jsonrpc': '2.0',
                    'id': message_id,
                    'method': method,
                    'params': params
                }
            else:
                data = {'jsonrpc': '2.0', 'id': message_id, 'method': method}
            if not data in self.sendcommands:
                self.sendcommands.append(data)
            else:
                self.sendcommands[self.sendcommands.index(data)] = data
            self.logger.debug('Sendcommands while sending: {0}'.format(
                self.sendcommands))
            self.reply_lock.acquire()
            try:
                self.sendingcommand = json.dumps(data, separators=(',', ':'))
            except Exception as err:
                self.sendingcommand = data
                self.logger.error("Problem with json.dumps: {}".format(err))
            self.logger.debug('Kodi sending: {0}'.format(self.sendingcommand))
            self.kodi_tcp_connection.send(
                (self.sendingcommand + '\r\n').encode())
            if wait:
                self.logger.debug("Waiting for reply_lock..")
                self.reply_lock.wait(1)
            self.reply_lock.release()
            reply = self.reply
            self.reply = None
            self.cmd_lock.release()
            self.logger.debug("Command lock released")
        else:
            self.logger.debug(
                'JSON-RPC command requested without an established connection to Kodi.'
            )
        return reply

    def received_data(self, connection, data):
        '''
        This method is called whenever data is received from the connection to
        Kodi.
        '''
        self.logger.debug('Kodi receiving: {0}'.format(data))
        try:
            events = (re.sub(r'\}\{', '}-#-{', data)).split("-#-")
            events = list(OrderedDict((x, True) for x in events).keys())
        except Exception as err:
            self.logger.warning(
                "Could not optimize reply. Error: {}".format(err))
        for event in events:
            try:
                event = json.loads(event)
            except Exception as err:
                self.logger.warning(
                    "Could not json.load reply. Error: {}".format(err))
            if len(events) > 1:
                self.logger.debug(
                    'Kodi checking from multianswer: {0}'.format(event))
            if 'id' in event:
                self.reply_lock.acquire()
                templist = []
                templist = self.sendcommands
                query_playerinfo = []
                for entry in templist:
                    if entry.get('id') == event.get('id'):
                        if self.senderrors.get(event.get('id')):
                            self.senderrors[event.get('id')] = 0
                        if 'error' in event:
                            self.logger.warning(
                                "There was a problem with the {} command: {}. Removing from queue."
                                .format(event.get('id'),
                                        event.get('error').get('message')))
                        elif event.get('id').startswith(
                                'Player.GetActivePlayers'):
                            if len(event.get('result')) > 1:
                                self.logger.info(
                                    'There is more than one active player. Sending request to each player!'
                                )
                                self.activeplayers = []
                                query_playerinfo = True
                                for player in event.get('result'):
                                    self.activeplayers.append(
                                        player.get('playerid'))
                                    query_playerinfo.append(
                                        player.get('playerid'))
                            elif len(event.get('result')) > 0:
                                self.activeplayers = [
                                    event.get('result')[0].get('playerid')
                                ]
                                query_playerinfo = [
                                    event.get('result')[0].get('playerid')
                                ]
                                for elem in self.registered_items['player']:
                                    elem(
                                        event.get('result')[0].get('playerid'),
                                        caller='Kodi')
                            else:
                                self.activeplayers = []
                                query_playerinfo = []
                                for elem in self.registered_items['state']:
                                    elem('No Active Player', caller='Kodi')
                        elif event.get('result') and event.get(
                                'id').startswith('Application.GetProperties'):
                            muted = event['result'].get('muted')
                            volume = event['result'].get('volume')
                            self.logger.debug(
                                "Received GetProperties: Change mute to {} and volume to {}"
                                .format(muted, volume))
                            for elem in self.registered_items['mute']:
                                elem(muted, caller='Kodi')
                            for elem in self.registered_items['volume']:
                                elem(volume, caller='Kodi')
                        elif event.get('result') and event.get(
                                'id').startswith('Favourites.GetFavourites'):
                            item_dict = dict()
                            if event.get('result').get('favourites') is None:
                                self.logger.debug("No favourites found.")
                            else:
                                item_dict = {
                                    elem['title']: elem
                                    for elem in event.get('result').get(
                                        'favourites')
                                }
                                self.logger.debug(
                                    "Favourites found: {}".format(item_dict))
                                for elem in self.registered_items[
                                        'favourites']:
                                    elem(item_dict, caller='Kodi')
                        elif event.get('result') and event.get(
                                'id').startswith('Player.GetItem'):
                            title = event.get('result')['item'].get('title')
                            typ = event.get('result')['item'].get('type')
                            if not title and 'label' in event.get(
                                    'result')['item']:
                                title = event.get('result')['item']['label']
                            for elem in self.registered_items['media']:
                                elem(typ.capitalize(), caller='Kodi')
                            if event.get('result')['item'].get('artist'):
                                artist = 'unknown' if len(
                                    event.get('result')['item'].get(
                                        'artist')) == 0 else event.get(
                                            'result')['item'].get('artist')[0]
                                title = artist + ' - ' + title
                            for elem in self.registered_items['title']:
                                elem(title, caller='Kodi')
                            self.logger.debug(
                                "Updated player info: title={}, type={}".
                                format(title, typ))
                        else:
                            self.logger.debug(
                                "Sent successfully {}.".format(entry))
                        try:
                            self.sendcommands.remove(entry)
                        except Exception as err:
                            self.logger.error(
                                "Could not remove sent command from queue. Error: {}"
                                .format(err))
                        self.reply_lock.notify()
                        self.reply_lock.release()
                for player in query_playerinfo:
                    self.logger.debug("Getting player info for {}".format(
                        event.get('result')))
                    self._get_player_info(player)
                self.logger.debug('Sendcommands after receiving: {0}'.format(
                    self.sendcommands))
            elif 'favourites' in event:
                item_dict = dict()
                item_dict = {
                    elem['title']: elem
                    for elem in result['favourites']
                }
                self.logger.debug("Favourites queried: {}".format(item_dict))
                for elem in self.registered_items['favourites']:
                    elem(item_dict, caller='Kodi')
            elif 'method' in event:
                if event['method'] == 'Player.OnPause':
                    self.logger.debug("Paused Player")
                    for elem in self.registered_items['state']:
                        elem('Pause', caller='Kodi')
                elif event['method'] == 'Player.OnStop':
                    self.logger.debug("Stopped Player")
                    for elem in self.registered_items['state']:
                        elem('Stopped Player', caller='Kodi')
                    for elem in self.registered_items['media']:
                        elem('', caller='Kodi')
                    for elem in self.registered_items['title']:
                        elem('', caller='Kodi')
                elif event['method'] == 'GUI.OnScreensaverActivated':
                    self.logger.debug("Activate Screensaver")
                    for elem in self.registered_items['state']:
                        elem('Screensaver', caller='Kodi')
                if event['method'] in ['Player.OnPlay', 'Player.OnAVChange']:
                    # use a different thread for event handling
                    self.logger.debug(
                        "Getting player info after player started")
                    data = {
                        'jsonrpc': '2.0',
                        'id': 'Player.GetActivePlayers',
                        'method': 'Player.GetActivePlayers'
                    }
                    if not data in self.sendcommands:
                        self.sendcommands.append(data)
                    else:
                        self.sendcommands[self.sendcommands.index(data)] = data
                    #self.scheduler_trigger('kodi-player-start', self.send_kodi_rpc, 'Kodi', 'OnPlay', {"method": "Player.GetActivePlayers"})
                elif event['method'] in ['Application.OnVolumeChanged']:
                    self.logger.debug(
                        "Change mute to {} and volume to {}".format(
                            event['params']['data']['muted'],
                            event['params']['data']['volume']))
                    for elem in self.registered_items['mute']:
                        elem(event['params']['data']['muted'], caller='Kodi')
                    for elem in self.registered_items['volume']:
                        elem(event['params']['data']['volume'], caller='Kodi')
        if len(self.sendcommands) > 0:
            id = self.sendcommands[0].get('id')
            if self.senderrors.get(id):
                self.senderrors[id] += 1
            else:
                self.senderrors[id] = 1
            if self.senderrors.get(id) <= self.send_retries:
                self.logger.debug("Sending again: {}. Retry {}/{}".format(
                    self.sendcommands[0], self.senderrors.get(id),
                    self.send_retries))
                self.send_kodi_rpc(self.sendcommands[0].get('method'),
                                   params=self.sendcommands[0].get('params'),
                                   message_id=self.sendcommands[0].get('id'))
            else:
                try:
                    self.senderrors.pop(id)
                except Exception:
                    pass
                self.logger.debug(
                    "Gave up resending {} because maximum retries {} reached. Error list: {}"
                    .format(self.sendcommands[0], self.send_retries,
                            self.senderrors))
                self.sendcommands.remove(self.sendcommands[0])
                if len(self.sendcommands) > 0:
                    self.logger.debug("Sending next command: {}".format(
                        self.sendcommands[0]))
                    self.send_kodi_rpc(
                        self.sendcommands[0].get('method'),
                        params=self.sendcommands[0].get('params'),
                        message_id=self.sendcommands[0].get('id'))

    def _send_player_command(self, method, params, kodi_item):
        '''
        This method should only be called from the update item method in
        a new thread in order to handle Play/Pause and Stop commands to
        the active Kodi players
        '''
        self.send_kodi_rpc(method='Player.GetActivePlayers',
                           params=None,
                           message_id='Player.GetActivePlayers')
        self.logger.debug("Active players: {}".format(self.activeplayers))
        if len(self.activeplayers) == 0:
            self.logger.warning(
                'Kodi: no active player found, skipping request!')
        else:
            params = params or {}
            if len(self.activeplayers) > 1:
                self.logger.info(
                    'Kodi: there is more than one active player. Sending request to each player!'
                )
            for player in self.activeplayers:
                if len(self.activeplayers) > 1:
                    params.update({'playerid': player})
                self.send_kodi_rpc(method=method, params=params, wait=False)

    def _get_player_info(self, result=None):
        '''
        Extract information from Kodi regarding the active player and save it
        to the respective items
        '''
        self.logger.debug("Getting player info. Checking {}".format(result))
        if not isinstance(result, list):
            return
        if len(result) == 0:
            self.logger.info('No active player found.')
            for elem in self.registered_items['title']:
                elem('', caller='Kodi')
            for elem in self.registered_items['media']:
                elem('', caller='Kodi')
            for elem in self.registered_items['state']:
                elem('No Active Player', caller='Kodi')
            return
        playerid = result[0].get('playerid')
        typ = result[0].get('type')
        for elem in self.registered_items['state']:
            elem('Playing', caller='Kodi')
        self.logger.debug(
            "Now checking player item for player with id {}".format(playerid))
        if typ == 'video':
            self.send_kodi_rpc(method='Player.GetItem',
                               params=dict(properties=['title'],
                                           playerid=playerid),
                               message_id='Player.GetItem_Video')
        elif typ == 'audio':
            for elem in self.registered_items['media']:
                elem('Audio', caller='Kodi')
            self.send_kodi_rpc(method='Player.GetItem',
                               params=dict(properties=['title', 'artist'],
                                           playerid=playerid),
                               message_id='Player.GetItem_Audio')

        elif typ == 'picture':
            for elem in self.registered_items['media']:
                elem('Picture', caller='Kodi')
            title = ''
            for elem in self.registered_items['title']:
                elem(title, caller='Kodi')
        else:
            self.logger.warning('Unknown type: {0}'.format(typ))
            return
コード例 #3
0
class KNX(SmartPlugin):

    PLUGIN_VERSION = "1.7.9"

    # tags actually used by the plugin are shown here
    # can be used later for backend item editing purposes, to check valid item attributes
    ITEM_TAG = [KNX_DPT, KNX_STATUS, KNX_SEND, KNX_REPLY, KNX_LISTEN, KNX_INIT, KNX_CACHE, KNX_POLL]
    ITEM_TAG_PLUS = [KNX_DTP]

    # provider for KNX service
    PROVIDER_KNXD =  'knxd'
    PROVIDER_KNXIP = 'IP Interface'
    PROVIDER_KNXMC = 'IP Router'

    def __init__(self, smarthome):
        self.provider = self.get_parameter_value('provider')
        self.host = self.get_parameter_value('host')
        self.port = self.get_parameter_value('port')

        from bin.smarthome import VERSION
        if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5':
            self.logger = logging.getLogger(__name__)

        name = 'plugins.' + self.get_fullname()
        self._client = Tcp_client(name=name, host=self.host, port=self.port, binary=True, autoreconnect=True, connect_cycle=5, retry_cycle=30)
        self._client.set_callbacks(connected=self.handle_connect, data_received=self.parse_knxd_message)  # , disconnected=disconnected_callback, data_received=receive_callback

        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug("init knx")
        self.shtime = Shtime.get_instance()

        self.gal = {}                   # group addresses to listen to {DPT: dpt, ITEMS: [item 1, item 2, ..., item n], LOGICS: [ logic 1, logic 2, ..., logic n]}
        self.gar = {}                   # group addresses to reply if requested from knx, {DPT: dpt, ITEM: item, LOGIC: None}
        self._init_ga = []
        self._cache_ga = []             # group addresses which should be initalized by the knxd cache
        self._cache_ga_response_pending = []
        self.time_ga = self.get_parameter_value('time_ga')
        self.date_ga = self.get_parameter_value('date_ga')
        self._send_time_do = self.get_parameter_value('send_time')
        self._bm_separatefile = False
        self._bm_format = "BM': {1} set {2} to {3}"

        # following needed for statistics
        self.enable_stats = self.get_parameter_value('enable_stats')
        self.stats_ga = {}              # statistics for used group addresses on the BUS
        self.stats_pa = {}              # statistics for used group addresses on the BUS
        self.stats_last_read = None     # last read request from KNX
        self.stats_last_write = None    # last write from KNX
        self.stats_last_response = None # last response from KNX
        self.stats_last_action = None   # the most recent action
        self._log_own_packets = self.get_parameter_value('log_own_packets')
        # following is for a special logger called busmonitor
        busmonitor = self.get_parameter_value('busmonitor')

        if busmonitor.lower() in ['on','true']:
            self._busmonitor = self.logger.info
        elif busmonitor.lower() in ['off', 'false']:
            self._busmonitor = self.logger.debug
        elif busmonitor.lower() == 'logger':
            self._bm_separatefile = True
            self._bm_format = "{0};{1};{2};{3}"
            self._busmonitor = logging.getLogger("knx_busmonitor").info
            self.logger.info(self.translate("Using busmonitor (L) = '{}'").format(busmonitor))
        else:
            self.logger.warning(self.translate("Invalid value '{}' configured for parameter 'busmonitor', using 'false'").format(busmonitor))
            self._busmonitor = self.logger.debug

        self.readonly = self.get_parameter_value('readonly')
        if self.readonly:
            self.logger.warning(self.translate("!!! KNX Plugin in READONLY mode !!!"))

        # extension to use knx project files from ETS5
        self.project_file_password = self.get_parameter_value( 'project_file_password')
        self.knxproj_ga = {}
        self.use_project_file = self.get_parameter_value('use_project_file')

        if self.use_project_file:
            # don't bother people without project files
            self.base_project_filename = "projectfile"
            self.projectpath = pathlib.Path(self.get_parameter_value('projectpath'))
            if self.projectpath.is_absolute():
                self.projectpath = self.projectpath / self.base_project_filename
                self.logger.warning(self.translate("Given path is absolute, using {}").format(self.projectpath))
            else:
                self.projectpath = pathlib.Path(self.get_sh().get_basedir()) / self.projectpath / self.base_project_filename
                self.logger.info(self.translate("Given path is relative, using {}").format(self.projectpath))

            self._parse_projectfile()

        self.init_webinterface(WebInterface)
        return

    def _parse_projectfile(self):
        self._check_projectfile_destination()
        if self.projectpath.is_file():
            self.knxproj_ga = knxproj.parse_projectfile(self.projectpath, self.project_file_password)

    def _check_projectfile_destination(self):
        if not self.projectpath.exists():
            self.logger.warning(self.translate("File at given path {} does not exist").format(self.projectpath))
            if not self.projectpath.parent.exists():
                self.logger.warning(self.translate("try to create directory {}").format(self.projectpath.parent))
                try:
                    self.projectpath.parent.mkdir(parents=True, exist_ok=True)
                    self.logger.warning(self.translate("directory {} was created").format(self.projectpath.parent))
                except:
                    self.logger.warning(self.translate("could not create directory {}").format(self.projectpath.parent))

    def _send(self, data):
        if len(data) < 2 or len(data) > 0xffff:
            if self.logger.isEnabledFor(logging.DEBUG):
                self.logger.debug(self.translate('Illegal data size: {}').format(repr(data)))
            return False
        # prepend data length for knxd
        send = bytearray(len(data).to_bytes(2, byteorder='big'))
        send.extend(data)
        self._client.send(send)

    def groupwrite(self, ga, payload, dpt, flag='write'):
        pkt = bytearray([0, KNXD.GROUP_PACKET])
        try:
            pkt.extend(self.encode(ga, 'ga'))
        except:
            self.logger.warning(self.translate('problem encoding ga: {}').format(ga))
            return
        pkt.extend([0])
        try:
            pkt.extend(self.encode(payload, dpt))
        except:
            self.logger.warning(self.translate('problem encoding payload {} for dpt {}').format(payload,dpt))
            return
        if flag == 'write':
            flag = KNXWRITE
        elif flag == 'response':
            flag = KNXRESP
        else:
            self.logger.warning(self.translate(
                "groupwrite telegram for {} with unknown flag: {}. Please choose beetween write and response.").format(
                    ga, flag))
            return
        pkt[5] = flag | pkt[5]
        if self.readonly:
            self.logger.info(self.translate("groupwrite telegram for: {} - Value: {} not sent. Plugin in READONLY mode.").format(ga, payload))
        else:
            if self.logger.isEnabledFor(logging.DEBUG):
                self.logger.debug(self.translate("groupwrite telegram for: {} - Value: {} sent.").format(ga, payload))
            self._send(pkt)

    def _cacheread(self, ga):
        pkt = bytearray([0, KNXD.CACHE_READ])
        try:
            pkt.extend(self.encode(ga, 'ga'))
        except:
            self.logger.warning(self.translate('problem encoding ga: {}').format(ga))
            return
        pkt.extend([0, 0])
        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug(self.translate('reading knxd cache for ga: {}').format(ga))
        self._send(pkt)

    def groupread(self, ga):
        pkt = bytearray([0, KNXD.GROUP_PACKET])
        try:
            pkt.extend(self.encode(ga, 'ga'))
        except:
            self.logger.warning(self.translate('problem encoding ga: {}').format(ga))
            return
        pkt.extend([0, KNXREAD])
        self._send(pkt)

    def _poll(self, **kwargs):
        if ITEM in kwargs:
            item = kwargs[ITEM]
        else:
            item = 'unknown item'

        if 'ga' in kwargs:
            self.groupread(kwargs['ga'])
        else:
            self.logger.warning(self.translate('problem polling {}, no known ga').format(item))

        if 'interval' in kwargs and 'ga' in kwargs:
            try:
                ga = kwargs['ga']
                interval = int(kwargs['interval'])
                next = self.shtime.now() + timedelta(seconds=interval)
                self._sh.scheduler.add(f'KNX poll {item}', self._poll,
                                    value={'instance': self.get_instance_name(), ITEM: item, 'ga': ga, 'interval': interval},
                                    next=next)
            except Exception as ex:
                self.logger.error(f"_poll function got an error {ex}")

    def _send_time(self):
        self.send_time(self.time_ga, self.date_ga)

    def send_time(self, time_ga=None, date_ga=None):
        now = self.shtime.now()
        if time_ga:
            self.groupwrite(time_ga, now, '10')
        if date_ga:
            self.groupwrite(date_ga, now.date(), '11')

    def handle_connect(self, client):
        """
        Callback function to set up internals after a connection to knxd was established

        :param client: the calling client for adaption purposes
        :type client: TCP_client
        """
        
        # let the knxd use its group address cache
        enable_cache = bytearray([0, KNXD.CACHE_ENABLE])
        self._send(enable_cache)
        
        # set next kind of data to expect from connection 
        self._isLength = True
        
        # if this is the first connect after init of plugin then read the
        # group addresses from knxd which have the knx_cache attribute
        if self._cache_ga != []:
            if self.logger.isEnabledFor(logging.DEBUG):
                self.logger.debug(self.translate('reading knxd cache'))
            for ga in self._cache_ga:
                self._cache_ga_response_pending.append(ga)
            for ga in self._cache_ga:
                self._cacheread(ga)
                # wait a little to not overdrive the knxd unless there is a fix
                time.sleep(KNXD_CACHEREAD_DELAY)
            self._cache_ga = []
            if self.logger.isEnabledFor(logging.DEBUG):
                self.logger.debug(self.translate('finished reading knxd cache'))

        # let knxd create a new group monitor and send the read requests
        # for all group addresses which have the knx_read  attribute 
        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug(self.translate('enable group monitor'))

        init = bytearray([0, KNXD.OPEN_GROUPCON, 0, 0, 0])
        self._send(init)
        client.terminator = 2
        if self._init_ga != []:
            if client.connected:
                if self.logger.isEnabledFor(logging.DEBUG):
                    self.logger.debug(self.translate('knxd init read for {} ga').format(len(self._init_ga)))
                for ga in self._init_ga:
                    self.groupread(ga)
                self._init_ga = []
                if self.logger.isEnabledFor(logging.DEBUG):
                    self.logger.debug(self.translate('finished knxd init read'))

    def encode(self, data, dpt):
        return dpts.encode[str(dpt)](data)

    def decode(self, data, dpt):
        return dpts.decode[str(dpt)](data)

    def parse_knxd_message(self, client, data):
        """
        inspects a message from knxd (eibd)

        :param client: Tcp_client
        :param data: message from knxd as bytearray
        
        a message from knxd will have 4 extra bytes plus eventually the knx telegram payload
        2 byte length
        2 byte knxd message type   --> see eibtypes.h
        
        knx telegram then consists of
            1 byte control byte
            2 byte source as physical address
            2 byte destination as group address
            2 byte command/data
            n byte data
        thus at least 7 bytes

        The process consists of two steps:
        * At first the variable ``self._isLength`` is True and the length for the 
          following knxd message plus eventual knx telegram is set to ``client.terminator``
        * then the next call to parse_knxd_message is awaited to contain 
          the knxd message type in the first two bytes and then following eventually a knx telegram
        """
        if self._isLength:
            self._isLength = False
            try:
                client.terminator = struct.unpack(">H", data)[0]
            except:
                self.logger.error("KNX[{0}]: problem unpacking length: {1}".format(self.get_instance_name(), data))
            return
        else:
            self._isLength = True
            client.terminator = 2

        knxd_msg_type = struct.unpack(">H", data[0:2])[0]
        if (knxd_msg_type != KNXD.GROUP_PACKET and knxd_msg_type != KNXD.CACHE_READ) or len(data) < 8:
            self.handle_other_knxd_messages(knxd_msg_type, data[2:])
            return

        if (data[6] & 0x03 or (data[7] & 0xC0) == 0xC0):
            if self.logger.isEnabledFor(logging.DEBUG):
                self.logger.debug("Unknown APDU")
            return
        src = self.decode(data[2:4], 'pa')
        dst = self.decode(data[4:6], 'ga')
        flg = data[7] & 0xC0
        if flg == KNXWRITE:
            flg = 'write'
        elif flg == KNXREAD:
            flg = 'read'
        elif flg == KNXRESP:
            flg = 'response'
        else:
            self.logger.warning("Unknown flag: {:02x} src: {} dest: {}".format(flg, src, dst))
            return
        if len(data) == 8:
            payload = bytearray([data[7] & 0x3f])
        else:
            payload = data[8:]

        if self.enable_stats:
            # update statistics on used group addresses
            if dst not in self.stats_ga:
                self.stats_ga[dst] = {}

            if flg not in self.stats_ga[dst]:
                self.stats_ga[dst][flg] = 1
            else:
                self.stats_ga[dst][flg] = self.stats_ga[dst][flg] + 1
            self.stats_ga[dst]['last_' + flg] = self.shtime.now()

            # update statistics on used physical addresses
            if src not in self.stats_pa:
                self.stats_pa[src] = {}

            if flg not in self.stats_pa[src]:
                self.stats_pa[src][flg] = 1
            else:
                self.stats_pa[src][flg] = self.stats_pa[src][flg] + 1
            self.stats_pa[src]['last_' + flg] = self.shtime.now()

        # further inspect what to do next
        if flg == 'write' or flg == 'response':
            if dst not in self.gal:  # update item/logic
                self._busmonitor(self._bm_format.format(self.get_instance_name(), src, dst, binascii.hexlify(payload).decode()))
                return
            dpt = self.gal[dst][DPT]
            try:
                val = self.decode(payload, dpt)
            except Exception as e:
                self.logger.exception("Problem decoding frame from {} to {} with '{}' and DPT {}. Exception: {}".format(src, dst, binascii.hexlify(payload).decode(), dpt, e))
                return
            if val is not None:
                self._busmonitor(self._bm_format.format(self.get_instance_name(), src, dst, val))
                # print "in:  {0}".format(self.decode(payload, 'hex'))
                # out = ''
                # for i in self.encode(val, dpt):
                #    out += " {0:x}".format(i)
                # print "out:{0}".format(out)

                # remove all ga that came from a cache read request
                if knxd_msg_type == KNXD.CACHE_READ:
                    if dst in self._cache_ga_response_pending:
                        self._cache_ga_response_pending.remove(dst)
                way = "" if knxd_msg_type != KNXD.CACHE_READ else " (from knxd Cache)"
                if self.logger.isEnabledFor(logging.DEBUG):
                    self.logger.debug("{} request from {} to {} with '{}' and DPT {}{}".format(flg, src, dst, binascii.hexlify(payload).decode(), dpt, way))
                src_wrk = self.get_instance_name()
                if src_wrk != '':
                    src_wrk += ':'
                src_wrk += src + ':ga=' + dst
                for item in self.gal[dst][ITEMS]:
                    if self.logger.isEnabledFor(logging.DEBUG):
                        self.logger.debug("Set Item '{}' to value '{}' caller='{}', source='{}', dest='{}'".format(item, val, self.get_shortname(), src, dst))
                    item(val, self.get_shortname(), src_wrk, dst)
                for logic in self.gal[dst][LOGICS]:
                    if self.logger.isEnabledFor(logging.DEBUG):
                        self.logger.debug("Trigger Logic '{}' from caller='{}', source='{}', value '{}', dest='{}'".format(logic, self.get_shortname(), src_wrk, val, dst))
                    logic.trigger(self.get_shortname(), src_wrk, val, dst)
            else:
                self.logger.warning("Wrong payload '{2}' for ga '{1}' with dpt '{0}'.".format(dpt, dst, binascii.hexlify(payload).decode()))
            if self.enable_stats:
                if flg == 'write':
                    self.stats_last_write = self.shtime.now()
                else:
                    self.stats_last_response = self.shtime.now()
        elif flg == 'read':
            if self.logger.isEnabledFor(logging.DEBUG):
                self.logger.debug("Device with physical address '{}' requests read for ga '{}'".format(src, dst))
            if self.enable_stats:
                self.stats_last_read = self.shtime.now()
            if dst in self.gar:  # read item
                if self.gar[dst][ITEM] is not None:
                    item = self.gar[dst][ITEM]
                    val = item()
                    if self.logger.isEnabledFor(logging.DEBUG):
                        self.logger.debug("groupwrite value '{}' to ga '{}' as DPT '{}' as response".format(dst, val, self.get_iattr_value(item.conf,KNX_DPT)))
                    if self._log_own_packets is True:
                        self._busmonitor(self._bm_format.format(self.get_instance_name(), src, dst, val))
                    self.groupwrite(dst, val, self.get_iattr_value(item.conf,KNX_DPT), 'response')
                if self.gar[dst][LOGIC] is not None:
                    src_wrk = self.get_instance_name()
                    if src_wrk != '':
                        src_wrk += ':'
                    src_wrk += src + ':ga=' + dst
                    if self.logger.isEnabledFor(logging.DEBUG):
                        self.logger.debug("Trigger Logic '{}' from caller='{}', source='{}', dest='{}'".format(self.gar[dst][LOGIC], self.get_shortname(), src_wrk, dst))
                    self.gar[dst][LOGIC].trigger(self.get_shortname(), src_wrk, None, dst)

    def handle_other_knxd_messages(self, knxd_msg_type, data):
        """to approach a two way communication we need to know more about the other messages"""
        if len(data) > 0:
            payloadstr = f" with data {binascii.hexlify(data).decode()}"
        else:
            payloadstr = " no further data"
        if knxd_msg_type in KNXD.MessageDescriptions:
            msg = f"KNXD message {KNXD.MessageDescriptions[knxd_msg_type]} received {payloadstr}"
        else:
            msg = f"KNXD message UNKNOWN received with data {payloadstr}"

        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug(msg)

    def run(self):
        """
        Run method for the plugin
        """
        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug("Plugin '{}': run method called".format(self.get_fullname()))
        self.alive = True
        self._client.connect()
        # moved from __init__() for proper restart behaviour
        if self._send_time_do:
            self._sh.scheduler.add('KNX[{0}] time'.format(self.get_instance_name()), self._send_time, prio=5, cycle=int(self._send_time_do))

    def stop(self):
        """
        Stop method for the plugin
        """
        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug("Plugin '{}': stop method called".format(self.get_fullname()))
        self.alive = False
        # added to effect better cleanup on stop
        if self.scheduler_get(f'KNX[{self.get_instance_name()}] time'):
            self.scheduler_remove(f'KNX[{self.get_instance_name()}] time')
        self._client.close()

    def parse_item(self, item):
        """
        Plugin parse_item method. Is called when the plugin is initialized.
        The plugin can, corresponding to its attribute keywords, decide what to do with
        the item in future, like adding it to an internal array for future reference
        :param item:    The item to process.
        :return:        If the plugin needs to be informed of an items change you should return a call back function
                        like the function update_item down below. An example when this is needed is the knx plugin
                        where parse_item returns the update_item function when the attribute knx_send is found.
                        This means that when the items value is about to be updated, the call back function is called
                        with the item, caller, source and dest as arguments and in case of the knx plugin the value
                        can be sent to the knx with a knx write function within the knx plugin.
        """
        if self.has_iattr(item.conf, KNX_DTP):
            self.logger.error("Ignoring {}: please change knx_dtp to knx_dpt.".format(item))
            return None
        if self.has_iattr(item.conf, KNX_DPT):
            dpt = self.get_iattr_value(item.conf, KNX_DPT)
            if dpt not in dpts.decode:
                self.logger.warning("Ignoring {} unknown dpt: {}".format(item, dpt))
                return None
        elif self.has_iattr(item.conf, KNX_STATUS) or self.has_iattr(item.conf, KNX_SEND) or self.has_iattr(item.conf, KNX_REPLY) or self.has_iattr(item.conf, KNX_LISTEN) or self.has_iattr(item.conf, KNX_INIT) or self.has_iattr(item.conf, KNX_CACHE):
            self.logger.warning(
                "Ignoring {}: please add knx_dpt.".format(item))
            return None
        else:
            return None
        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug("Item {} is mapped to KNX Instance {}".format(item, self.get_instance_name()))

        if self.has_iattr(item.conf, KNX_LISTEN):
            knx_listen = self.get_iattr_value(item.conf, KNX_LISTEN)
            if isinstance(knx_listen, str):
                knx_listen = [knx_listen, ]
            for ga in knx_listen:
                if self.logger.isEnabledFor(logging.DEBUG):
                    self.logger.debug("{} listen on {}".format(item, ga))
                if ga not in self.gal:
                    self.gal[ga] = {DPT: dpt, ITEMS: [item], LOGICS: []}
                else:
                    if item not in self.gal[ga][ITEMS]:
                        self.gal[ga][ITEMS].append(item)

        if self.has_iattr(item.conf, KNX_INIT):
            ga = self.get_iattr_value(item.conf, KNX_INIT)
            if self.logger.isEnabledFor(logging.DEBUG):
                self.logger.debug("{} listen on and init with {}".format(item, ga))
            if Utils.get_type(ga) == 'list':
                self.logger.warning("{} Problem while doing knx_init: Multiple GA specified in item definition, using first GA ({}) for reading value".format(item.id(), ga))
                ga = ga[0]
            if ga not in self.gal:
                self.gal[ga] = {DPT: dpt, ITEMS: [item], LOGICS: []}
            else:
                if item not in self.gal[ga][ITEMS]:
                    self.gal[ga][ITEMS].append(item)
            self._init_ga.append(ga)

        if self.has_iattr(item.conf, KNX_CACHE):
            ga = self.get_iattr_value(item.conf, KNX_CACHE)
            if self.logger.isEnabledFor(logging.DEBUG):
                self.logger.debug("{} listen on and init with cache {}".format(item, ga))
            if Utils.get_type(ga) == 'list':
                self.logger.warning("{} Problem while reading KNX cache: Multiple GA specified in item definition, using first GA ({}) for reading cache".format(item.id(), ga))
                ga = ga[0]
            if ga not in self.gal:
                self.gal[ga] = {DPT: dpt, ITEMS: [item], LOGICS: []}
            else:
                if item not in self.gal[ga][ITEMS]:
                    self.gal[ga][ITEMS].append(item)
            self._cache_ga.append(ga)

        if self.has_iattr(item.conf, KNX_REPLY):
            knx_reply = self.get_iattr_value(item.conf, KNX_REPLY)
            if isinstance(knx_reply, str):
                knx_reply = [knx_reply, ]
            for ga in knx_reply:
                if self.logger.isEnabledFor(logging.DEBUG):
                    self.logger.debug("{} reply to {}".format(item, ga))
                if ga not in self.gar:
                    self.gar[ga] = {DPT: dpt, ITEM: item, LOGIC: None}
                else:
                    self.logger.warning(
                        "{} knx_reply ({}) already defined for {}".format(item.id(), ga, self.gar[ga][ITEM]))

        if self.has_iattr(item.conf, KNX_SEND):
            if isinstance(self.get_iattr_value(item.conf, KNX_SEND), str):
                self.set_attr_value(item.conf, KNX_SEND, [self.get_iattr_value(item.conf, KNX_SEND), ])

        if self.has_iattr(item.conf, KNX_STATUS):
            if isinstance(self.get_iattr_value(item.conf, KNX_STATUS), str):
                self.set_attr_value(item.conf, KNX_STATUS, [self.get_iattr_value(item.conf, KNX_STATUS), ])

        if self.has_iattr(item.conf, KNX_POLL):
            knx_poll = self.get_iattr_value(item.conf, KNX_POLL)
            if isinstance(knx_poll, str):
                knx_poll = [knx_poll, ]
            if len(knx_poll) == 2:
                poll_ga = knx_poll[0]
                poll_interval = float(knx_poll[1]) 

                self.logger.info(
                    "Item {} is polled on GA {} every {} seconds".format(item, poll_ga, poll_interval))
                randomwait = random.randrange(15)
                next = self.shtime.now() + timedelta(seconds=poll_interval + randomwait)
                self._sh.scheduler.add(f'KNX poll {item}', self._poll,
                                       value={ITEM: item, 'ga': poll_ga, 'interval': poll_interval}, next=next)
            else:
                self.logger.warning(
                    "Ignoring knx_poll for item {}: We need two parameters, one for the GA and one for the polling interval.".format(
                        item))
                pass

        if self.has_iattr(item.conf, KNX_STATUS) or self.has_iattr(item.conf, KNX_SEND):
            return self.update_item

        return None

    def parse_logic(self, logic):
        """
        Plugin parse_logic method
        """
        if KNX_DPT in logic.conf:
            dpt = logic.conf[KNX_DPT]
            if dpt not in dpts.decode:
                self.logger.warning("Ignoring {} unknown dpt: {}".format(logic, dpt))
                return None
        else:
            return None

        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug("Logic {} is mapped to KNX Instance {}".format(logic, self.get_instance_name()))

        if KNX_LISTEN in logic.conf:
            knx_listen = logic.conf[KNX_LISTEN]
            if isinstance(knx_listen, str):
                knx_listen = [knx_listen, ]
            for ga in knx_listen:
                if self.logger.isEnabledFor(logging.DEBUG):
                    self.logger.debug("{} listen on {}".format(logic, ga))
                if ga not in self.gal:
                    self.gal[ga] = {DPT: dpt, ITEMS: [], LOGICS: [logic]}
                else:
                    self.gal[ga][LOGICS].append(logic)

        if KNX_REPLY in logic.conf:
            knx_reply = logic.conf[KNX_REPLY]
            if isinstance(knx_reply, str):
                knx_reply = [knx_reply, ]
            for ga in knx_reply:
                if self.logger.isEnabledFor(logging.DEBUG):
                    self.logger.debug("{} reply to {}".format(logic, ga))
                if ga in self.gar:
                    if self.gar[ga][LOGIC] is False:
                        obj = self.gar[ga][ITEM]
                    else:
                        obj = self.gar[ga][LOGIC]
                    self.logger.warning("{} knx_reply ({}) already defined for {}".format(logic, ga, obj))
                else:
                    self.gar[ga] = {DPT: dpt, ITEM: None, LOGIC: logic}

    def update_item(self, item, caller=None, source=None, dest=None):
        """
        Item has been updated

        This method is called, if the value of an item has been updated by SmartHomeNG.
        It should write the changed value out to the device (hardware/interface) that
        is managed by this plugin.

        :param item: item to be updated towards the plugin
        :param caller: if given it represents the callers name
        :param source: if given it represents the source
        :param dest: if given it represents the dest
        """
        if self.has_iattr(item.conf, KNX_SEND):
            if caller != self.get_shortname():
                for ga in self.get_iattr_value(item.conf, KNX_SEND):
                    _value = item()
                    if self._log_own_packets is True:
                        self._busmonitor(self._bm_format.format(self.get_instance_name(), 'SEND', ga, _value))
                    self.groupwrite(ga, _value, self.get_iattr_value(item.conf, KNX_DPT))
        if self.has_iattr(item.conf, KNX_STATUS):
            for ga in self.get_iattr_value(item.conf, KNX_STATUS):  # send status update
                if ga != dest:
                    _value = item()
                    if self._log_own_packets is True:
                        self._busmonitor(self._bm_format.format(self.get_instance_name(), 'STATUS', ga, _value))
                    self.groupwrite(ga, _value, self.get_iattr_value(item.conf, KNX_DPT))


# ------------------------------------------
#    Statistics functions for KNX
# ------------------------------------------

    """
    The statistics functions were introduced to watch what is happening on the KNX.
    Mainly it is recorded which physical device sends data by write or response or requests data
    by read operation.
    Whenever such a telegram is received, it is recorded
    - which physical device sended the request (originator)
    - which kind of request (read, write, response)
    - target group address affected
    - a counter for the specific kind of request (read, write, response) is increased.

    With an additional logic these statistics can be requested from the plugin and examined for
    - unknown group addresses which are not known to either ETS or to SmartHomeNG
    - unknown physical addresses which are new and unexpected
    - adresses that do not react upon requests
    - adresses that can't satisfy cache requests
    """
    def enable_stats(self):
        """
        Enables the tracking of KNX telegrams during runtime of SmartHomeNG
        """
        self.enable_stats = True

    def disable_stats(self):
        """
        Disables the tracking of KNX telegrams during runtime of SmartHomeNG
        It might be a good idea to clear your stats afterwards with clear_stats()
        """
        self.enable_stats = False

    def clear_stats(self):
        """
        clear all statistic values
        """
        self.clear_stats_ga()
        self.clear_stats_pa()

    def clear_stats_ga(self):
        """
        clear statistic values for group addresses
        """
        if len(self.stats_ga):
            for ga in self.stats_ga:
                self.stats_ga[ga] = {}

    def clear_stats_pa(self):
        """
        clear statistic values for physical addresses
        """
        if len(self.stats_pa):
            for ga in self.stats_pa:
                self.stats_pa[ga] = {}

    def get_stats_ga(self):
        """
        returns a dict with following structure
        ```
        stats_ga = { ga1 : { 'read' : n-read,               # counter of read requests from KNX
                             'write' : n-write,             # counter of write operations from KNX
                             'response' : n-response,       # counter of response operations from KNX
                             'last_read' : datetime,        # the respective datetime object of read,
                             'last_write' : datetime,       # write
                             'last_response' : datetime },  # and response
                     ga2 : {...} }
        ```
        :return: dict
        """
        return self.stats_ga

    def get_stats_pa(self):
        """
        returns a dict with following structure
        ```
        stats_pa = { pa1 : { 'read' : n-read,               # counter of read requests from KNX
                             'write' : n-write,             # counter of write operations from KNX
                             'response' : n-response,       # counter of response operations from KNX
                             'last_read' : datetime,        # the respective datetime object of read,
                             'last_write' : datetime,       # write
                             'last_response' : datetime },  # and response
                     pa2 : {...} }
        ```
        :return: dict
        """
        return self.stats_pa

    def get_stats_last_read(self):
        """
        return the time of the last read request on KNX
        :return: datetime of last time read
        """
        return self.stats_last_read

    def get_stats_last_write(self):
        """
        return the time of the last write request on KNX
        :return: datetime of last time write
        """
        return self.stats_last_write

    def get_stats_last_response(self):
        """
        return the time of the last response on KNX
        :return: datetime of last response write
        """
        return self.stats_last_response

    def get_stats_last_action(self):
        """
        gives back the last point in time when a telegram from KNX arrived
        :return: datetime of last time
        """
        ar = [self.stats_last_response, self.stats_last_write, self.stats_last_read]
        while None in ar:
            ar.remove(None)
        if ar == []:
            return None
        else:
            return max(ar)

    def get_unsatisfied_cache_read_ga(self):
        """
        At start all items that have a knx_cache attribute will be queried to knxd
        it could however happen, that not all of these queries are satisfied with a response,
        either of a knx failure, an internatl knxd problem or absent devices
        So ideally no reminding ga should be left after a delay time of startup
        :return: list of group addresses that did not receive a cache read response
        """
        return self._cache_ga_response_pending
コード例 #4
0
class Russound(SmartPlugin):
    """
    Main class of the Plugin. Does all plugin specific stuff and provides
    the update functions for the items
    """

    PLUGIN_VERSION = '1.7.0'

    def __init__(self, sh, *args, **kwargs):
        """
        Initalizes the plugin. The parameters describe for this method are pulled from the entry in plugin.conf.
        """
        from bin.smarthome import VERSION
        if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5':
            self.logger = logging.getLogger(__name__)

        try:
            # sh = self.get_sh() to get it.
            self.host = self.get_parameter_value('host')
            self.port = self.get_parameter_value('port')
            pass
        except KeyError as e:
            self.logger.critical(
                "Plugin '{}': Inconsistent plugin (invalid metadata definition: {} not defined)"
                .format(self.get_shortname(), e))
            self._init_complete = False
            return

        # Initialization code goes here
        self.terminator = RESP_DELIMITER
        self._client = Tcp_client(self.host,
                                  self.port,
                                  terminator=self.terminator)
        self._client.set_callbacks(data_received=self.found_terminator)
        self.params = {}
        self.sources = {}

        self.init_webinterface()
        return

    def run(self):
        """
        Run method for the plugin
        """
        self.logger.debug("Run method called")
        if not self._client.connect():
            self.logger.debug(
                f'Connection to {self.host}:{self.port} not possible. Plugin deactivated.'
            )
            return

        self.alive = True
#
# self.close()

    def stop(self):
        """
        Stop method for the plugin
        """
        self.logger.debug("Stop method called")
        self.alive = False
        self._client.close()

    def parse_item(self, item):
        """
        Default plugin parse_item method. Is called when the plugin is initialized.
        The plugin can, corresponding to its attribute keywords, decide what to do with
        the item in future, like adding it to an internal array for future reference
        :param item:    The item to process.
        :return:        If the plugin needs to be informed of an items change you should return a call back function
                        like the function update_item down below. An example when this is needed is the knx plugin
                        where parse_item returns the update_item function when the attribute knx_send is found.
                        This means that when the items value is about to be updated, the call back function is called
                        with the item, caller, source and dest as arguments and in case of the knx plugin the value
                        can be sent to the knx with a knx write function within the knx plugin.
        """
        # if self.has_iattr(item.conf, 'rus_src'):
        #     s = int(self.get_iattr_value(item.conf, 'rus_src'))
        #     self.sources[s] = {'s': s, 'item':item}
        #     self.logger.debug("Source {0} added".format(s))
        #     return None

        if self.has_iattr(item.conf, 'rus_path'):
            self.logger.debug("parse item: {}".format(item))

            path = self.get_iattr_value(item.conf, 'rus_path')
            parts = path.split('.', 2)

            if len(parts) != 3:
                self.logger.warning(
                    "Invalid Russound path with value {0}, format should be 'c.z.p' c = controller, z = zone, p = parameter name."
                    .format(path))
                return None

            c = parts[0]
            z = parts[1]
            param = parts[2]

        else:
            if self.has_iattr(item.conf, 'rus_controller'):
                c = self.get_iattr_value(item.conf, 'rus_controller')
                path = c + '.'
            else:
                return None

            if self.has_iattr(item.conf, 'rus_zone'):
                z = self.get_iattr_value(item.conf, 'rus_zone')
                path += z + '.'
            else:
                self.logger.warning(
                    "No zone specified for controller {0} in config of item {1}"
                    .format(c, item))
                return None

            if self.has_iattr(item.conf, 'rus_parameter'):
                param = self.get_iattr_value(item.conf, 'rus_parameter')
                path += param
            else:
                self.logger.warning(
                    "No parameter specified for zone {0} on controller {1} in config of item {2}"
                    .format(z, c, item))
                return None

            if param == 'relativevolume':
                # item._enforce_updates = True
                item.property.enforce_updates = True

            self.set_attr_value(item.conf, 'rus_path', path)

        param = param.lower()
        self.params[path] = {
            'c': int(c),
            'z': int(z),
            'param': param,
            'item': item
        }
        self.logger.debug("Parameter {0} with path {1} added".format(
            item, path))

        return self.update_item

    def parse_logic(self, logic):
        pass

    def _restrict(self, val, minval, maxval):
        if val < minval:
            return minval
        if val > maxval:
            return maxval
        return val

    def update_item(self, item, caller=None, source=None, dest=None):
        """
        Item has been updated

        This method is called, if the value of an item has been updated by SmartHomeNG.
        It should write the changed value out to the device (hardware/interface) that
        is managed by this plugin.

        :param item: item to be updated towards the plugin
        :param caller: if given it represents the callers name
        :param source: if given it represents the source
        :param dest: if given it represents the dest
        """
        if self.alive and caller != self.get_shortname():
            # code to execute if the plugin is not stopped
            # and only, if the item has not been changed by this this plugin:
            self.logger.info(
                "Update item: {}, item has been changed outside this plugin (caller={}, source={}, dest={})"
                .format(item.id(), caller, source, dest))

            if self.has_iattr(item.conf, 'rus_path'):
                path = self.get_iattr_value(item.conf, 'rus_path')
                p = self.params[path]
                cmd = p['param']
                c = p['c']
                z = p['z']

                if cmd == 'bass':
                    self.send_set(c, z, cmd, self._restrict(item(), -10, 10))
                elif cmd == 'treble':
                    self.send_set(c, z, cmd, self._restrict(item(), -10, 10))
                elif cmd == 'balance':
                    self.send_set(c, z, cmd, self._restrict(item(), -10, 10))
                elif cmd == 'loudness':
                    self.send_set(c, z, cmd, 'ON' if item() else 'OFF')
                elif cmd == 'turnonvolume':
                    self.send_set(c, z, cmd, self._restrict(item(), 0, 50))
                elif cmd == 'status':
                    self.send_event(c, z, 'ZoneOn' if item() else 'ZoneOff')
                elif cmd == 'partymode':
                    self.send_event(c, z, cmd, item().lower())
                elif cmd == 'donotdisturb':
                    self.send_event(c, z, cmd, 'on' if item() else 'off')
                elif cmd == 'volume':
                    self.send_event(c, z, 'KeyPress', 'Volume',
                                    self._restrict(item(), 0, 50))
                elif cmd == 'currentsource':
                    self.send_event(c, z, 'SelectSource', item())
                elif cmd == 'relativevolume':
                    self.send_event(c, z, 'KeyPress',
                                    'VolumeUp' if item() else 'VolumeDown')
                elif cmd == 'name':
                    return
                else:
                    self.key_release(c, z, cmd)

    def send_set(self, c, z, cmd, value):
        self._send_cmd('SET C[{0}].Z[{1}].{2}="{3}"\r'.format(
            c, z, cmd, value))

    def send_event(self, c, z, cmd, value1=None, value2=None):
        if value1 is None and value2 is None:
            self._send_cmd('EVENT C[{0}].Z[{1}]!{2}\r'.format(c, z, cmd))
        elif value2 is None:
            self._send_cmd('EVENT C[{0}].Z[{1}]!{2} {3}\r'.format(
                c, z, cmd, value1))
        else:
            self._send_cmd('EVENT C[{0}].Z[{1}]!{2} {3} {4}\r'.format(
                c, z, cmd, value1, value2))

    def key_release(self, c, z, key_code):
        self.send_event(c, z, 'KeyRelease', key_code)

    def key_hold(self, c, z, key_code, hold_time):
        self.send_event(c, z, 'KeyHold', key_code, hold_time)

    def _watch_zone(self, controller, zone):
        self._send_cmd('WATCH C[{0}].Z[{1}] ON\r'.format(controller, zone))

    def _watch_source(self, source):
        self._send_cmd('WATCH S[{0}] ON\r'.format(source))

    def _watch_system(self):
        self._send_cmd('WATCH System ON\r')

    def _send_cmd(self, cmd):
        if not self.alive:
            self.logger.error('Trying to send data but plugin is not running')
            return
        self.logger.debug("Sending request: {0}".format(cmd))

        # if connection is closed we don't wait for sh.con to reopen it
        # instead we reconnect immediatly
        #
        # if not self.connected:
        #     self.connect()
        if not self._client.connected:
            self._client.connect()

        # self.send(cmd.encode())
        self._client.send(cmd.encode())
#

    def found_terminator(self, resp):
        try:
            resp = resp.decode()
        except Exception as e:
            self.logger.error(
                "found_terminator: exception in decode: {}".format(e))
            return

        try:
            self.logger.debug("Parse response: {0}".format(resp))
            if resp[0] == 'S':
                return
            if resp[0] == 'E':
                self.logger.debug("Received response error: {0}".format(resp))
            elif resp[0] == 'N':
                resp = resp[2:]

                if resp[0] == 'C':
                    resp = resp.split('.', 2)
                    c = int(resp[0][2])
                    z = int(resp[1][2])
                    resp = resp[2]
                    cmd = resp.split('=')[0].lower()
                    value = resp.split('"')[1]

                    path = '{0}.{1}.{2}'.format(c, z, cmd)
                    if path in list(self.params.keys()):
                        self.params[path]['item'](self._decode(cmd, value),
                                                  self.get_shortname())
                elif resp.startswith('System.status'):
                    return
                elif resp[0] == 'S':
                    resp = resp.split('.', 1)
                    #                   s = int(resp[0][2])
                    resp = resp[1]
                    cmd = resp.split('=')[0].lower()
                    value = resp.split('"')[1]

                    #                    if s in self.sources.keys():
                    #                        for child in self.sources[s]['item'].return_children():
                    #                            if str(child).lower() == cmd.lower():
                    #                                child(unicode(value, 'utf-8'), self.get_shortname())
                    return
        except Exception as e:
            self.logger.error(e)

    def _decode(self, cmd, value):
        cmd = cmd.lower()
        if cmd in ['bass', 'treble', 'balance', 'turnonvolume', 'volume']:
            return int(value)
        elif cmd in ['loudness', 'status', 'mute']:
            return value == 'ON'
        elif cmd in ['partymode', 'donotdisturb']:
            return value.lower()
        elif cmd == 'currentsource':
            return value
        elif cmd == 'name':
            return str(value)

    def handle_connect(self):
        self.discard_buffers()
        self.terminator = RESP_DELIMITER
        self._watch_system()

        zones = []
        for path in self.params:
            p = self.params[path]
            key = '{0}.{1}'.format(p['c'], p['z'])
            if key not in zones:
                zones.append(key)
                self._watch_zone(p['c'], p['z'])

        for s in self.sources:
            self._watch_source(s)

    def poll_device(self):
        """
        Polls for updates of the device

        This method is only needed, if the device (hardware/interface) does not propagate
        changes on it's own, but has to be polled to get the actual status.
        It is called by the scheduler which is set within run() method.
        """
        # # get the value from the device
        # device_value = ...
        #
        # # find the item(s) to update:
        # for item in self.sh.find_items('...'):
        #
        #     # update the item by calling item(value, caller, source=None, dest=None)
        #     # - value and caller must be specified, source and dest are optional
        #     #
        #     # The simple case:
        #     item(device_value, self.get_shortname())
        #     # if the plugin is a gateway plugin which may receive updates from several external sources,
        #     # the source should be included when updating the the value:
        #     item(device_value, self.get_shortname(), source=device_source_id)
        pass

    def init_webinterface(self):
        """"
        Initialize the web interface for this plugin

        This method is only needed if the plugin is implementing a web interface
        """
        try:
            self.mod_http = Modules.get_instance().get_module(
                'http'
            )  # try/except to handle running in a core version that does not support modules
        except:
            self.mod_http = None
        if self.mod_http is None:
            self.logger.error("Not initializing the web interface")
            return False

        if "SmartPluginWebIf" not in list(
                sys.modules['lib.model.smartplugin'].__dict__):
            self.logger.warning(
                "Web interface needs SmartHomeNG v1.5 and up. Not initializing the web interface"
            )
            return False

        # set application configuration for cherrypy
        webif_dir = self.path_join(self.get_plugin_dir(), 'webif')
        config = {
            '/': {
                'tools.staticdir.root': webif_dir,
            },
            '/static': {
                'tools.staticdir.on': True,
                'tools.staticdir.dir': 'static'
            }
        }

        # Register the web interface as a cherrypy app
        self.mod_http.register_webif(WebInterface(webif_dir, self),
                                     self.get_shortname(),
                                     config,
                                     self.get_classname(),
                                     self.get_instance_name(),
                                     description='')

        return True
コード例 #5
0
ファイル: __init__.py プロジェクト: Foxi352/netlib
class KNX(SmartPlugin):
    ALLOW_MULTIINSTANCE = True
    PLUGIN_VERSION = "1.3.4"

    # tags actually used by the plugin are shown here
    # can be used later for backend item editing purposes, to check valid item attributes
    ITEM_TAG = [
        KNX_DPT, KNX_STATUS, KNX_SEND, KNX_REPLY, KNX_LISTEN, KNX_INIT,
        KNX_CACHE, KNX_POLL
    ]
    ITEM_TAG_PLUS = [KNX_DTP]

    def __init__(self,
                 smarthome,
                 time_ga=None,
                 date_ga=None,
                 send_time=False,
                 busmonitor=False,
                 host='127.0.0.1',
                 port=6720,
                 readonly=False,
                 instance='default',
                 enable_stats=True):
        #lib.connection.Client.__init__(self, host, port, monitor=True)
        self._client = Tcp_client(name='KNX',
                                  host=host,
                                  port=port,
                                  binary=True,
                                  autoreconnect=True,
                                  connect_cycle=5,
                                  retry_cycle=30)
        self._client.set_callbacks(
            connected=self.handle_connect, data_received=self.parse_telegram
        )  # , disconnected=disconnected_callback, data_received=receive_callback
        #self._client.terminator = 2
        self.logger = logging.getLogger(__name__)
        self.logger.debug("init knx")
        self._sh = smarthome
        self.gal = {
        }  # group addresses to listen to {DPT: dpt, ITEMS: [item 1, item 2, ..., item n], LOGICS: [ logic 1, logic 2, ..., logic n]}
        self.gar = {
        }  # group addresses to reply if requested from knx, {DPT: dpt, ITEM: item, LOGIC: None}
        self._init_ga = []
        self._cache_ga = [
        ]  # group addresses which should be initalized by the knxd cache
        self._cache_ga_response_pending = []
        self.time_ga = time_ga
        self.date_ga = date_ga
        self._instance = instance
        self._lock = threading.Lock()
        self._bm_separatefile = False
        self._bm_format = "KNX[{0}]: {1} set {2} to {3}"
        self._isLength = False
        # following needed for statistics
        self.enable_stats = enable_stats
        self.stats_ga = {}  # statistics for used group addresses on the BUS
        self.stats_pa = {}  # statistics for used group addresses on the BUS
        self.stats_last_read = None  # last read request from KNX
        self.stats_last_write = None  # last write from KNX
        self.stats_last_response = None  # last response from KNX
        self.stats_last_action = None  # the newes

        if self.to_bool(busmonitor, default=busmonitor):
            self._busmonitor = self.logger.info
        else:
            self._busmonitor = self.logger.debug

            # write bus messages in a separate logger
            if isinstance(busmonitor, str):
                if busmonitor.lower() in ['logger']:
                    self._bm_separatefile = True
                    self._bm_format = "{0};{1};{2};{3}"
                    self._busmonitor = logging.getLogger("knx_busmonitor").info

        if send_time:
            self._sh.scheduler.add('KNX[{0}] time'.format(
                self.get_instance_name()),
                                   self._send_time,
                                   prio=5,
                                   cycle=int(send_time))

        readonly = self.to_bool(readonly)
        if readonly:
            self.logger.warning("!!! KNX Plugin in READONLY mode !!! ")
        self.readonly = readonly

    #### just here until the smartplugin base class is fixed: Unfortunately it does not set it's name if ALLOW_MULTIINSTANCE is False
    def get_instance_name(self):
        """
            return instance name of the plugin
            :rtype: str
        """
        return self._instance

    def _send(self, data):
        if len(data) < 2 or len(data) > 0xffff:
            self.logger.debug('KNX[{0}]: Illegal data size: {1}'.format(
                self.get_instance_name(), repr(data)))
            return False
        # prepend data length
        send = bytearray(len(data).to_bytes(2, byteorder='big'))
        send.extend(data)
        #self.send(send)
        self._client.send(send)

    def groupwrite(self, ga, payload, dpt, flag='write'):
        pkt = bytearray([0, KNXD_GROUP_PACKET])
        try:
            pkt.extend(self.encode(ga, 'ga'))
        except:
            self.logger.warning('KNX[{0}]: problem encoding ga: {1}'.format(
                self.get_instance_name(), ga))
            return
        pkt.extend([0])
        pkt.extend(self.encode(payload, dpt))
        if flag == 'write':
            flag = KNXWRITE
        elif flag == 'response':
            flag = KNXRESP
        else:
            self.logger.warning(
                "KNX[{0}]: groupwrite telegram for {1} with unknown flag: {2}. Please choose beetween write and response."
                .format(self.get_instance_name(), ga, flag))
            return
        pkt[5] = flag | pkt[5]
        if self.readonly:
            self.logger.info(
                "KNX[{2}]: groupwrite telegram for: {0} - Value: {1} not send. Plugin in READONLY mode. "
                .format(ga, payload, self.get_instance_name()))
        else:
            self._send(pkt)

    def _cacheread(self, ga):
        pkt = bytearray([0, KNXD_CACHE_READ])
        try:
            pkt.extend(self.encode(ga, 'ga'))
        except:
            self.logger.warning('KNX[{0}]: problem encoding ga: {1}'.format(
                self.get_instance_name(), ga))
            return
        pkt.extend([0, 0])
        self.logger.debug('KNX[{0}]: reading knxd cache for ga: {1}'.format(
            self.get_instance_name(), ga))
        self._send(pkt)

    def groupread(self, ga):
        pkt = bytearray([0, KNXD_GROUP_PACKET])
        try:
            pkt.extend(self.encode(ga, 'ga'))
        except:
            self.logger.warning('KNX[{0}]: problem encoding ga: {1}'.format(
                self.get_instance_name(), ga))
            return
        pkt.extend([0, KNXREAD])
        self._send(pkt)

    def _poll(self, **kwargs):
        if ITEM in kwargs:
            item = kwargs[ITEM]
        else:
            item = 'unknown item'

        if 'ga' in kwargs:
            self.groupread(kwargs['ga'])
        else:
            self.logger.warning(
                'KNX[{0}]: problem polling {1}, no known ga'.format(
                    self.get_instance_name(), item))

        if 'interval' in kwargs and 'ga' in kwargs:
            ga = kwargs['ga']
            interval = int(kwargs['interval'])
            next = self._sh.now() + timedelta(seconds=interval)
            self._sh.scheduler.add('KNX poll {}'.format(item),
                                   self._poll,
                                   value={
                                       'instance': self.get_instance_name(),
                                       ITEM: item,
                                       'ga': ga,
                                       'interval': interval
                                   },
                                   next=next)

    def _send_time(self):
        self.send_time(self.time_ga, self.date_ga)

    def send_time(self, time_ga=None, date_ga=None):
        now = self._sh.now()
        if time_ga:
            self.groupwrite(time_ga, now, '10')
        if date_ga:
            self.groupwrite(date_ga, now.date(), '11')

    def handle_connect(self, client):
        #self.discard_buffers()
        enable_cache = bytearray([0, KNXD_CACHE_ENABLE])
        self._send(enable_cache)
        self.found_terminator = self.parse_length
        self._isLength = True
        if self._cache_ga != []:
            if client.connected:
                self.logger.debug('KNX[{0}]: reading knxd cache'.format(
                    self.get_instance_name()))
                for ga in self._cache_ga:
                    self._cache_ga_response_pending.append(ga)
                for ga in self._cache_ga:
                    self._cacheread(ga)
                    # wait a little to not overdrive the knxd unless there is a fix
                    time.sleep(KNXD_CACHEREAD_DELAY)
                self._cache_ga = []
                self.logger.debug(
                    'KNX[{0}]: finished reading knxd cache'.format(
                        self.get_instance_name()))

        self.logger.debug('KNX[{0}]: enable group monitor'.format(
            self.get_instance_name()))
        init = bytearray([0, KNXD_OPEN_GROUPCON, 0, 0, 0])
        self._send(init)
        client.terminator = 2
        if self._init_ga != []:
            if client.connected:
                self.logger.debug('KNX[{0}]: knxd init read for {1} ga'.format(
                    self.get_instance_name(), len(self._init_ga)))
                for ga in self._init_ga:
                    self.groupread(ga)
                self._init_ga = []
                self.logger.debug('KNX[{0}]: finished knxd init read'.format(
                    self.get_instance_name()))

#   def collect_incoming_data(self, data):
#       print('#  bin   h  d')
#       for i in data:
#           print("{0:08b} {0:02x} {0:02d}".format(i))
#       self.buffer.extend(data)

    def parse_length(self, length):
        # self.found_terminator is introduced in lib/connection.py
        #self.found_terminator = self.parse_telegram
        try:
            self.terminator = struct.unpack(">H", length)[0]
            self.logger.debug("******** TERMINATOR: {}".format(
                self.terminator))
        except:
            self.logger.error("KNX[{0}]: problem unpacking length: {1}".format(
                self.get_instance_name(), length))
            self.close()

    def encode(self, data, dpt):
        return dpts.encode[str(dpt)](data)

    def decode(self, data, dpt):
        return dpts.decode[str(dpt)](data)

    def parse_telegram(self, client, data):
        """
        inspects a received eibd/knxd compatible telegram
        
        :param data: expected is a bytearray with
            2 byte type   --> see eibtypes.h
            2 byte source as physical address
            2 byte destination as group address
            2 byte command/data
            n byte data

        """
        if self._isLength:
            self._isLength = False
            try:
                client.terminator = struct.unpack(">H", data)[0]
            except:
                self.logger.error(
                    "KNX[{0}]: problem unpacking length: {1}".format(
                        self.get_instance_name(), data))
            return
        else:
            self._isLength = True
            client.terminator = 2
        typ = struct.unpack(">H", data[0:2])[0]
        if (typ != KNXD_GROUP_PACKET
                and typ != KNXD_CACHE_READ) or len(data) < 8:
            #self.logger.debug("Ignore telegram.")
            return
        if (data[6] & 0x03 or (data[7] & 0xC0) == 0xC0):
            self.logger.debug("KNX[{0}]: Unknown APDU".format(
                self.get_instance_name()))
            return
        src = self.decode(data[2:4], 'pa')
        dst = self.decode(data[4:6], 'ga')
        flg = data[7] & 0xC0
        if flg == KNXWRITE:
            flg = 'write'
        elif flg == KNXREAD:
            flg = 'read'
        elif flg == KNXRESP:
            flg = 'response'
        else:
            self.logger.warning(
                "KNX[{0}]: Unknown flag: {1:02x} src: {2} dest: {3}".format(
                    self.get_instance_name(), flg, src, dst))
            return
        if len(data) == 8:
            payload = bytearray([data[7] & 0x3f])
        else:
            payload = data[8:]

        if self.enable_stats:
            # update statistics on used group addresses
            if not dst in self.stats_ga:
                self.stats_ga[dst] = {}

            if not flg in self.stats_ga[dst]:
                self.stats_ga[dst][flg] = 1
            else:
                self.stats_ga[dst][flg] = self.stats_ga[dst][flg] + 1

            # update statistics on used group addresses
            if not src in self.stats_pa:
                self.stats_pa[src] = {}

            if not flg in self.stats_pa[src]:
                self.stats_pa[src][flg] = 1
            else:
                self.stats_pa[src][flg] = self.stats_pa[src][flg] + 1

        # further inspect what to do next
        if flg == 'write' or flg == 'response':
            if dst not in self.gal:  # update item/logic
                self._busmonitor(
                    self._bm_format.format(self.get_instance_name(), src, dst,
                                           binascii.hexlify(payload).decode()))
                return
            dpt = self.gal[dst][DPT]
            try:
                val = self.decode(payload, dpt)
            except Exception as e:
                self.logger.exception(
                    "KNX[{0}]: Problem decoding frame from {1} to {2} with '{3}' and DPT {4}. Exception: {5}"
                    .format(self.get_instance_name(), src, dst,
                            binascii.hexlify(payload).decode(), dpt, e))
                return
            if val is not None:
                self._busmonitor(
                    self._bm_format.format(self.get_instance_name(), src, dst,
                                           val))
                #print "in:  {0}".format(self.decode(payload, 'hex'))
                #out = ''
                #for i in self.encode(val, dpt):
                #    out += " {0:x}".format(i)
                #print "out:{0}".format(out)

                # remove all ga that came from a cache read request
                if typ == KNXD_CACHE_READ:
                    if dst in self._cache_ga_response_pending:
                        self._cache_ga_response_pending.remove(dst)
                way = "" if typ != KNXD_CACHE_READ else " (from knxd Cache)"
                self.logger.debug(
                    "KNX[{0}]: {5} request from {1} to {2} with '{3}' and DPT {4}{6}"
                    .format(self.get_instance_name(), src, dst,
                            binascii.hexlify(payload).decode(), dpt, flg, way))
                for item in self.gal[dst][ITEMS]:
                    item(val, 'KNX', src, dst)
                for logic in self.gal[dst][LOGICS]:
                    logic.trigger('KNX', src, val, dst)
            else:
                self.logger.warning(
                    "KNX[{0}]: Wrong payload '{3}' for ga '{2}' with dpt '{1}'."
                    .format(self.get_instance_name(), dpt, dst,
                            binascii.hexlify(payload).decode()))
            if self.enable_stats:
                if flg == 'write':
                    self.stats_last_write = self._sh.now()
                else:
                    self.stats_last_response = self._sh.now()
        elif flg == 'read':
            self.logger.debug("KNX[{0}]: {1} read {2}".format(
                self.get_instance_name(), src, dst))
            if self.enable_stats:
                self.stats_last_read = self._sh.now()
            if dst in self.gar:  # read item
                if self.gar[dst][ITEM] is not None:
                    item = self.gar[dst][ITEM]
                    self.groupwrite(dst, item(),
                                    self.get_iattr_value(item.conf, KNX_DPT),
                                    'response')
                if self.gar[dst][LOGIC] is not None:
                    self.gar[dst][LOGIC].trigger('KNX', src, None, dst)

    def run(self):
        self.alive = True
        self._client.connect()

    def stop(self):
        self.alive = False
        self._client.close()

    def parse_item(self, item):
        """
        examines item attributes to see if action is needed by change of the item via SmartHomeNG
        :param item: a dictionary with item attributes
        :return: a callback function to be called then item is to be changed
        """
        if self.has_iattr(item.conf, KNX_DTP):
            self.logger.error(
                "KNX[{0}]: Ignoring {1}: please change knx_dtp to knx_dpt.".
                format(self.get_instance_name(), item))
            return None
        if self.has_iattr(item.conf, KNX_DPT):
            dpt = self.get_iattr_value(item.conf, KNX_DPT)
            if dpt not in dpts.decode:
                self.logger.warning(
                    "KNX[{0}]: Ignoring {1} unknown dpt: {2}".format(
                        self.get_instance_name(), item, dpt))
                return None
        elif self.has_iattr(item.conf, KNX_STATUS) or self.has_iattr(
                item.conf, KNX_SEND) or self.has_iattr(
                    item.conf, KNX_REPLY) or self.has_iattr(
                        item.conf, KNX_LISTEN) or self.has_iattr(
                            item.conf, KNX_INIT) or self.has_iattr(
                                item.conf, KNX_CACHE):
            self.logger.warning(
                "KNX[{0}]: Ignoring {1}: please add knx_dpt.".format(
                    self.get_instance_name(), item))
            return None
        else:
            return None
        # todo:
        # cleanup here
        # following deleted because multi instance was changed
        #if self.has_iattr(item.conf, self.KNX_INSTANCE):
        #    if not item.conf[self.KNX_INSTANCE] == self.instance:
        #        return None
        #else:
        #    if not self.instance == 'default':
        #        return None
        self.logger.debug(
            "KNX[{1}]: Item {0} is mapped to KNX Instance {1}".format(
                item, self.get_instance_name()))

        if self.has_iattr(item.conf, KNX_LISTEN):
            knx_listen = self.get_iattr_value(item.conf, KNX_LISTEN)
            if isinstance(knx_listen, str):
                knx_listen = [
                    knx_listen,
                ]
            for ga in knx_listen:
                self.logger.debug("KNX[{0}]: {1} listen on {2}".format(
                    self.get_instance_name(), item, ga))
                if not ga in self.gal:
                    self.gal[ga] = {DPT: dpt, ITEMS: [item], LOGICS: []}
                else:
                    if not item in self.gal[ga][ITEMS]:
                        self.gal[ga][ITEMS].append(item)

        if self.has_iattr(item.conf, KNX_INIT):
            ga = self.get_iattr_value(item.conf, KNX_INIT)
            self.logger.debug(
                "KNX[{0}]: {1} listen on and init with {2}".format(
                    self.get_instance_name(), item, ga))
            if not ga in self.gal:
                self.gal[ga] = {DPT: dpt, ITEMS: [item], LOGICS: []}
            else:
                if not item in self.gal[ga][ITEMS]:
                    self.gal[ga][ITEMS].append(item)
            self._init_ga.append(ga)

        if self.has_iattr(item.conf, KNX_CACHE):
            ga = self.get_iattr_value(item.conf, KNX_CACHE)
            self.logger.debug(
                "KNX[{0}]: {1} listen on and init with cache {2}".format(
                    self.get_instance_name(), item, ga))
            if not ga in self.gal:
                self.gal[ga] = {DPT: dpt, ITEMS: [item], LOGICS: []}
            else:
                if not item in self.gal[ga][ITEMS]:
                    self.gal[ga][ITEMS].append(item)
            self._cache_ga.append(ga)

        if self.has_iattr(item.conf, KNX_REPLY):
            knx_reply = self.get_iattr_value(item.conf, KNX_REPLY)
            if isinstance(knx_reply, str):
                knx_reply = [
                    knx_reply,
                ]
            for ga in knx_reply:
                self.logger.debug("KNX[{0}]: {1} reply to {2}".format(
                    self.get_instance_name(), item, ga))
                if ga not in self.gar:
                    self.gar[ga] = {DPT: dpt, ITEM: item, LOGIC: None}
                else:
                    self.logger.warning(
                        "KNX[{0}]: {1} knx_reply ({2}) already defined for {3}"
                        .format(self.get_instance_name(), item.id(), ga,
                                self.gar[ga][ITEM]))

        if self.has_iattr(item.conf, KNX_SEND):
            if isinstance(self.get_iattr_value(item.conf, KNX_SEND), str):
                self.set_attr_value(item.conf, KNX_SEND, [
                    self.get_iattr_value(item.conf, KNX_SEND),
                ])
                #item.conf['knx_send'] = [self.get_iattr_value(item.conf,'knx_send'), ]

        if self.has_iattr(item.conf, KNX_STATUS):
            if isinstance(self.get_iattr_value(item.conf, KNX_STATUS), str):
                self.set_attr_value(item.conf, KNX_STATUS, [
                    self.get_iattr_value(item.conf, KNX_STATUS),
                ])
                #item.conf['knx_status'] = [self.get_iattr_value(item.conf,'knx_status'), ]

        if self.has_iattr(item.conf, KNX_STATUS) or self.has_iattr(
                item.conf, KNX_SEND):
            return self.update_item

        if self.has_iattr(item.conf, KNX_POLL):
            knx_poll = self.get_iattr_value(item.conf, KNX_POLL)
            if isinstance(knx_poll, str):
                knx_poll = [
                    knx_poll,
                ]
            if len(knx_poll) == 2:
                poll_ga = knx_poll[0]
                poll_interval = knx_poll[1]

                self.logger.info(
                    "KNX[{0}]: Item {1} is polled on GA {2} every {3} seconds".
                    format(self.get_instance_name(), item, poll_ga,
                           poll_interval))
                randomwait = random.randrange(15)
                next = self._sh.now() + timedelta(seconds=poll_interval +
                                                  randomwait)
                self._sh.scheduler.add('KNX poll {}'.format(item),
                                       self._poll,
                                       value={
                                           ITEM: item,
                                           'ga': poll_ga,
                                           'interval': poll_interval
                                       },
                                       next=next)
            else:
                self.logger.warning(
                    "KNX[{0}]: Ignoring knx_poll for item {1}: We need two parameters, one for the GA and one for the polling interval."
                    .format(self.get_instance_name(), item))
                pass

        return None

    def parse_logic(self, logic):
        if KNX_DPT in logic.conf:
            dpt = logic.conf[KNX_DPT]
            if dpt not in dpts.decode:
                self.logger.warning(
                    "KNX[{0}]: Ignoring {1} unknown dpt: {2}".format(
                        self.get_instance_name(), logic, dpt))
                return None
        else:
            return None

        #if self.has_iattr(logic.conf,'knx_instance'):
        #    if not logic.conf['knx_instance'] == self.get_instance_name():
        #        return None
        #else:
        #    if not self.get_instance_name() == 'default':
        #        return None
        self.logger.debug(
            "KNX[{1}]: Logic {0} is mapped to KNX Instance {1}".format(
                logic, self.get_instance_name()))

        if KNX_LISTEN in logic.conf:
            knx_listen = logic.conf[KNX_LISTEN]
            if isinstance(knx_listen, str):
                knx_listen = [
                    knx_listen,
                ]
            for ga in knx_listen:
                self.logger.debug("KNX[{0}]: {1} listen on {2}".format(
                    self.get_instance_name(), logic, ga))
                if not ga in self.gal:
                    self.gal[ga] = {DPT: dpt, ITEMS: [], LOGICS: [logic]}
                else:
                    self.gal[ga][LOGICS].append(logic)

        if KNX_REPLY in logic.conf:
            knx_reply = logic.conf[KNX_REPLY]
            if isinstance(knx_reply, str):
                knx_reply = [
                    knx_reply,
                ]
            for ga in knx_reply:
                self.logger.debug("KNX[{0}]: {1} reply to {2}".format(
                    self.get_instance_name(), logic, ga))
                if ga in self.gar:
                    if self.gar[ga][LOGIC] is False:
                        obj = self.gar[ga][ITEM]
                    else:
                        obj = self.gar[ga][LOGIC]
                    self.logger.warning(
                        "KNX[{0}]: {1} knx_reply ({2}) already defined for {3}"
                        .format(self.get_instance_name(), logic, ga, obj))
                else:
                    self.gar[ga] = {DPT: dpt, ITEM: None, LOGIC: logic}

    def update_item(self, item, caller=None, source=None, dest=None):
        """
        decides what to do with an updates item value
        :param item: the item with its attributes
        :param caller: a hint to the originator of the values change
        """
        if self.has_iattr(item.conf, KNX_SEND):
            if caller != 'KNX':
                for ga in self.get_iattr_value(item.conf, KNX_SEND):
                    self.groupwrite(ga, item(),
                                    self.get_iattr_value(item.conf, KNX_DPT))
        if self.has_iattr(item.conf, KNX_STATUS):
            for ga in self.get_iattr_value(item.conf,
                                           KNX_STATUS):  # send status update
                if ga != dest:
                    self.groupwrite(ga, item(),
                                    self.get_iattr_value(item.conf, KNX_DPT))

    """
    The statistics functions were introduced to watch what is happening on the KNX.
    Mainly it is recorded which physical device sends data by write or response or requests data
    by read operation.
    Whenever such a telegram is received, it is recorded 
    - which physical device sended the request (originator)
    - which kind of request (read, write, response)
    - target group address affected
    - a counter for the specific kind of request (read, write, response) is increased.
    
    With an additional logic these statistics can be requested from the plugin and examined for
    - unknown group addresses which are not known to either ETS or to SmartHomeNG
    - unknown physical addresses which are new and unexpected
    - adresses that do not react upon requests
    - adresses that can't satisfy cache requests
    """

    def enable_stats(self):
        """
        Enables the tracking of KNX telegrams during runtime of SmartHomeNG
        """
        self.enable_stats = True

    def disable_stats(self):
        """
        Disables the tracking of KNX telegrams during runtime of SmartHomeNG
        It might be a good idea to clear your stats afterwards with clear_stats()
        """
        self.enable_stats = False

    def clear_stats(self):
        """
        clear all statistic values
        """
        self.clear_stats_ga()
        self.clear_stats_pa()

    def clear_stats_ga(self):
        """
        clear statistic values for group addresses
        """
        if len(self.stats_ga):
            for ga in self.stats_ga:
                self.stats_ga[ga] = {}

    def clear_stats_pa(self):
        """
        clear statistic values for physical addresses
        """
        if len(self.stats_pa):
            for ga in self.stats_pa:
                self.stats_pa[ga] = {}

    def get_stats_ga(self):
        """
        returns a dict with following structure
        ```
        stats_ga = { ga1 : { 'read' : n-read,               # counter of read requests from KNX
                             'write' : n-write,             # counter of write operations from KNX
                             'response' : n-response,       # counter of response operations from KNX
                             'last_read' : datetime,        # the respective datetime object of read,
                             'last_write' : datetime,       # write
                             'last_response' : datetime },  # and response
                     ga2 : {...} }
        ```
        :return: dict
        """
        return self.stats_ga

    def get_stats_pa(self):
        """
        returns a dict with following structure
        ```
        stats_pa = { pa1 : { 'read' : n-read,               # counter of read requests from KNX
                             'write' : n-write,             # counter of write operations from KNX
                             'response' : n-response,       # counter of response operations from KNX
                             'last_read' : datetime,        # the respective datetime object of read,
                             'last_write' : datetime,       # write
                             'last_response' : datetime },  # and response
                     pa2 : {...} }
        ```
        :return: dict
        """
        return self.stats_pa

    def get_stats_last_read(self):
        """
        return the time of the last read request on KNX
        :return: datetime of last time read
        """
        return self.stats_last_read

    def get_stats_last_write(self):
        """
        return the time of the last write request on KNX
        :return: datetime of last time write
        """
        return self.stats_last_write

    def get_stats_last_response(self):
        """
        return the time of the last response on KNX
        :return: datetime of last response write
        """
        return self.stats_last_response

    def get_stats_last_action(self):
        """
        gives back the last point in time when a telegram from KNX arrived
        :return: datetime of last time
        """
        ar = [
            self.stats_last_response, self.stats_last_write,
            self.stats_last_read
        ]
        while None in ar:
            ar.remove(None)
        return max(ar)

    def get_unsatisfied_cache_read_ga(self):
        """
        At start all items that have a knx_cache attribute will be queried to knxd
        it could however happen, that not all of these queries are satisfied with a response,
        either of a knx failure, an internatl knxd problem or absent devices
        So ideally no reminding ga should be left after a delay time of startup
        :return: list of group addresses that did not receive a cache read response
        """
        return self._cache_ga_response_pending
コード例 #6
0
class Kodi(SmartPlugin):
    '''
    Main class of the Plugin. Does all plugin specific stuff and provides
    the update functions for the items
    '''
    PLUGIN_VERSION = '1.6.1'
    ALLOW_MULTIINSTANCE = True
    _initcommands = ['get_actplayer', 'get_status_au', 'get_favourites']

    def __init__(self, sh, *args, **kwargs):
        '''
        Initalizes the plugin.
        '''
        # init logger
        self.logger = logging.getLogger(__name__)
        self.logger.info('Init Plugin')
        self._host = self.get_parameter_value('host')
        self._port = self.get_parameter_value('port')
        self._autoreconnect = self.get_parameter_value('autoreconnect')
        self._connect_retries = self.get_parameter_value('connect_retries')
        self._connect_cycle = self.get_parameter_value('connect_cycle')

        self._command_timeout = self.get_parameter_value('command_timeout')
        self._command_repeat = self.get_parameter_value('command_repeat')

        self._check_stale_cycle = float(self._command_timeout) / 2
        self._next_stale_check = 0
        self._last_stale_check = 0

        self._kodi_tcp_connection = Tcp_client(
            host=self._host,
            port=self._port,
            name='KodiTCPConnection',
            autoreconnect=self._autoreconnect,
            connect_retries=self._connect_retries,
            connect_cycle=self._connect_cycle)
        self._kodi_tcp_connection.set_callbacks(
            connected=self._on_connect,
            data_received=self._on_received_data,
            disconnected=self._on_disconnect)
        self._kodi_server_alive = False

        self._registered_items = {
            key: []
            for key in set(list(commands.commands.keys()))
        }
        self._CMD = commands.commands
        self._MACRO = commands.macros

        self._message_id = 0
        self._msgid_lock = threading.Lock()
        self._send_queue = queue.Queue()
        self._stale_lock = threading.Lock()

        # self._message_archive[str message_id] = [time.time() sendtime, str method, str params or None, int repeat]
        self._message_archive = {}

        self._activeplayers = []
        self._playerid = 0

        self._shutdown_active = False

        if not self._check_commands_data():
            self._init_complete = False

    def run(self):
        '''
        Run method for the plugin
        '''
        self.logger.debug('run method called')
        self._connect('run')
        self.alive = True
        self._next_stale_check = time.time() + self._check_stale_cycle

    def stop(self):
        '''
        Stop method for the plugin
        '''
        self.alive = False
        self.logger.debug('stop method called')
        self._kodi_tcp_connection.close()
        self._kodi_server_alive = False

    def parse_item(self, item):
        '''
        Method for parsing Kodi items.
        If the item carries the kodi_item field, this item is registered to the plugin.

        :param item:    The item to process.
        :type item:     object

        :return:        The item update method to be triggered if the kodi_item is in the set item dict.
        :rtype:         object
        '''
        if self.has_iattr(item.conf, 'kodi_item'):
            command = self.get_iattr_value(item.conf, 'kodi_item')
            self.logger.debug('Registering item: {}'.format(item))
            if command in self._registered_items:
                self._registered_items[command].append(item)
            else:
                self.logger.warning(
                    'I do not know the kodi_item {}, skipping!'.format(
                        command))
            if self._CMD[command]['set']:
                return self.update_item

    def parse_logic(self, logic):
        '''
        Method to parse plugin logics

        :note: Not implemented
        '''
        pass

    def update_item(self, item, caller=None, source=None, dest=None):
        '''
        Callback method for sending values to Kodi when a registered item has changed

        :param item: item to be updated towards the plugin
        :param caller: if given it represents the callers name
        :param source: if given it represents the source
        :param dest: if given it represents the dest
        '''
        # !! self.logger.debug('update_item called for item {} by {} with value {}'.format(item, caller, item()))
        if item(
        ) is not None and caller != self.get_shortname() and self.has_iattr(
                item.conf, 'kodi_item'):
            # update item was triggered by something other than this plugin -> send to Kodi

            kodi_item = self.get_iattr_value(item.conf, 'kodi_item')
            self.logger.debug(
                'Updating item {} using kodi command {} with value {}, called by {}'
                .format(item, kodi_item, item(), caller))

            # power is handled specially as it is not a command for kodi per se
            if kodi_item == 'power' and item():
                # if item is set to True, try to (re)establish a connection to Kodi
                self._connect('update')
                # if item is set to False, send shutdown command to Kodi.
                # This is handled in the standard block below though

            # trigger status update mechanism
            elif kodi_item == 'update':
                if item():
                    self._update_status()

            # macros
            elif kodi_item == 'macro':
                self._process_macro(item)

            # writable item
            elif kodi_item in self._CMD and self._CMD[kodi_item]['set']:
                # !! self.logger.debug('Running send_command({}, {}, False)'.format(kodi_item, item()))
                self._send_command(kodi_item, item())

            else:
                self.logger.info('kodi_item "%s" not in send_keys, skipping!',
                                 kodi_item)

            # set flag if shutdown of kodi server was ordered
            if kodi_item == 'power' and not item():
                self._shutdown_active = True

        elif self.has_iattr(item.conf, 'kodi_item'):
            self.logger.debug(
                'Not acting on item update: {} has set item {} to {}'.format(
                    caller, item, item()))

    def notify(self, title, message, image=None, display_time=10000):
        '''
        Send a notification to Kodi to be displayed on the screen

        :param title: the title of the message
        :param message: the message itself
        :param image: an optional image to be displayed alongside the message
        :param display_time: how long the message is displayed in milli seconds
        '''
        params = {
            'title': title,
            'message': message,
            'displaytime': display_time
        }
        if image is not None:
            params['image'] = image
        self._send_rpc_message('GUI.ShowNotification', params)

    def _on_connect(self, by=None):
        '''
        Recall method for succesful connect to Kodi
        On a connect first check if the JSON-RPC API is available.
        If this is the case query Kodi to initialize all items

        :param by: caller information
        :type by: str
        '''
        self._kodi_server_alive = True
        if isinstance(by, (dict, Tcp_client)):
            by = 'TCP_Connect'
        self.logger.info(
            'Connected to {}, onconnect called by {}, send queue contains {} commands'
            .format(self._host, by, self._send_queue.qsize()))
        self._set_all_items('power', True)
        if self._send_queue.qsize() == 0:
            for command in self._initcommands:
                self.logger.debug(
                    'Sending command after connect: {}'.format(command))
                self._send_command(command, None)

    def _on_disconnect(self, obj=None):
        '''
        Recall method for TCP disconnect
        '''
        self.logger.info('Received disconnect from {}'.format(self._host))
        self._set_all_items('power', False)
        self._kodi_server_alive = False
        for item in self._registered_items['power']:
            item(False, caller=self.get_shortname())

        # did we power down kodi? then clear queues
        if self._shutdown_active:
            old_queue = self._send_queue
            self._send_queue = queue.Queue()
            del old_queue
            self._stale_lock.acquire()
            self._message_archive = {}
            self._stale_lock.release()
            self._shutdown_active = False

    def _on_received_data(self, connection, response):
        '''
        This method is called by the TCP connection object whenever data is received from the host.
        '''

        self.logger.debug('Received data from TCP: {}'.format(response))

        # split multi-response data into list items
        try:
            datalist = response.replace('}{', '}-#-{').split('-#-')
            datalist = list(OrderedDict((x, True) for x in datalist).keys())
        except:
            datalist = [response]

        # process all response items
        for data in datalist:
            self.logger.debug('Processing received data item #{} ({})'.format(
                datalist.index(data), data))

            try:
                jdata = json.loads(data)
            except Exception as err:
                self.logger.warning(
                    'Could not json.load data item {} with error {}'.format(
                        data, err))
                continue

            # formerly there was a check for error responses, which triggered up to <param>
            # retries. Re-sending a command which produces an error seem quite ignorant, so
            # this functionality was dropped

            # check for replies
            if 'id' in jdata:
                response_id = jdata['id']

                # reply or error received, remove command
                if response_id in self._message_archive:
                    # possibly the command was resent and removed before processing the reply
                    # so let's 'try' at least...
                    try:
                        cmd = self._message_archive[response_id][1]
                        del self._message_archive[response_id]
                    except KeyError:
                        cmd = '(deleted)' if '#' not in response_id else response_id[
                            response_id.find('#') + 1:]
                else:
                    cmd = None

                # log possible errors
                if 'error' in jdata:
                    self.logger.error(
                        'Received error {} in response to command {}'.format(
                            jdata, cmd))
                elif cmd:
                    self.logger.debug(
                        'Command sent successfully: {}'.format(cmd))

            # process data
            self._parse_response(jdata)

        # check _message_archive for old commands - check time reached?
        if self._next_stale_check < time.time():

            # try to lock check routine, fail quickly if already locked = running
            if self._stale_lock.acquire(False):

                # we cannot deny access to self._message_archive as this would block sending
                # instead, copy it and check the copy
                stale_cmds = self._message_archive.copy()
                remove_ids = []
                requeue_cmds = []

                # self._message_archive[message_id] = [time.time(), method, params, repeat]
                self.logger.debug(
                    'Checking for unanswered commands, last check was {} seconds ago, {} commands saved'
                    .format(
                        int(time.time()) - self._last_stale_check,
                        len(self._message_archive)))
                # !! self.logger.debug('Stale commands: {}'.format(stale_cmds))
                for (message_id, (send_time, method, params,
                                  repeat)) in stale_cmds.items():

                    if send_time + self._command_timeout < time.time():

                        # reply timeout reached, check repeat count
                        if repeat <= self._command_repeat:

                            # send again, increase counter
                            self.logger.info(
                                'Repeating unanswered command {} ({}), try {}'.
                                format(method, params, repeat + 1))
                            requeue_cmds.append(
                                [method, params, message_id, repeat + 1])
                        else:
                            self.logger.info(
                                'Unanswered command {} ({}) repeated {} times, giving up.'
                                .format(method, params, repeat))
                            remove_ids.append(message_id)

                for msgid in remove_ids:
                    # it is possible that while processing stale commands, a reply arrived
                    # and the command was removed. So just to be sure, 'try' and delete...
                    self.logger.debug(
                        'Removing stale msgid {} from archive'.format(msgid))
                    try:
                        del self._message_archive[msgid]
                    except KeyError:
                        pass

                # resend pending repeats - after original
                for (method, params, message_id, repeat) in requeue_cmds:
                    self._send_rpc_message(method, params, message_id, repeat)

                # set next stale check time
                self._last_stale_check = time.time()
                self._next_stale_check = self._last_stale_check + self._check_stale_cycle

                del stale_cmds
                del requeue_cmds
                del remove_ids
                self._stale_lock.release()

# !!
            else:
                # !!
                self.logger.debug(
                    'Skipping stale check {} seconds after last check'.format(
                        time.time() - self._last_stale_check))

    def _parse_response(self, data):
        '''
        This method parses (multi-)responses, extracts values and assigns values to assigned items

        :param data: json response data
        :type data: dict

        :return: True if no error was found in response
        :rtype: bool
        '''
        if 'error' in data:
            # errors should already have been logged in on_received_data(), as errors should
            # only occur in reply to commands, so they all have msg_ids. Errors without msg_id
            # will be silently and maliciously ignored
            return False

        query_playerinfo = []
        result_data = data.get('result')

        if 'id' in data:
            response_id = str(data['id'])
            if '#' in response_id:
                response_method = response_id.split('#')[1]
            else:
                response_method = response_id

            # got playerids
            if response_method == 'Player.GetActivePlayers':
                if len(result_data) == 1:
                    # one active player
                    query_playerinfo = self._activeplayers = [
                        result_data[0].get('playerid')
                    ]
                    self._playerid = self._activeplayers[0]
                    self.logger.debug(
                        'Received GetActivePlayers, set playerid to {}'.format(
                            self._playerid))
                    self._set_all_items('player', self._playerid)
                    self._set_all_items(
                        'media', result_data[0].get('type').capitalize())
                    # self._set_all_items('state', 'Playing')
                elif len(result_data) > 1:
                    # multiple active players. Have not yet seen this happen
                    self._activeplayers = []
                    for player in result_data:
                        self._activeplayers.append(player.get('playerid'))
                        query_playerinfo.append(player.get('playerid'))
                    self._playerid = min(self._activeplayers)
                    self.logger.debug(
                        'Received GetActivePlayers, set playerid to {}'.format(
                            self._playerid))
                else:
                    # no active players
                    self._activeplayers = []
                    self._set_all_items('state', 'No active player')
                    self._set_all_items('player', 0)
                    self._set_all_items('title', '')
                    self._set_all_items('media', '')
                    self._set_all_items('stop', True)
                    self._set_all_items('playpause', False)
                    self._set_all_items('streams', None)
                    self._set_all_items('subtitles', None)
                    self._set_all_items('audio', '')
                    self._set_all_items('subtitle', '')
                    self._playerid = 0
                    self.logger.debug(
                        'Received GetActivePlayers, reset playerid to 0')

            # got status info
            elif response_method == 'Application.GetProperties':
                muted = result_data.get('muted')
                volume = result_data.get('volume')
                self.logger.debug(
                    'Received GetProperties: Change mute to {} and volume to {}'
                    .format(muted, volume))
                self._set_all_items('mute', muted)
                self._set_all_items('volume', volume)

            # got favourites
            elif response_method == 'Favourites.GetFavourites':
                if not result_data.get('favourites'):
                    self.logger.debug('No favourites found.')
                else:
                    item_dict = {
                        item['title']: item
                        for item in result_data.get('favourites')
                    }
                    self.logger.debug('Favourites found: {}'.format(item_dict))
                    self._set_all_items('get_favourites', item_dict)

            # got item info
            elif response_method == 'Player.GetItem':
                title = result_data['item'].get('title')
                player_type = result_data['item'].get('type')
                if not title:
                    title = result_data['item'].get('label')
                self._set_all_items('media', player_type.capitalize())
                if player_type == 'audio' and 'artist' in result_data['item']:
                    artist = 'unknown' if len(
                        result_data['item'].get('artist')
                    ) == 0 else result_data['item'].get('artist')[0]
                    title = artist + ' - ' + title
                self._set_all_items('title', title)
                self.logger.debug(
                    'Received GetItem: update player info to title={}, type={}'
                    .format(title, player_type))

            # got player status
            elif response_method == 'Player.GetProperties':
                self.logger.debug(
                    'Received Player.GetProperties, update media data')
                self._set_all_items('speed', result_data.get('speed'))
                self._set_all_items('seek', result_data.get('percentage'))
                self._set_all_items('streams', result_data.get('audiostreams'))
                self._set_all_items('audio',
                                    result_data.get('currentaudiostream'))
                self._set_all_items('subtitles', result_data.get('subtitles'))
                if result_data.get('subtitleenabled'):
                    subtitle = result_data.get('currentsubtitle')
                else:
                    subtitle = 'Off'
                self._set_all_items('subtitle', subtitle)

                # speed != 0 -> play; speed == 0 -> pause
                if result_data.get('speed') == 0:
                    self._set_all_items('state', 'Paused')
                    self._set_all_items('stop', False)
                    self._set_all_items('playpause', False)
                else:
                    self._set_all_items('state', 'Playing')
                    self._set_all_items('stop', False)
                    self._set_all_items('playpause', True)

        elif 'method' in data:
            # no id, notification or other
            if data['method'] == 'Player.OnResume':
                self.logger.debug('Received: resumed player')
                self._set_all_items('state', 'Playing')
                self._set_all_items('stop', False)
                self._set_all_items('playpause', True)
                query_playerinfo.append(
                    data['params']['data']['player']['playerid'])

            elif data['method'] == 'Player.OnPause':
                self.logger.debug('Received: paused player')
                self._set_all_items('state', 'Paused')
                self._set_all_items('stop', False)
                self._set_all_items('playpause', False)
                query_playerinfo.append(
                    data['params']['data']['player']['playerid'])

            elif data['method'] == 'Player.OnStop':
                self.logger.debug(
                    'Received: stopped player, set playerid to 0')
                self._set_all_items('state', 'No active player')
                self._set_all_items('media', '')
                self._set_all_items('title', '')
                self._set_all_items('player', 0)
                self._set_all_items('stop', True)
                self._set_all_items('playpause', False)
                self._set_all_items('streams', None)
                self._set_all_items('subtitles', None)
                self._set_all_items('audio', '')
                self._set_all_items('subtitle', '')
                self._activeplayers = []
                self._playerid = 0

            elif data['method'] == 'GUI.OnScreensaverActivated':
                self.logger.debug('Received: activated screensaver')
                self._set_all_items('state', 'Screensaver')

            elif data['method'] in ['Player.OnPlay', 'Player.OnAVChange']:
                self.logger.debug('Received: started/changed playback')
                self._set_all_items('state', 'Playing')
                query_playerinfo.append(
                    data['params']['data']['player']['playerid'])

            elif data['method'] == 'Application.OnVolumeChanged':
                self.logger.debug(
                    'Received: volume changed, got new values mute: {} and volume: {}'
                    .format(data['params']['data']['muted'],
                            data['params']['data']['volume']))
                self._set_all_items('mute', data['params']['data']['muted'])
                self._set_all_items('volume', data['params']['data']['volume'])

        # if active playerid(s) was changed, update status for active player(s)
        if query_playerinfo:
            self.logger.debug(
                'Player info query requested for playerid(s) {}'.format(
                    query_playerinfo))
            for player_id in query_playerinfo:
                self.logger.debug(
                    'Getting player info for player #{}'.format(player_id))
                self._send_rpc_message('Player.GetItem', {
                    'properties': ['title', 'artist'],
                    'playerid': player_id
                })
                self._send_rpc_message(
                    'Player.GetProperties', {
                        'properties': [
                            'speed', 'percentage', 'currentaudiostream',
                            'audiostreams', 'subtitleenabled',
                            'currentsubtitle', 'subtitles'
                        ],
                        'playerid':
                        player_id
                    })

        return True

    def _set_all_items(self, kodi_item, value):
        '''
        This method sets all items which are registered for kodi_item to value with own caller_id

        :parameter kodi_item: command name from commands.py / item config
        :type kodi_item: str
        :parameter value: value to set for all items
        '''
        for item in self._registered_items[kodi_item]:
            item(value, caller=self.get_shortname())

    def _check_commands_data(self):
        '''
        Method checks consistency of imported commands data

        :return: True if data is consistent
        :rtype: bool
        '''
        no_method = []
        wrong_keys = []
        unmatched = []
        bounds = []
        values = []
        for command, entry in commands.commands.items():
            # verify all keys are present
            if not ['method', 'set', 'get', 'params', 'values', 'bounds'
                    ].sort() == list(entry.keys()).sort():
                wrong_keys.append(command)
            elif not ['set', 'special'].sort() == list(entry.keys()).sort():
                # check that method is not empty
                if not entry['method']:
                    no_method.append(command)
                par = entry['params']
                val = entry['values']
                bnd = entry['bounds']
                # params and values must be either both None or both lists of equal length
                if par is None and val is not None or par is not None and val is None:
                    unmatched.append(command)
                elif par is not None and val is not None and len(par) != len(
                        val):
                    unmatched.append(command)
                vals = 0
                if val is not None:
                    # check that max. one 'VAL' entry is present
                    for item in val:
                        if item == 'VAL':
                            vals += 1
                    if vals > 1:
                        values.append(command)
                # check that bounds are None or list or (tuple and len(bounds)=2)
                if bnd is not None and \
                   not isinstance(bnd, list) and \
                   (not (isinstance(bnd, tuple) and len(bnd) == 2)):
                    bounds.append(command)
                # check that bounds are only defined if 'VAL' is present
                if vals == 0 and bnd is not None:
                    bounds.append(command)

        # found any errors?
        if len(no_method + wrong_keys + unmatched + bounds + values) > 0:
            if len(wrong_keys) > 0:
                self.logger.error('Commands data not consistent: commands "' +
                                  '", "'.join(wrong_keys) +
                                  '" have wrong keys')
            if len(no_method) > 0:
                self.logger.error('Commands data not consistent: commands "' +
                                  '", "'.join(no_method) + '" have no method')
            if len(unmatched) > 0:
                self.logger.error('Commands data not consistent: commands "' +
                                  '", "'.join(unmatched) +
                                  '" have unmatched params/values')
            if len(bounds) > 0:
                self.logger.error('Commands data not consistent: commands "' +
                                  '", "'.join(bounds) +
                                  '" have erroneous bounds')
            if len(values) > 0:
                self.logger.error('Commands data not consistent: commands "' +
                                  '", "'.join(values) +
                                  '" have more than one "VAL" field')

            return False

        macros = []
        for macro, entry in commands.macros.items():
            if not isinstance(entry, list):
                macros.append(macro)
            else:
                for step in entry:
                    if not isinstance(step, list) or len(step) != 2:
                        macros.append(macro)

        if len(macros) > 0:
            self.logger.error('Macro data not consistent for macros "' +
                              '", "'.join(macros) + '"')
            # errors in macro definition don't hinder normal plugin functionality, so just
            # refill self._MACRO omitting erroneous entries. With bad luck, _MACRO is empty ;)
            self._MACRO = {}
            for command, entry in command.macros:
                if command not in macros:
                    self._MACRO[command] = entry

        return True

    def _connect(self, by):
        '''
        Method to try to establish a new connection to Kodi

        :note: While this method is called during the start-up phase of the plugin, it can also be used to establish a connection to the Kodi server if the plugin was initialized before the server went up or the connection is interrupted.

        :param by: caller information
        :type by: str
        '''
        self.logger.debug(
            'Initializing connection, initiated by {}'.format(by))
        if not self._kodi_tcp_connection.connected():
            self._kodi_tcp_connection.connect()
            # we allow for 2 seconds to connect
            time.sleep(2)
        if not self._kodi_tcp_connection.connected():
            # no connection could be established, Kodi may be offline
            self.logger.info(
                'Could not establish a connection to Kodi at {}'.format(
                    self._host))
            self._kodi_server_alive = False
        else:
            self._kodi_server_alive = True
        if self._kodi_server_alive:
            for item in self._registered_items['power']:
                item(True, caller=self.get_shortname())

    def _process_macro(self, item):
        '''
        This method processes macro sequences. Macros can be definded in commands.py
        or dynamically be submitted via the item value.

        :param item: the item refenencing the macro
        :type item: object
        '''
        if item() in self._MACRO:
            # predefined macro
            macro = item()
            macroseq = self._MACRO[macro]
        elif isinstance(item(), list):
            # custom/dynamic macro seq provided as item()
            for step in item():
                if not isinstance(step, list) or len(step) != 2:
                    self.logger.error(
                        'Custom macro "{}" from item {} is not a valid macro sequence'
                        .format(item(), item))
                    return
            macro = '(custom {})'.format(item)
            macroseq = item()
        else:
            self.logger.error(
                'Macro "{}"" not in macro definitions and no valid macro itself'
                .format(item()))
            return

        for [command, value] in macroseq:
            if command == 'wait':
                self.logger.debug('Macro {} waiting for {} second(s)'.format(
                    item(), value))
                time.sleep(value)
            else:
                self.logger.debug(
                    'Macro {} calling command {} with value {}'.format(
                        command, value))
                self._send_command(command, value)
        self.logger.debug('Macro {} finished'.format(item()))

    def _build_command_params(self, command, data):
        '''
        This method validates the data according to the command definitions and creates the parameter dict for the command

        :param command: command to send as defined in _CMD
        :type command: str
        :param data: parameter data to send, format according to command requirements

        :return: parameter for sending
        :rtype: dict
        '''
        if command not in self._CMD:
            self.logger.error('Command unknown: {}'.format(command))
            return False

        if self._CMD[command]['params'] is None:
            return None

        self.logger.debug('Building params set for {}'.format(data))

        cmdset = self._CMD[command]
        cmd_params = cmdset['params']
        cmd_values = cmdset['values']
        cmd_bounds = cmdset['bounds']

        if (isinstance(data, str) and data.isnumeric()):
            data = self._str2num(data)
            if data is None:
                self.logger.error(
                    'Invalid data: value {} is not numeric for command {}'.
                    format(data, command))
                return False

        params = {}
        for idx in range(len(cmd_params)):

            # VAL field
            if cmd_values[idx] == 'VAL':

                # check validity if bounds are given
                if cmd_bounds is not None:
                    if isinstance(cmd_bounds, list):
                        if data not in cmd_bounds:
                            self.logger.debug(
                                'Invalid data: value {} not in list {}'.format(
                                    data, cmd_bounds))
                            return False
                    elif isinstance(cmd_bounds, tuple):
                        if not isinstance(data, type(cmd_bounds[0])):
                            if type(data) is float and type(
                                    cmd_bounds[0]) is int:
                                data = int(data)
                            else:
                                self.logger.error(
                                    'Invalid data: type {} ({}) given for {} bounds {}'
                                    .format(type(data), data,
                                            type(cmd_bounds[0]), cmd_bounds))
                                return False
                        if not cmd_bounds[0] <= data <= cmd_bounds[1]:
                            self.logger.error(
                                'Invalid data: value {} out of bounds ({})'.
                                format(data, cmd_bounds))
                            return False
                params[cmd_params[idx]] = data

            # playerid
            elif cmd_values[idx] == 'ID':
                params[cmd_params[idx]] = self._playerid

            # tuple => eval expression with VAL substituted for data
            elif isinstance(cmd_values[idx], tuple):
                try:
                    expr = str(cmd_values[idx][0]).replace('VAL', str(data))
                    result = eval(expr)
                except Exception as e:
                    self.logger.error(
                        'Invalid data: eval expression {} with argument {} raised error: {}'
                        .format(cmd_values[idx][0], data, e.message))
                    return False
                params[cmd_params[idx]] = result

            # bare value (including list) => just send it
            else:
                params[cmd_params[idx]] = cmd_values[idx]

        self.logger.debug('Built params array {}'.format(params))

        return params

    def _send_command(self, command, data):
        '''
        This method prepares and send the command string to send to Kodi device

        :param command: command to send as defined in _CMD
        :type command: str
        :param data: parameter data to send, format according to command requirements

        :return: True if send succeeded / acknowledged by Kodi
        :rtype: bool
        '''

        if command not in self._CMD:
            self.logger.error('Command unknown: {}'.format(command))
            return False

        params = self._build_command_params(command, data)

        # error occured on param build? success yields dict or None
        if params is False:
            return False

        # !! self.logger.debug('Calling send_rpc method for command {} with method={} and params={}'.format(command, self._CMD[command]['method'], params))
        self._send_rpc_message(self._CMD[command]['method'], params)

    def _send_rpc_message(self,
                          method,
                          params=None,
                          message_id=None,
                          repeat=0):
        '''
        Send a JSON RPC to Kodi.
        The  JSON string is extracted from the supplied method and the given parameters.

        :param method: the Kodi method to be triggered
        :param params: parameters dictionary
        :param message_id: the message ID to be used. If none, use the internal counter
        '''
        self.logger.debug(
            'Preparing message to send method {} with data {}, try #{}, connection is {}'
            .format(method, params, repeat, self._kodi_server_alive))

        if message_id is None:
            # safely acquire next message_id
            # !! self.logger.debug('Locking message id access ({})'.format(self._message_id))
            self._msgid_lock.acquire()
            self._message_id += 1
            new_msgid = self._message_id
            self._msgid_lock.release()
            message_id = str(new_msgid) + '#' + method
            # !! self.logger.debug('Releasing message id access ({})'.format(self._message_id))

        # create message packet
        data = {'jsonrpc': '2.0', 'id': message_id, 'method': method}
        if params:
            data['params'] = params
        try:
            send_command = json.dumps(data, separators=(',', ':'))
        except Exception as err:
            self.logger.error('Problem with json.dumps: {}'.format(err))
            send_command = data

        # push message in queue
        # !! self.logger.debug('Queuing message {}'.format(send_command))
        self._send_queue.put(
            [message_id, send_command, method, params, repeat])
        # !! self.logger.debug('Queued message {}'.format(send_command))

        # try to actually send all queued messages
        # !!
        self.logger.debug('Processing queue - {} elements'.format(
            self._send_queue.qsize()))
        while not self._send_queue.empty():
            (message_id, data, method, params, repeat) = self._send_queue.get()
            self.logger.debug('Sending queued msg {} - {} (#{})'.format(
                message_id, data, repeat))
            self._kodi_tcp_connection.send((data + '\r\n').encode())
            # !! self.logger.debug('Adding cmd to message archive: {} - {} (try #{})'.format(message_id, data, repeat))
            self._message_archive[message_id] = [
                time.time(), method, params, repeat
            ]
            # !! self.logger.debug('Sent msg {} - {}'.format(message_id, data))
        # !! self.logger.debug('Processing queue finished - {} elements remaining'.format(self._send_queue.qsize()))

    def _update_status(self):
        '''
        This method requests several status infos
        '''
        if self.alive:
            if not self._kodi_server_alive:
                self._connect('update')
            else:
                self._send_command('get_status_au', None)
                if self._playerid:
                    self._send_command('get_status_play', None)
                    self._send_command('get_item', None)

    def _str2num(self, s):
        try:
            val = int(s)
            return (val)
        except ValueError:
            try:
                val = float(s)
                return (val)
            except ValueError:
                return None
コード例 #7
0
class LIRC(SmartPlugin):

    ALLOW_MULTIINSTANCE = True
    PLUGIN_VERSION = "1.5.0"

    def __init__(self, smarthome):
        if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5':
            self.logger = logging.getLogger(__name__)
        self._host = self.get_parameter_value('host')
        if self._host is None:
            self._host = self.get_parameter_value('lirc_host')
        self._port = self.get_parameter_value('port')
        self._autoreconnect = self.get_parameter_value('autoreconnect')
        self._connect_retries = self.get_parameter_value('connect_retries')
        self._connect_cycle = self.get_parameter_value('connect_cycle')
        name = 'plugins.' + self.get_fullname()
        self._lirc_tcp_connection = Tcp_client(
            host=self._host,
            port=self._port,
            name=name,
            autoreconnect=self._autoreconnect,
            connect_retries=self._connect_retries,
            connect_cycle=self._connect_cycle,
            binary=True,
            terminator=b'\n')
        self._lirc_tcp_connection.set_callbacks(
            connected=self._on_connect,
            data_received=self._on_received_data,
            disconnected=self._on_disconnect)
        self._cmd_lock = threading.Lock()
        self._reply_lock = threading.Condition()

        self._lircd_version = ''
        self._responseStr = None
        self._parseLine = 0
        self._error = False
        self._lirc_server_alive = False

    def run(self):
        self.alive = True
        self.logger.debug("run method called")
        self._connect('run')

    def stop(self):
        self.logger.debug("stop method called")
        self.alive = False
        self._reply_lock.acquire()
        self._reply_lock.notify()
        self._reply_lock.release()
        if self._cmd_lock.locked():
            self._cmd_lock.release()
        self._lirc_server_alive = False
        self.logger.debug("Threads released")
        self._lirc_tcp_connection.close()
        self.logger.debug("Connection closed")

    def parse_item(self, item):
        if self.has_iattr(item.conf, REMOTE_ATTRS[0]) and \
           self.has_iattr(item.conf, REMOTE_ATTRS[1]):
            self.logger.debug("{}: callback assigned".format(item))
            return self.update_item
        return None

    def _connect(self, by):
        '''
        Method to try to establish a new connection to lirc

        :note: While this method is called during the start-up phase of the plugin, it can also be used to establish a connection to the lirc server if the plugin was initialized before the server went up or the connection is interrupted.

        :param by: caller information
        :type by: str
        '''
        self.logger.debug(
            'Initializing connection to {}:{}, initiated by {}'.format(
                self._host, self._port, by))
        if not self._lirc_tcp_connection.connected():
            self._lirc_tcp_connection.connect()
            # we allow for 2 seconds to connect
            time.sleep(2)
        if not self._lirc_tcp_connection.connected():
            # no connection could be established, lirc may be offline
            self.logger.info(
                'Could not establish a connection to lirc at {}'.format(
                    self._host))
            self._lirc_server_alive = False
        else:
            self._lirc_server_alive = True

    def _on_connect(self, by=None):
        '''
        Recall method for succesful connect to lirc
        On a connect first check if the JSON-RPC API is available.
        If this is the case query lirc to initialize all items

        :param by: caller information
        :type by: str
        '''
        self._lirc_server_alive = True
        if isinstance(by, (dict, Tcp_client)):
            by = 'TCP_Connect'
        self.logger.info('Connected to {}, onconnect called by {}.'.format(
            self._host, by))
        self.request_version()

    def _on_disconnect(self, obj=None):
        '''
        Recall method for TCP disconnect
        '''
        self.logger.info('Received disconnect from {}'.format(self._host))
        self._lirc_server_alive = False

    def _on_received_data(self, connection, response):
        data = response.decode()
        #self.logger.debug("Got: {0}".format(data))
        if data.startswith('BEGIN'):
            return None
        elif data.startswith('END'):
            self._parseLine = 0
            self._reply_lock.acquire()
            self._reply_lock.notify()
            self._reply_lock.release()
            return None
        if self._parseLine >= 0:
            self._parseLine += 1
            if self._parseLine == 1:
                self._responseStr = str(data) + '\n'
            elif self._parseLine == 2:
                if data.startswith('ERROR'):
                    self._error = True
                else:
                    self._error = False
            elif self._parseLine == 3:
                pass  #ignore field DATA
            elif self._parseLine == 4:
                pass  #ignore field n
            else:
                self._responseStr += str(data) + '\n'

    def update_item(self, item, caller=None, source=None, dest=None):
        val = item()
        if val == 0:
            return None
        item(0)
        if val < 0:
            self.logger.warning("ignoring invalid value {}".format(val))
        else:
            remote = self.get_iattr_value(item.conf, REMOTE_ATTRS[0])
            key = self.get_iattr_value(item.conf, REMOTE_ATTRS[1])
            self.logger.debug(
                "update_item {}, val: {}, remote: {}, key: {}".format(
                    item, val, remote, key))
            command = "SEND_ONCE {} {} {}".format(remote, key, val)
            self.logger.debug("command: {}".format(command))
            self._send(command)

    def request_version(self):
        self._lircd_version = self._send("VERSION", True)
        if self._lircd_version:
            self.logger.info("connected to lircd {} on {}:{}".format( \
            self._lircd_version.replace("VERSION\n","").replace("\n",""), \
            self._host,self._port))
            return True
        else:
            self.logger.error("lircd Version not detectable")
            return False

    def _send(self, command, reply=True):
        i = 0
        while not self._lirc_server_alive:
            self.logger.debug(
                "Waiting to send command {} as connection is not yet established. Count: {}/10"
                .format(command, i))
            i += 1
            time.sleep(1)
            if i >= 10:
                self.logger.warning(
                    "10 seconds wait time for sending {} is over. Sending it now."
                    .format(command))
                break
        self._responseStr = None
        self._cmd_lock.acquire()
        self.logger.debug("send command: {}".format(command))
        self._reply_lock.acquire()
        self._lirc_tcp_connection.send(bytes(command + '\n', 'utf-8'))
        if reply:
            self._reply_lock.wait(1)
        self._reply_lock.release()
        self._cmd_lock.release()
        if self._error:
            self.logger.error("error from lircd: {}".format(
                self._responseStr.replace("\n", " ")))
            self._error = False
        elif isinstance(self._responseStr, str):
            self.logger.debug("response: {}".format(
                self._responseStr.replace("\n", " ")))
        return self._responseStr
コード例 #8
0
class MPD(SmartPlugin):

    PLUGIN_VERSION = "1.5.2"
    STATUS = 'mpd_status'
    SONGINFO = 'mpd_songinfo'
    STATISTIC = 'mpd_statistic'
    COMMAND = 'mpd_command'
    URL = 'mpd_url'
    LOCALPLAYLIST = 'mpd_localplaylist'
    RAWCOMMAND = 'mpd_rawcommand'
    DATABASE = 'mpd_database'

    # use e.g. as MPD.STATUS to keep in namespace

    def __init__(self, sh):
        """
        Initalizes the plugin.

        If the sh object is needed at all, the method self.get_sh() should be used to get it.
        There should be almost no need for a reference to the sh object any more.

        Plugins have to use the new way of getting parameter values:
        use the SmartPlugin method get_parameter_value(parameter_name). Anywhere within the Plugin you can get
        the configured (and checked) value for a parameter by calling self.get_parameter_value(parameter_name). It
        returns the value in the datatype that is defined in the metadata.
        """

        # Call init code of parent class (SmartPlugin)
        super().__init__()

        from bin.smarthome import VERSION
        if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5':
            self.logger = logging.getLogger(__name__)

        self.instance = self.get_instance_name()

        self.host = self.get_parameter_value('host')
        self.port = self.get_parameter_value('port')
        self._cycle = self.get_parameter_value('cycle')

        self.terminator = b'\n'
        self._client = Tcp_client(self.host,
                                  self.port,
                                  terminator=self.terminator)
        self._client.set_callbacks(connected=self.handle_connect,
                                   data_received=self.parse_reply)
        self._cmd_lock = threading.Lock()
        self._reply_lock = threading.Condition()
        self._reply = {}

        self._status_items = {}
        self._currentsong_items = {}
        self._statistic_items = {}
        self.orphanItems = []
        self.lastWarnTime = None
        self.warnInterval = 3600  # warn once per hour for orphaned items if some exist
        self._internal_tems = {
            'isPlaying': False,
            'isPaused': False,
            'isStopped': False,
            'isMuted': False,
            'lastVolume': 20,
            'currentName': ''
        }
        self._mpd_statusRequests = [
            'volume', 'repeat', 'random', 'single', 'consume', 'playlist',
            'playlistlength', 'mixrampdb', 'state', 'song', 'songid', 'time',
            'elapsed', 'bitrate', 'audio', 'nextsong', 'nextsongid',
            'duration', 'xfade', 'mixrampdelay', 'updating_db', 'error',
            'playpause', 'mute'
        ]
        self._mpd_currentsongRequests = [
            'file', 'Last-Modified', 'Artist', 'Album', 'Title', 'Name',
            'Track', 'Time', 'Pos', 'Id'
        ]
        self._mpd_statisticRequests = [
            'artists', 'albums', 'songs', 'uptime', 'db_playtime', 'db_update',
            'playtime'
        ]
        # _mpd_playbackCommands and _mpd_playbackOptions are both handled as 'mpd_command'!
        self._mpd_playbackCommands = [
            'next', 'pause', 'play', 'playid', 'previous', 'seek', 'seekid',
            'seekcur', 'stop', 'playpause', 'mute'
        ]
        self._mpd_playbackOptions = [
            'consume', 'crossfade', 'mixrampdb', 'mixrampdelay', 'random',
            'repeat', 'setvol', 'single', 'replay_gain_mode'
        ]
        self._mpd_rawCommand = ['rawcommand']
        self._mpd_databaseCommands = ['update', 'rescan']

    def loggercmd(self, logstr, level):
        if not logstr:
            return
        else:
            logstr = 'MPD_' + self.instance + ': ' + logstr
        if level == 'i' or level == 'info':
            self.logger.info(logstr)
        elif level == 'w' or level == 'warning':
            self.logger.warning(logstr)
        elif level == 'd' or level == 'debug':
            self.logger.debug(logstr)
        elif level == 'e' or level == 'error':
            self.logger.error(logstr)
        else:
            self.logger.critical(logstr)

    def run(self):
        if not self._client.connect():
            self.logger.error(
                f'Connection to {self.host}:{self.port} not possible. Plugin deactivated.'
            )
            return

        self.alive = True
        self.scheduler_add('update_status',
                           self.update_status,
                           cycle=self._cycle)

    def stop(self):
        self.alive = False
        # added to effect better cleanup on stop
        if self.scheduler_get('update_status'):
            self.scheduler_remove('update_status')
        self._client.close()

    def handle_connect(self):
        self.loggercmd("handle_connect", 'd')
        # self.found_terminator = self.parse_reply

    def parse_reply(self, data):
        data = data.decode()
        self.loggercmd("parse_reply => {}".format(data), 'd')
        if data.startswith('OK'):
            self._reply_lock.acquire()
            self._reply_lock.notify()
            self._reply_lock.release()
        elif data.startswith('ACK'):
            self.loggercmd(data, 'e')
        else:
            key, sep, value = data.partition(': ')
            self._reply[key] = value

    def parse_item(self, item):
        # all status-related items here
        if self.get_iattr_value(item.conf,
                                MPD.STATUS) in self._mpd_statusRequests:
            key = (self.get_iattr_value(item.conf, MPD.STATUS))
            self._status_items[key] = item
        if self.get_iattr_value(item.conf,
                                MPD.SONGINFO) in self._mpd_currentsongRequests:
            key = (self.get_iattr_value(item.conf, MPD.SONGINFO))
            self._currentsong_items[key] = item
        if self.get_iattr_value(item.conf,
                                MPD.STATISTIC) in self._mpd_statisticRequests:
            key = (self.get_iattr_value(item.conf, MPD.STATISTIC))
            self._statistic_items[key] = item
        # do not return after status-related items => they can be combined with command-related items
        # all command-related items here
        if self.get_iattr_value(item.conf, MPD.COMMAND) in self._mpd_playbackCommands \
                or self.get_iattr_value(item.conf, MPD.COMMAND) in self._mpd_playbackOptions \
                or self.get_iattr_value(item.conf, MPD.URL) is not None \
                or self.get_iattr_value(item.conf, MPD.LOCALPLAYLIST) is not None \
                or self.get_iattr_value(item.conf, MPD.RAWCOMMAND) in self._mpd_rawCommand \
                or self.get_iattr_value(item.conf, MPD.DATABASE) in self._mpd_databaseCommands:

            self.loggercmd("callback assigned for item {}".format(item), 'd')
            return self.update_item

    def update_status(self):
        # refresh all subscribed items
        warn = self.canWarnNow()
        self.update_statusitems(warn)
        self.update_currentsong(warn)
        self.update_statistic(warn)
        if warn:
            self.lastWarnTime = datetime.datetime.now()
            for warn in self.orphanItems:
                self.loggercmd(warn, 'w')
            self.orphanItems = []

    def update_statusitems(self, warn):
        if not self._client.connected():
            if warn:
                self.loggercmd("update_status while not connected", 'e')
            return
        if (len(self._status_items) <= 0):
            if warn:
                self.loggercmd("status: no items to refresh", 'w')
            return
        self.loggercmd("requesting status", 'd')
        status = self._send('status')
        self.refreshItems(self._status_items, status, warn)

    def update_currentsong(self, warn):
        if not self._client.connected():
            if warn:
                self.loggercmd("update_currentsong while not connected", 'e')
            return
        if (len(self._currentsong_items) <= 0):
            if warn:
                self.loggercmd("currentsong: no items to refresh", 'w')
            return
        self.loggercmd("requesting currentsong", 'd')
        currentsong = self._send('currentsong')
        self.refreshItems(self._currentsong_items, currentsong, warn)

    def update_statistic(self, warn):
        if not self._client.connected():
            if warn:
                self.loggercmd("update_statistic while not connected", 'e')
            return
        if (len(self._statistic_items) <= 0):
            if warn:
                self.loggercmd("statistic: no items to refresh", 'w')
            return
        self.loggercmd("requesting statistic", 'd')
        stats = self._send('stats')
        self.refreshItems(self._statistic_items, stats, warn)

    def refreshItems(self, subscribedItems, response, warn):
        if not self.alive:
            return
        # 1. check response for the internal items and refresh them
        if 'state' in response:
            val = response['state']
            if val == 'play':
                self._internal_tems['isPlaying'] = True
                self._internal_tems['isPaused'] = False
                self._internal_tems['isStopped'] = False
            elif val == 'pause':
                self._internal_tems['isPlaying'] = False
                self._internal_tems['isPaused'] = True
                self._internal_tems['isStopped'] = False
            elif val == 'stop':
                self._internal_tems['isPlaying'] = False
                self._internal_tems['isPaused'] = False
                self._internal_tems['isStopped'] = True
            else:
                self.loggercmd("unknown state: {}".format(val), 'e')
        if 'volume' in response:
            val = float(response['volume'])
            if val <= 0:
                self._internal_tems['isMuted'] = True
            else:
                self._internal_tems['isMuted'] = False
                self._internal_tems['lastVolume'] = val
        if 'Name' in response:
            val = response['Name']
            if val:
                self._internal_tems['currentName'] = val
        # 2. check response for subscribed items and refresh them
        for key in subscribedItems:
            # update subscribed items (if value has changed) which exist directly in the response from MPD
            if key in response:
                val = response[key]
                item = subscribedItems[key]
                if item.type() == 'num':
                    try:
                        val = float(val)
                    except:
                        self.loggercmd("can't parse {} to float".format(val),
                                       'e')
                        continue
                elif item.type() == 'bool':
                    if val == '0':
                        val = False
                    elif val == '1':
                        val = True
                    else:
                        self.loggercmd("can't parse {} to bool".format(val),
                                       'e')
                        continue
                if item() != val:
                    self.loggercmd(
                        "update item {}, old value:{} type:{}, new value:{} type:{}"
                        .format(item, item(), item.type(), val,
                                type(val)), 'd')
                    self.setItemValue(item, val)
            # update subscribed items which do not exist in the response from MPD
            elif key == 'playpause':
                item = subscribedItems[key]
                val = self._internal_tems['isPlaying']
                if item() != val:
                    self.setItemValue(item, val)
            elif key == 'mute':
                item = subscribedItems[key]
                val = self._internal_tems['isMuted']
                if item() != val:
                    self.setItemValue(item, val)
            elif key == 'Artist':
                item = subscribedItems[key]
                val = self._internal_tems['currentName']
                if item() != val:
                    self.setItemValue(item, val)
            # do not reset these items when the tags are missing in the response from MPD
            # that happens while MPD switches the current track!
            elif key in ('volume', 'repeat', 'random'):
                return
            else:
                if warn:
                    self.orphanItems.append(
                        "subscribed item \"{}\" not in response from MPD => consider unsubscribing this item"
                        .format(key))
                # reset orphaned items because MPD does not send some items when they are disabled e.g. mixrampdb, error,...
                # to keep these items consisitend in SHNG set them to "" or 0. Whenever MPD resends values the items will be refreshed in SHNG
                item = subscribedItems[key]
                self.setItemValue(item, None)

    def setItemValue(self, item, value):
        if item.type() == 'str':
            if value is None or not value:
                value = ''
            item(str(value), 'MPD')
        elif item.type() == 'num':
            if value is None:
                value = 0
            item(float(value), 'MPD')
        else:
            item(value, 'MPD')

    def update_item(self, item, caller=None, source=None, dest=None):
        if not self.alive:
            return False
        if caller != 'MPD':
            self.loggercmd("update_item called for item {}".format(item), 'd')
            # playbackCommands
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'next':
                self._send('next')
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'play':
                self._send("play {}".format(item()))
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'pause':
                self._send("pause {}".format(item()))
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'stop':
                self._send('stop')
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'playpause':
                if self._internal_tems['isPlaying']:
                    self._send("pause 1")
                elif self._internal_tems['isPaused']:
                    self._send("pause 0")
                elif self._internal_tems['isStopped']:
                    self._send("play")
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'playid':
                self._send("playid {}".format(item()))
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'previous':
                self._send("previous")
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'seek':
                val = item()
                if val:
                    pattern = re.compile('^\d+[ ]\d+$')
                    if pattern.match(val):
                        self._send("seek {}".format(val))
                        return
                self.loggercmd("ignoring invalid seek value", 'w')
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'seekid':
                val = item()
                if val:
                    pattern = re.compile('^\d+[ ]\d+$')
                    if pattern.match(val):
                        self._send("seekid {}".format(val))
                        return
                self.loggercmd("ignoring invalid seekid value", 'w')
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'seekcur':
                val = item()
                if val:
                    pattern = re.compile('^[+-]?\d+$')
                    if pattern.match(val):
                        self._send("seekcur {}".format(val))
                        return
                self.loggercmd("ignoring invalid seekcur value", 'w')
                return
            if self.get_iattr_value(item.conf,
                                    MPD.COMMAND) == 'mute':  # own-defined item
                if self._internal_tems[
                        'lastVolume'] < 0:  # can be -1 if MPD can't detect the current volume
                    self._internal_tems['lastVolume'] = 20
                self._send("setvol {}".format(
                    int(self._internal_tems['lastVolume']) if self.
                    _internal_tems['isMuted'] else 0))
                return
            # playbackoptions
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'consume':
                self._send("consume {}".format(1 if item() else 0))
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'crossfade':
                self._send("crossfade {}".format(item()))
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'mixrampdb':
                self._send("mixrampdb {}".format(item()))
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'mixrampdelay':
                self._send("mixrampdelay {}".format(item()))
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'random':
                self._send("random {}".format(1 if item() else 0))
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'repeat':
                self._send("repeat {}".format(1 if item() else 0))
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'setvol':
                val = item()
                if val > 100:
                    self.loggercmd("invalid volume => value > 100 => set 100",
                                   'w')
                    val = 100
                elif val < 0:
                    self.loggercmd("invalid volume => value < 0 => set 0", 'w')
                    val = 0
                self._send("setvol {}".format(val))
                return
            if self.get_iattr_value(item.conf, MPD.COMMAND) == 'single':
                self._send("single {}".format(1 if item() else 0))
                return
            if self.get_iattr_value(item.conf,
                                    MPD.COMMAND) == 'replay_gain_mode':
                val = item()
                if val in ['off', 'track', 'album', 'auto']:
                    self._send(
                        "replay_gain_mode {}".format(1 if item() else 0))
                else:
                    self.loggercmd(
                        "ignoring invalid value ({}) for replay_gain_mode".
                        format(val), 'w')
                    pass
                return
            # url
            if self.has_iattr(item.conf, MPD.URL):
                self.play_url(item)
                return
            # localplaylist
            if self.has_iattr(item.conf, MPD.LOCALPLAYLIST):
                self.play_localplaylist(item)
                return
            # rawcommand
            if self.get_iattr_value(item.conf, MPD.RAWCOMMAND) == 'rawcommand':
                self._send(item())
                return
            # database
            if self.get_iattr_value(item.conf, MPD.DATABASE) == 'update':
                command = 'update'
                if item():
                    command = "{} {}".format(command, item())
                self._send(command)
                return
            if self.get_iattr_value(item.conf, MPD.DATABASE) == 'rescan':
                command = 'rescan'
                if item():
                    command = "{} {}".format(command, item())
                self._send(command)
                return

    def canWarnNow(self):
        if self.lastWarnTime is None:
            return True
        if self.lastWarnTime + datetime.timedelta(seconds=self.warnInterval) \
                <= datetime.datetime.now():
            return True
        return False

    def _parse_url(self, url):
        name, sep, ext = url.rpartition('.')
        ext = ext.lower()
        play = []
        if ext in ('m3u', 'pls'):
            content = self.get_sh().tools.fetch_url(url, timeout=4)
            if content is False:
                return play
            content = content.decode()
            if ext == 'pls':
                for line in content.splitlines():
                    if line.startswith('File'):
                        num, tmp, url = line.partition('=')
                        play.append(url)
            else:
                for line in content.splitlines():
                    if line.startswith('http://'):
                        play.append(line)
        else:
            play.append(url)
        return play

    def play_url(self, item):
        url = self.get_iattr_value(item.conf, MPD.URL)
        play = self._parse_url(url)
        if play == []:
            self.loggercmd("no url to add", 'w')
            return
        self._send('clear', False)
        for url in play:
            self._send("add {}".format(url), False)
        self._send('play', False)

    def play_localplaylist(self, item):
        file = self.get_iattr_value(item.conf, MPD.LOCALPLAYLIST)
        if file:
            self._send('clear', False)
            self._send('load {}'.format(file), False)
            self._send('play', False)
        else:
            self.loggercmd("no playlistname to send", 'w')

    def _send(self, command, wait=True):
        if not self.alive:
            self.logger.error('Trying to send data but plugin is not running')
            return None
        self._cmd_lock.acquire()
        self._reply = {}
        self._reply_lock.acquire()
        self.loggercmd("send {} to MPD".format(command), 'd')
        self._client.send((command + '\n').encode())
        if wait:
            self._reply_lock.wait(1)
        self._reply_lock.release()
        reply = self._reply
        self._reply = {}
        self._cmd_lock.release()
        return reply
コード例 #9
0
class Asterisk(SmartPlugin):

    PLUGIN_VERSION = "1.4.0"

    DB = 'ast_db'
    DEV = 'ast_dev'
    BOX = 'ast_box'
    USEREVENT = 'ast_userevent'

    # use e.g. as Asterisk.BOX to keep in namespace

    def __init__(self, sh):
        """
        Initalizes the plugin.

        If you need the sh object at all, use the method self.get_sh() to get it. There should be almost no need for
        a reference to the sh object any more.

        Plugins have to use the new way of getting parameter values:
        use the SmartPlugin method get_parameter_value(parameter_name). Anywhere within the Plugin you can get
        the configured (and checked) value for a parameter by calling self.get_parameter_value(parameter_name). It
        returns the value in the datatype that is defined in the metadata.
        """

        # Call init code of parent class (SmartPlugin)
        super().__init__()

        from bin.smarthome import VERSION
        if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5':
            self.logger = logging.getLogger(__name__)

        # get the parameters for the plugin (as defined in metadata plugin.yaml):
        self.host = self.get_parameter_value('host')
        self.port = self.get_parameter_value('port')
        self.username = self.get_parameter_value('username')
        self.password = self.get_parameter_value('password')

        self.terminator = b'\r\n\r\n'
        self._client = Tcp_client(self.host,
                                  self.port,
                                  terminator=self.terminator)
        self._client.set_callbacks(connected=self.handle_connect,
                                   data_received=self.found_terminator)
        self._init_cmd = {
            'Action': 'Login',
            'Username': self.username,
            'Secret': self.password,
            'Events': 'call,user,cdr'
        }
        self._reply_lock = threading.Condition()
        self._cmd_lock = threading.Lock()
        self._aid = 0
        self._devices = {}
        self._mailboxes = {}
        self._trigger_logics = {}
        self._log_in = lib.log.Log(
            self.get_sh(), 'env.asterisk.log.in',
            ['start', 'name', 'number', 'duration', 'direction'])

    def _command(self, d, reply=True):
        """
        This function sends a command to the Asterisk Server
        """
        if not self._client.connected():
            return
        self._cmd_lock.acquire()
        if self._aid > 100:
            self._aid = 0
        self._aid += 1
        self._reply = None
        self._error = False
        if reply:
            d['ActionID'] = self._aid
        # self.logger.debug("Request {0} - sending: {1}".format(self._aid, d))
        self._reply_lock.acquire()
        #
        # self.send(('\r\n'.join(['{0}: {1}'.format(key, value) for (key, value) in list(d.items())]) + '\r\n\r\n').encode())
        self._client.send(('\r\n'.join([
            '{0}: {1}'.format(key, value) for (key, value) in list(d.items())
        ]) + '\r\n\r\n').encode())
        #
        if reply:
            self._reply_lock.wait(2)
        self._reply_lock.release()
        reply = self._reply
        # self.logger.debug("Request {0} - reply: {1}".format(self._aid, reply))
        error = self._error
        self._cmd_lock.release()
        if error:
            raise Exception(error)
        return reply

    def db_read(self, key):
        """ Read from Asterisk database """
        fam, sep, key = key.partition('/')
        try:
            return self._command({
                'Action': 'DBGet',
                'Family': fam,
                'Key': key
            })
        except Exception:
            self.logger.warning("Asterisk: Problem reading {0}/{1}.".format(
                fam, key))

    def db_write(self, key, value):
        """ Write to Asterisk database """
        fam, sep, key = key.partition('/')
        try:
            return self._command({
                'Action': 'DBPut',
                'Family': fam,
                'Key': key,
                'Val': value
            })
        except Exception as e:
            self.logger.warning(
                "Asterisk: Problem updating {0}/{1} to {2}: {3}.".format(
                    fam, key, value, e))

    def mailbox_count(self, mailbox, context='default'):
        """ get mailbox count tuple """
        try:
            return self._command({
                'Action': 'MailboxCount',
                'Mailbox': mailbox + '@' + context
            })
        except Exception as e:
            self.logger.warning(
                "Asterisk: Problem reading mailbox count {0}@{1}: {2}.".format(
                    mailbox, context, e))
            return (0, 0)

    def call(self, source, dest, context, callerid=None):
        cmd = {
            'Action': 'Originate',
            'Channel': source,
            'Exten': dest,
            'Context': context,
            'Priority': '1',
            'Async': 'true'
        }
        if callerid:
            cmd['Callerid'] = callerid
        try:
            self._command(cmd, reply=False)
        except Exception as e:
            self.logger.warning(
                "Asterisk: Problem calling {0} from {1} with context {2}: {3}."
                .format(dest, source, context, e))

    def hangup(self, hang):
        active_channels = self._command({'Action': 'CoreShowChannels'})
        if active_channels is None:
            active_channels = []
        for channel in active_channels:
            device = self._get_device(channel)
            if device == hang:
                self._command({
                    'Action': 'Hangup',
                    'Channel': channel
                },
                              reply=False)

    def found_terminator(self, data):
        data = data.decode()
        event = {}
        for line in data.splitlines():
            key, sep, value = line.partition(': ')
            event[key] = value
        if 'ActionID' in event:
            aid = int(event['ActionID'])
            if aid != self._aid:
                return  # old request
            if 'Response' in event:
                if event['Response'] == 'Error':
                    self._error = event['Message']
                    self._reply_lock.acquire()
                    self._reply_lock.notify()
                    self._reply_lock.release()
                elif event['Message'] == 'Updated database successfully':
                    self._reply_lock.acquire()
                    self._reply_lock.notify()
                    self._reply_lock.release()
                elif event['Message'] == 'Mailbox Message Count':
                    self._reply = [
                        int(event['OldMessages']),
                        int(event['NewMessages'])
                    ]
                    self._reply_lock.acquire()
                    self._reply_lock.notify()
                    self._reply_lock.release()
        if 'Event' not in event:  # ignore
            return
        if event[
                'Event'] == 'Newchannel':  # or data.startswith('Event: Newstate') ) and 'ChannelStateDesc: Ring' in data:
            device = self._get_device(event['Channel'])
            if device in self._devices:
                self._devices[device](True, 'Asterisk')
        elif event['Event'] == 'Hangup':
            self.scheduler_trigger('Ast.UpDev',
                                   self._update_devices,
                                   by='Asterisk')
        elif event['Event'] == 'CoreShowChannel':
            if self._reply is None:
                self._reply = [event['Channel']]
            else:
                self._reply.append(event['Channel'])
        elif event['Event'] == 'CoreShowChannelsComplete':
            self._reply_lock.acquire()
            self._reply_lock.notify()
            self._reply_lock.release()
        elif event['Event'] == 'UserEvent':
            if 'Source' in event:
                source = event['Source']
            else:
                source = None
            if 'Value' in data:
                value = event['Value']
            else:
                value = None
            if 'Destination' in data:
                destination = event['Destination']
            else:
                destination = None
            if event['UserEvent'] in self._trigger_logics:
                for logic in self._trigger_logics[event['UserEvent']]:
                    logic.trigger('Asterisk', source, value, destination)
        elif event['Event'] == 'DBGetResponse':
            self._reply = event['Val']
            self._reply_lock.acquire()
            self._reply_lock.notify()
            self._reply_lock.release()
        elif event['Event'] == 'MessageWaiting':
            mb = event['Mailbox'].split('@')[0]
            if mb in self._mailboxes:
                if 'New' in event:
                    self._mailboxes[mb](event['New'])
                else:
                    self._mailboxes[mb](0)
        elif event['Event'] == 'Cdr':
            end = self.get_sh().now()
            start = end - datetime.timedelta(seconds=int(event['Duration']))
            duration = event['BillableSeconds']
            if len(event['Source']) <= 4:
                direction = '=>'
                number = event['Destination']
            else:
                direction = '<='
                number = event['Source']
                name = event['CallerID'].split('<')[0].strip('" ')
                self._log_in.add([start, name, number, duration, direction])

    def _update_devices(self):
        active_channels = self._command({'Action': 'CoreShowChannels'})
        if active_channels is None:
            active_channels = []
        active_devices = list(map(self._get_device, active_channels))
        for device in self._devices:
            if device not in active_devices:
                self._devices[device](False, 'Asterisk')

    def _get_device(self, channel):
        channel, s, d = channel.rpartition('-')
        a, b, channel = channel.partition('/')
        return channel

    def parse_item(self, item):
        """
        Default plugin parse_item method. Is called when the plugin is initialized.
        The plugin can, corresponding to its attribute keywords, decide what to do with
        the item in future, like adding it to an internal array for future reference
        :param item:    The item to process.
        :return:        If the plugin needs to be informed of an items change you should return a call back function
                        like the function update_item down below. An example when this is needed is the knx plugin
                        where parse_item returns the update_item function when the attribute knx_send is found.
                        This means that when the items value is about to be updated, the call back function is called
                        with the item, caller, source and dest as arguments and in case of the knx plugin the value
                        can be sent to the knx with a knx write function within the knx plugin.
        """
        if self.has_iattr(item.conf, Asterisk.DEV):
            self._devices[self.get_iattr_value(item.conf, Asterisk.DEV)] = item
        if self.has_iattr(item.conf, Asterisk.BOX):
            self._mailboxes[self.get_iattr_value(item.conf,
                                                 Asterisk.BOX)] = item
        if self.has_iattr(item.conf, Asterisk.DB):
            return self.update_item

    def update_item(self, item, caller=None, source=None, dest=None):
        """
        Item has been updated

        This method is called, if the value of an item has been updated by SmartHomeNG.
        It should write the changed value out to the device (hardware/interface) that
        is managed by this plugin.

        :param item: item to be updated towards the plugin
        :param caller: if given it represents the callers name
        :param source: if given it represents the source
        :param dest: if given it represents the dest
        """
        if self.alive and caller != self.get_shortname():
            self.logger.debug(
                "Update item: {}, item has been changed outside this plugin".
                format(item.id()))
            if self.has_iattr(item.conf, Asterisk.DB):
                value = item()
                if isinstance(value, bool):
                    value = int(item())
                self.db_write(self.get_iattr_value(item.conf, Asterisk.DB),
                              value)

    def parse_logic(self, logic):
        """
        Default plugin parse_logic method
        """
        if Asterisk.USEREVENT in logic.conf:
            event = logic.conf[Asterisk.USEREVENT]
            if event not in self._trigger_logics:
                self._trigger_logics[event] = [logic]
            else:
                self._trigger_logics[event].append(logic)

    def run(self):
        """
        Run method for the plugin
        """
        self.logger.debug("Run method called")
        if self._client.connect():
            self.alive = True
        else:
            self.logger.error(
                f'Connection to {self.host}:{self.port} not possible, plugin not starting'
            )

    def handle_connect(self):
        self._command(self._init_cmd, reply=False)
        for mb in self._mailboxes:
            mbc = self.mailbox_count(mb)
            if mbc is not None:
                self._mailboxes[mb](mbc[1])

    def stop(self):
        """
        Stop method for the plugin
        """
        self.logger.debug("Stop method called")
        if self._client.connected():
            self._client.close()
        self.alive = False
        self._reply_lock.acquire()
        self._reply_lock.notify()
        self._reply_lock.release()
コード例 #10
0
class Squeezebox(SmartPlugin):
    ALLOW_MULTIINSTANCE = False
    PLUGIN_VERSION = "1.4.0"

    def __init__(self, smarthome):
        if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5':
            self.logger = logging.getLogger(__name__)
        try:
            self._host = self.get_parameter_value('host')
            self._port = self.get_parameter_value('port')
            self._autoreconnect = self.get_parameter_value('autoreconnect')
            self._connect_retries = self.get_parameter_value('connect_retries')
            self._connect_cycle = self.get_parameter_value('connect_cycle')
            self._squeezebox_server_alive = False
            self._web_port = self.get_parameter_value('web_port')
            name = 'plugins.' + self.get_fullname()
            self._squeezebox_tcp_connection = Tcp_client(
                host=self._host,
                port=self._port,
                name=name,
                autoreconnect=self._autoreconnect,
                connect_retries=self._connect_retries,
                connect_cycle=self._connect_cycle,
                binary=True,
                terminator=b'\r\n')
            self._squeezebox_tcp_connection.set_callbacks(
                connected=self._on_connect,
                data_received=self._on_received_data,
                disconnected=self._on_disconnect)
            self._val = {}
            self._obj = {}
            self._init_cmds = []
            self._listen = False
        except Exception:
            self._init_complete = False
            return

    def _check_mac(self, mac):
        return re.match("[0-9a-fA-F]{2}([:][0-9a-fA-F]{2}){5}", mac)

    def _resolv_full_cmd(self, item, attr):
        # check if PlayerID wildcard is used
        if self.has_iattr(item.conf[attr], '<playerid>'):
            # try to get from parent object
            parent_item = item.return_parent()
            if (parent_item is not None) and self.has_iattr(
                    parent_item.conf,
                    'squeezebox_playerid') and self._check_mac(
                        self.get_iattr_value(parent_item.conf,
                                             'squeezebox_playerid')):
                item.conf[attr] = item.conf[attr].replace(
                    '<playerid>',
                    self.get_iattr_value(parent_item.conf,
                                         'squeezebox_playerid'))
            else:
                grandparent_item = parent_item.return_parent()
                if (grandparent_item is not None) and self.has_iattr(
                        grandparent_item.conf,
                        'squeezebox_playerid') and self._check_mac(
                            self.get_iattr_value(grandparent_item.conf,
                                                 'squeezebox_playerid')):
                    item.conf[attr] = item.conf[attr].replace(
                        '<playerid>',
                        self.get_iattr_value(grandparent_item.conf,
                                             'squeezebox_playerid'))
                else:
                    grandgrandparent_item = grandparent_item.return_parent()
                    if (grandgrandparent_item is not None) and self.has_iattr(
                            grandgrandparent_item.conf,
                            'squeezebox_playerid') and self._check_mac(
                                self.get_iattr_value(
                                    grandgrandparent_item.conf,
                                    'squeezebox_playerid')):
                        item.conf[attr] = item.conf[attr].replace(
                            '<playerid>',
                            self.get_iattr_value(grandgrandparent_item.conf,
                                                 'squeezebox_playerid'))
                    else:
                        self.logger.warning(
                            "could not resolve playerid for {0} from parent item {1}, neither from grandparent {2} or grandgrandparent {3}"
                            .format(item, parent_item, grandparent_item,
                                    grandgrandparent_item))
                        return None

        return item.conf[attr]

    def parse_item(self, item):
        if self.has_iattr(item.conf, 'squeezebox_recv'):
            cmd = self._resolv_full_cmd(item, 'squeezebox_recv')
            if (cmd is None):
                return None

            self.logger.debug("{0} receives updates by \"{1}\"".format(
                item, cmd))
            if not cmd in self._val:
                self._val[cmd] = {'items': [item], 'logics': []}
            else:
                if not item in self._val[cmd]['items']:
                    self._val[cmd]['items'].append(item)

        if self.has_iattr(item.conf, 'squeezebox_albumart'):
            playerid = self._resolv_full_cmd(item, 'squeezebox_albumart')
            if (playerid is None):
                return None
            url = 'http://{}:{}/music/current/cover.jpg?player={}'.format(
                self._host, self._web_port, playerid)
            item(url, 'LMS', 'parse')
            self.logger.debug("album art item {0} is set to \"{1}\"".format(
                item, url))

        if self.has_iattr(item.conf, 'squeezebox_init'):
            cmd = self._resolv_full_cmd(item, 'squeezebox_init')
            if (cmd is None):
                return None

            self.logger.debug("{0} is initialized by \"{1}\"".format(
                item, cmd))
            if not cmd in self._val:
                self._val[cmd] = {'items': [item], 'logics': []}
            else:
                if not item in self._val[cmd]['items']:
                    self._val[cmd]['items'].append(item)

            if not cmd in self._init_cmds:
                self._init_cmds.append(cmd)

        if self.has_iattr(item.conf, 'squeezebox_send'):
            cmd = self._resolv_full_cmd(item, 'squeezebox_send')
            if (cmd is None):
                return None
            self.logger.debug("{0} is sent to \"{1}\"".format(item, cmd))
            return self.update_item
        else:
            return None

    def parse_logic(self, logic):
        if self.has_iattr(logic.conf, 'squeezebox_playerid'):
            playerid = self.get_iattr_value(logic.conf, 'squeezebox_playerid')
            if not self._check_mac(playerid):
                self.logger.warning("invalid playerid for {0}".format(
                    logic.name))
                return None
        else:
            playerid = 'playerid_not_set'
        if self.has_iattr(logic.conf, 'squeezebox_recv'):
            cmds = self.get_iattr_value(logic.conf, 'squeezebox_recv')
            if isinstance(cmds, str):
                cmds = [
                    cmds,
                ]
            for cmd in cmds:
                cmd = cmd.replace('<playerid>', playerid)
                if not self._check_mac(cmd.split(maxsplit=1)[0]):
                    self.logger.warning(
                        "no valid playerid in \"{}\"".format(cmd))
                    continue
                self.logger.debug("{} will be triggered by \"{}\"".format(
                    logic.name, cmd))
                if not cmd in self._val:
                    self._val[cmd] = {'items': [], 'logics': [logic]}
                else:
                    if not logic in self._val[cmd]['logics']:
                        self._val[cmd]['logics'].append(logic)
        else:
            return None

    def update_item(self, item, caller=None, source=None, dest=None):
        # be careful: as the server echoes ALL comands not using this will
        # result in a loop
        if caller != 'LMS':
            cmd = self._resolv_full_cmd(item, 'squeezebox_send').split()
            if not self._check_mac(cmd[0]):
                self.logger.debug(
                    "Command {0} does not include player_id. Is that on purpose?"
                    .format(cmd))
            value = item()
            if isinstance(value, bool):
                # convert to get '0'/'1' instead of 'True'/'False'
                value = int(value)

            # special handling for bool-types that need other commands or values
            # to behave intuitively
            if isinstance(source, str):
                newsource = source.split(".")[-1:][0]
            else:
                newsource = 'None'
            condition0 = len(cmd) > 1
            condition1 = condition0 and cmd[1] == 'playlist' and cmd[2] in [
                'shuffle', 'repeat'
            ]
            condition2 = condition0 and cmd[1] == 'playlist' and cmd[
                2] == 'shuffle' and caller == 'on_change' and newsource == 'shuffle'
            condition3 = condition0 and cmd[1] == 'playlist' and cmd[
                2] == 'repeat' and caller == 'on_change' and newsource == 'repeat'

            if (len(cmd) >= 2) and not item() and (condition2 or condition3):
                # If shuffle or playlist item got updates by on_change nothing should happen to prevent endless loops
                self.logger.debug(
                    "Command {0} ignored to prevent repeat/shuffle command loops"
                    .format(cmd))
                return
            if (len(cmd) >= 2) and not item():
                if (cmd[1] == 'play'):
                    # if 'play' was set to false, send 'pause' to allow
                    # single-item-operation
                    cmd[1] = 'pause'
                    value = 1
                if condition1:
                    # if a boolean item of [...] was set to false, send '0' to disable the option whatsoever
                    # replace cmd[3], as there are fixed values given and
                    # filling in 'value' is pointless
                    cmd[3] = '0'
            self._send(' '.join(
                urllib.parse.quote(cmd_str.format(value),
                                   encoding='iso-8859-1') for cmd_str in cmd))

    def _send(self, cmd):
        if self._squeezebox_tcp_connection.connected():
            # replace german umlauts
            repl = (('%FC', '%C3%BC'), ('%F6', '%C3%B6'), ('%E4', '%C3%A4'),
                    ('%DC', '%C3%9C'), ('%D6', '%C3%96'), ('%C4', '%C3%84'))
            for r in repl:
                cmd = cmd.replace(*r)
            i = 0
            while not cmd == "listen 1" and self._listen is False:
                self.logger.debug(
                    "Waiting to send command {} as connection is not yet established. Count: {}/10"
                    .format(cmd, i))
                i += 1
                time.sleep(1)
                if i >= 10:
                    self.logger.warning(
                        "10 seconds wait time for sending {} is over. Sending it now."
                        .format(cmd))
                    break
            self.logger.debug("Sending request: {0}".format(cmd))
            self._squeezebox_tcp_connection.send(bytes(cmd + '\r\n', 'utf-8'))
        else:
            self.logger.warning(
                "Can not send because squeezebox is not connected.")

    def _on_received_data(self, connection, response):
        data = [
            urllib.parse.unquote(data_str)
            for data_str in response.decode().split()
        ]

        self.logger.debug("Got: {0}".format(data))

        try:
            if (data[0].lower() == 'listen'):
                value = int(data[1])
                if (value == 1):
                    self.logger.info("Listen-mode enabled")
                    self._listen = True
                    if self._init_cmds != []:
                        self.logger.debug('squeezebox: init read')
                        for cmd in self._init_cmds:
                            self._send(cmd)
                else:
                    self.logger.info(
                        "Listen-mode disabled. The plugin won't receive any info!"
                    )
                    self._listen = False

            if self._check_mac(data[0]):
                if (data[1] == 'time' and
                    (data[2].startswith('+') or data[2].startswith('-'))):
                    self._send(data[0] + ' time ?')
                    return
                elif (data[1] == 'play'):
                    self._update_items_with_data([data[0], 'play', '1'])
                    self._update_items_with_data([data[0], 'stop', '0'])
                    self._update_items_with_data([data[0], 'pause', '0'])
                    self._update_items_with_data([data[0], 'mode', 'play'])
                    self._send(data[0] + ' time ?')
                    # play also overrules mute
                    self._update_items_with_data([data[0], 'mute', '0'])
                    return
                elif (data[1] == 'stop'):
                    self._update_items_with_data([data[0], 'play', '0'])
                    self._update_items_with_data([data[0], 'stop', '1'])
                    self._update_items_with_data([data[0], 'pause', '0'])
                    self._update_items_with_data([data[0], 'mode', 'stop'])
                    self._send(data[0] + ' time ?')
                    return
                elif (data[1] == 'pause'):
                    self._update_items_with_data([data[0], 'play', '0'])
                    self._update_items_with_data([data[0], 'stop', '0'])
                    self._update_items_with_data([data[0], 'pause', '1'])
                    self._update_items_with_data([data[0], 'mode', 'pause'])
                    self._send(data[0] + ' mixer muting ?')
                    self._send(data[0] + ' time ?')
                    return
                elif (data[1] == 'mode'):
                    self._update_items_with_data(
                        [data[0], 'play',
                         str(data[2] == 'play')])
                    self._update_items_with_data(
                        [data[0], 'stop',
                         str(data[2] == 'stop')])
                    self._update_items_with_data(
                        [data[0], 'pause',
                         str(data[2] == 'pause')])
                    self._update_items_with_data([data[0], 'mode', data[2]])
                    # play also overrules mute
                    if (data[2] == 'play'):
                        self._update_items_with_data([data[0], 'mute', '0'])
                    return
                elif ((((data[1] == 'prefset') and (data[2] == 'server')) or
                       (data[1] == 'mixer')) and (data[-2] == 'volume')
                      and data[-1].startswith('-')):
                    # make sure value is always positive - also if muted!
                    self._update_items_with_data([data[0], 'mute', '1'])
                    data[-1] = data[-1][1:]
                elif (data[1] == 'playlist'):
                    if (data[2] == 'play' and data[3] == '1'):
                        self._update_items_with_data([data[0], 'play', '1'])
                        self._update_items_with_data([data[0], 'stop', '0'])
                        self._update_items_with_data([data[0], 'pause', '0'])
                        self._update_items_with_data([data[0], 'mode', 'play'])
                        self._send(data[0] + ' time ?')
                        # play also overrules mute
                        self._update_items_with_data([data[0], 'mute', '0'])
                        return
                    elif (data[2] == 'stop'):
                        self._update_items_with_data([data[0], 'play', '0'])
                        self._update_items_with_data([data[0], 'stop', '1'])
                        self._update_items_with_data([data[0], 'pause', '0'])
                        self._update_items_with_data([data[0], 'mode', 'stop'])
                        self._send(data[0] + ' time ?')
                        return
                    elif (data[2] == 'pause' and data[3] == '1'):
                        self._update_items_with_data([data[0], 'play', '0'])
                        self._update_items_with_data([data[0], 'stop', '0'])
                        self._update_items_with_data([data[0], 'pause', '1'])
                        self._update_items_with_data(
                            [data[0], 'mode', 'pause'])
                        self._send(data[0] + ' mixer muting ?')
                        self._send(data[0] + ' time ?')
                        return
                    elif (data[2] == 'jump') and (len(data) == 4):
                        self._update_items_with_data(
                            [data[0], 'playlist index', data[3]])
                    elif (data[2] == 'name') and (len(data) <= 3):
                        self._update_items_with_data(
                            [data[0], 'playlist name', ''])
                    elif (data[2] == 'loadtracks'):
                        self._send(data[0] + ' playlist name ?')
                    elif (data[2] == 'newsong'):
                        self._send(data[0] + ' mode ?')
                        if (len(data) >= 4):
                            self._update_items_with_data(
                                [data[0], 'title', data[3]])
                        else:
                            self._send(data[0] + ' title ?')
                        if (len(data) >= 5):
                            self._update_items_with_data(
                                [data[0], 'playlist index', data[4]])
                        # trigger reading of other song fields
                        for field in ['genre', 'artist', 'album', 'duration']:
                            self._send(data[0] + ' ' + field + ' ?')
                elif (data[1] in ['genre', 'artist', 'album', 'title'
                                  ]) and (len(data) == 2):
                    # these fields are returned empty so update fails - append
                    # '' to allow update
                    data.append('')
                elif (data[1] in ['duration']) and (len(data) == 2):
                    # these fields are returned empty so update fails - append
                    # '0' to allow update
                    data.append('0')
            # finally check for '?'
            if (data[-1] == '?'):
                return
            self._update_items_with_data(data)
        except Exception as e:
            self.logger.error(
                "exception while parsing \'{0}\'. Exception: {1}".format(
                    data, e))

    def _update_items_with_data(self, data):
        cmd = ' '.join(data_str for data_str in data[:-1])

        if (cmd in self._val):

            for item in self._val[cmd]['items']:
                if re.match("[+-][0-9]+$",
                            data[-1]) and not isinstance(item(), str):
                    data[-1] = int(data[-1]) + item()
                item(data[-1], 'LMS', self._host)
            for logic in self._val[cmd]['logics']:
                logic.trigger('squeezebox', cmd, data[-1])

    def _connect(self, by):
        '''
        Method to try to establish a new connection to squeezebox

        :note: While this method is called during the start-up phase of the plugin, it can also be used to establish a connection to the squeezebox server if the plugin was initialized before the server went up or the connection is interrupted.

        :param by: caller information
        :type by: str
        '''
        self.logger.debug(
            'Initializing connection, initiated by {}'.format(by))
        if not self._squeezebox_tcp_connection.connected():
            self._squeezebox_tcp_connection.connect()
            # we allow for 2 seconds to connect
            time.sleep(2)
        if not self._squeezebox_tcp_connection.connected():
            # no connection could be established, squeezebox may be offline
            self.logger.info(
                'Could not establish a connection to squeezebox at {}'.format(
                    self._host))
            self._squeezebox_server_alive = False
        else:
            self._squeezebox_server_alive = True

    def _on_connect(self, by=None):
        '''
        Recall method for succesful connect to squeezebox
        On a connect first check if the JSON-RPC API is available.
        If this is the case query squeezebox to initialize all items

        :param by: caller information
        :type by: str
        '''
        self._squeezebox_server_alive = True
        if isinstance(by, (dict, Tcp_client)):
            by = 'TCP_Connect'
        self.logger.info('Connected to {}, onconnect called by {}.'.format(
            self._host, by))
        self._send('listen 1')

    def _on_disconnect(self, obj=None):
        '''
        Recall method for TCP disconnect
        '''
        self.logger.info('Received disconnect from {}'.format(self._host))
        self._squeezebox_server_alive = False

    def run(self):
        self.alive = True
        self._connect('run')
        self.logger.debug("run method called")

    def stop(self):
        self.alive = False
        self.logger.debug("stop method called")
        self._squeezebox_tcp_connection.close()
        self._squeezebox_server_alive = False