class Server(Plugin): implements(IZWObserver) implements(ISignalObserver) def __init__(self): self.listener = None CommandHandler.rf433 = RF433(self.context) CommandHandler.context = self.context if TelldusZWave is not None: self.zwave = TelldusZWave(self.context) Application().registerShutdown(self.__stop) self.autoDiscovery = SocketServer.UDPServer(('0.0.0.0', 30303), AutoDiscoveryHandler) self.commandSocket = SocketServer.UDPServer(('0.0.0.0', 42314), CommandHandler) Thread(target=self.__autoDiscoveryStart).start() Thread(target=self.__commandSocketStart).start() def reglistener(self, socket, clientAddress): self.listener = socket self.clientAddress = clientAddress self.sendVersion() @slot('rf433RawData') def rf433RawData(self, data, *args, **kwargs): if 'data' in data: data['data'] = int(data['data'], 16) msg = LiveMessage("RawData") msg.append(data) try: self.listener.sendto(msg.toByteArray(), self.clientAddress) except: # for example if listener isn't set pass def zwaveReady(self): self.sendVersion() def sendVersion(self): if self.zwave is None or not self.zwave.controller.version(): return # nothing or not finished yet if not self.listener: return # No listener registered msg = LiveMessage("zwaveinfo") msg.append({'version': self.zwave.controller.version()}) try: self.listener.sendto(msg.toByteArray(), self.clientAddress) except: # for example if listener isn't set pass def __autoDiscoveryStart(self): self.autoDiscovery.serve_forever() def __commandSocketStart(self): self.commandSocket.serve_forever() def __stop(self): self.autoDiscovery.shutdown() self.commandSocket.shutdown()
class DeviceEventFactory(Plugin): implements(IEventFactory) implements(IDeviceChange) def __init__(self): self.deviceTriggers = [] self.sensorTriggers = [] self.deviceManager = DeviceManager(self.context) def clearAll(self): self.deviceTriggers = [] self.sensorTriggers = [] def createAction(self, type, params, **kwargs): if type == 'device': if 'local' in params and params['local'] == 1: return DeviceAction(manager=self.deviceManager, **kwargs) return None def createCondition(self, type, params, **kwargs): if type == 'device': if 'local' in params and params['local'] == 1: return DeviceCondition(manager=self.deviceManager, **kwargs) return None if type == 'sensor': if 'local' in params and params['local'] == 1: return SensorCondition(manager=self.deviceManager, **kwargs) return None def createTrigger(self, type, **kwargs): if type == 'device': trigger = DeviceTrigger(self, **kwargs) self.deviceTriggers.append(trigger) return trigger if type == 'sensor': trigger = SensorTrigger(self, **kwargs) self.sensorTriggers.append(trigger) return trigger return None def deleteTrigger(self, trigger): if trigger in self.deviceTriggers: self.deviceTriggers.remove(trigger) elif trigger in self.sensorTriggers: self.sensorTriggers.remove(trigger) def sensorValueUpdated(self, device, valueType, value, scale): for trigger in self.sensorTriggers: if trigger.sensorId == device.id(): trigger.triggerSensorUpdate(valueType, value, scale) def stateChanged(self, device, method, statevalue): for trigger in self.deviceTriggers: if trigger.deviceId == device.id() and trigger.method == int( method): trigger.triggered({ 'triggertype': 'device', 'clientdeviceid': device.id(), 'method': int(method) })
class SchedulerEventFactory(Plugin): implements(IEventFactory) implements(ISignalObserver) def __init__(self): self.triggerManager = TimeTriggerManager() self.blockheaterTriggers = [] def clearAll(self): self.triggerManager.clearAll() @staticmethod def createCondition(type, params, **kwargs): # pylint: disable=W0622 del params if type == 'suntime': return SuntimeCondition(**kwargs) elif type == 'time': return TimeCondition(**kwargs) elif type == 'weekdays': return WeekdayCondition(**kwargs) def createTrigger(self, type, **kwargs): # pylint: disable=W0622 if type == 'blockheater': trigger = BlockheaterTrigger( factory=self, manager=self.triggerManager, deviceManager=DeviceManager(self.context), **kwargs ) self.blockheaterTriggers.append(trigger) return trigger if type == 'time': return TimeTrigger(manager=self.triggerManager, **kwargs) if type == 'suntime': return SuntimeTrigger(manager=self.triggerManager, **kwargs) return None def deleteTrigger(self, trigger): if trigger in self.blockheaterTriggers: self.blockheaterTriggers.remove(trigger) def recalcTrigger(self): self.triggerManager.recalcAll() @slot('sensorValueUpdated') def sensorValueUpdated(self, device, valueType, value, __scale): if valueType != Device.TEMPERATURE: return for trigger in self.blockheaterTriggers: if trigger.sensorId == device.id(): trigger.setTemp(value) break
class RemoteSupport(Plugin): implements(ITelldusLiveObserver) @TelldusLive.handler('remotesupport') def __handleCommand(self, msg): data = msg.argument(0).toNative() if data['action'] == 'start': self.start(data['server'], data['username']) def start(self, server, username): client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.WarningPolicy()) try: client.connect(server, 22, username=username, key_filename=resource_filename('remotesupport', 'id_rsa')) except Exception as e: logging.exception(e) return transport = client.get_transport() port = transport.request_port_forward('', 0) TelldusLive(self.context).pushToWeb('remotesupport', 'connected', port) thr = threading.Thread(target=self.waitForConnection, args=(client,transport,)) thr.setDaemon(True) thr.start() def waitForConnection(self, client, transport): chan = transport.accept(60) if chan is None: transport.close() TelldusLive(self.context).pushToWeb('remotesupport', 'disconnected', None) return thr = threading.Thread(target=self.tunnelhandler, args=(client, chan,)) thr.setDaemon(True) thr.start() def tunnelhandler(self, client, chan): sock = socket.socket() try: sock.connect(('localhost', 22)) except Exception as e: logging.exception(e) return while True: r, w, x = select.select([sock, chan], [], [], 3) if sock in r: data = sock.recv(1024) if len(data) == 0: break chan.send(data) if chan in r: data = chan.recv(1024) if len(data) == 0: break sock.send(data) chan.close() sock.close() TelldusLive(self.context).pushToWeb('remotesupport', 'disconnected', None) client.close() client = None
class Group(Plugin): implements(ITelldusLiveObserver) def __init__(self): self.devices = [] self.deviceManager = DeviceManager(self.context) for d in self.deviceManager.retrieveDevices('group'): p = d.params() device = GroupDevice() self.devices.append(device) device.setNodeId(d.id()) device.setParams(p) self.deviceManager.addDevice(device) self.deviceManager.finishedLoading('group') self.live = TelldusLive(self.context) def addDevice(self, name, devices): if type(devices) != list: return device = GroupDevice() device.setName(name) device.setParams({ 'devices': devices }) self.devices.append(device) self.deviceManager.addDevice(device) @TelldusLive.handler('group') def __handleCommand(self, msg): data = msg.argument(0).toNative() action = data['action'] if action == 'addGroup': self.addDevice(data['name'], data['devices']) elif action == 'editGroup': deviceId = data['device'] for device in self.devices: if device.id() == deviceId: device.setParams({ 'devices': data['devices'], }) device.paramUpdated('') break elif action == 'groupInfo': deviceId = data['device'] for device in self.devices: if device.id() == deviceId: params = device.params() params['deviceId'] = deviceId self.live.pushToWeb('group', 'groupInfo', params) return elif action == 'remove': deviceId = data['device'] for device in self.devices: if device.id() == deviceId: self.deviceManager.removeDevice(deviceId) self.devices.remove(device) return
class WebUI(Plugin): implements(IWebRequestHandler, IWebReactHandler) def __init__(self): pass @staticmethod def getReactComponents(): retval = { 'com.telldus.firmware': { 'title': 'Firmware', 'builtin': 'FirmwareSettings', 'tags': ['settings'], }, } return retval @staticmethod def handleRequest(plugin, path, params, **__kwargs): if plugin != 'telldus': return None if path == 'info': try: with open('/etc/distribution') as fd: distribution = fd.readline().strip() except Exception as __error: distribution = '' return WebResponseJson({ 'firmware': { 'version': Board.firmwareVersion(), 'distribution': distribution, }, }) if path == 'setDistribution': if params.get('name', '') not in ['beta', 'stable']: return WebResponseJson({ 'success': False, 'error': 'Invalid distribution' }) retval = os.system('/usr/sbin/telldus-helper distribution %s' % params['name']) if retval == 0: return WebResponseJson({'success': True}) return WebResponseJson({ 'success': False, 'error': 'Could not change the firmware version' }) return None @staticmethod def matchRequest(plugin, path): if plugin == 'telldus' and path in ['info', 'setDistribution']: return True return False
class Knobo(Plugin): implements(IWebReactHandler) @cherrypy.expose def index(self): return "Hello World!" @staticmethod def main(): print("The main")
class Client(Plugin): implements(ISignalObserver) def __init__(self): self.client = mqtt.Client() self.client.on_connect = self.onConnect self.client.on_message = self.onMessage self.client.on_publish = self.onPublish self.client.on_subscribe = self.onSubscribe if self.config('hostname') != '': self.connect() def configWasUpdated(self, key, __value): if key == 'hostname': self.connect() def connect(self): if self.config('username') != '': self.client.username_pw_set(self.config('username'), self.config('password')) self.client.connect_async(self.config('hostname'), self.config('port')) self.client.loop_start() @slot('deviceStateChanged') def onDeviceStateChanged(self, device, state, stateValue, origin=None): del origin self.client.publish( 'telldus/device/%s/state' % (device.id()), json.dumps({ 'state': state, 'stateValue': stateValue, #'origin': origin, })) def onConnect(self, client, userdata, flags, result): pass def onMessage(self, client, userdata, msg): pass def onPublish(self, client, obj, mid): pass @slot('sensorValueUpdated') def onSensorValueUpdated(self, device, valueType, value, scale): self.client.publish( 'telldus/sensor/%s/value' % (device.id()), json.dumps({ 'value': FloatWrapper(value), 'valueType': valueType, 'scale': scale, })) def onSubscribe(self, client, obj, mid, granted_qos): pass
class WebRequestHandler(Plugin): implements(IWebRequestHandler, IWebRequestAuthenticationHandler) def __init__(self): self.store = memstore.MemoryStore() def getTemplatesDirs(self): return [resource_filename('tellduslive', 'web/templates')] def isUrlAuthorized(self, request): return request.session('loggedIn', False) def handleAuthenticationForUrl(self, request): return False def handleRequest(self, plugin, path, params, request, **kwargs): if plugin != 'tellduslive': return None oidconsumer = consumer.Consumer({}, self.store) if path == 'login': try: authrequest = oidconsumer.begin('http://login.telldus.com') except consumer.DiscoveryFailure, exc: logging.error(str(exc[0])) return None # TODO(micke): Error sregRequest = sreg.SRegRequest(required=['fullname', 'email']) authrequest.addExtension(sregRequest) trustRoot = request.base() returnTo = '%s/tellduslive/authorize' % request.base() url = authrequest.redirectURL(trustRoot, returnTo) return WebResponseRedirect(url) if path == 'authorize': url = '%s/tellduslive/authorize' % request.base() info = oidconsumer.complete(params, url) displayIdentifier = info.getDisplayIdentifier() if info.status == consumer.FAILURE and displayIdentifier: return None # TODO(micke): Error elif info.status == consumer.SUCCESS: sregResp = sreg.SRegResponse.fromSuccessResponse(info) data = dict(sregResp.items()) if 'email' not in data: return None # TODO(micke): Error tellduslive = TelldusLive(self.context) if data['email'] != tellduslive.email: return 'loginFailed.html', { 'reason': 1, 'loginEmail': data['email'], 'registeredEmail': tellduslive.email } request.setSession('loggedIn', True) return request.loggedIn() else: return None # TODO(micke): Error return None
class Webinterface(Plugin): implements(IWebReactHandler) @staticmethod def getReactComponents(): return { 'webinterface': { 'title': 'Webinterface example', 'script': 'webinterface/welcome.js', 'tags': ['menu'], } }
class React(Plugin): implements(IWebRequestHandler) observers = ObserverCollection(IWebReactHandler) def __init__(self): pass def components(self): retval = {} for o in self.observers: components = o.getReactComponents() if type(components) != dict: continue # Make sure defaults exists for name in components: tags = components[name].setdefault('tags', []) if 'menu' in tags: components[name].setdefault('path', '/%s' % name) retval.update(components) return retval def getTemplatesDirs(self): return [resource_filename('telldus', 'templates')] def handleRequest(self, plugin, path, params, request): if path == '' and plugin == '': return WebResponseHtml('react.html') if plugin == 'telldus': if path == 'reactComponents': return WebResponseJson(self.components()) if path in ['settings']: return WebResponseHtml('react.html') fullPath = '/%s/%s' % (plugin, path) if path is not '' else '/%s' % plugin components = self.components() for name in components: if components[name].get('path', None) == fullPath: return WebResponseHtml('react.html') return None def matchRequest(self, plugin, path): if path == '' and plugin == '': return True if plugin == 'telldus' and path in ['reactComponents', 'settings']: return True # Check if we match a react route fullPath = '/%s/%s' % (plugin, path) if path is not '' else '/%s' % plugin components = self.components() for name in components: if components[name].get('path', None) == fullPath: return True return False
class WebRequestHandler(Plugin): """Default handler for the /web subpath""" authObservers = ObserverCollection(IWebRequestAuthenticationHandler) implements(IWebRequestHandler) @staticmethod def getTemplatesDirs(): return [resource_filename('web', 'templates')] def handleRequest(self, plugin, path, __params, **__kwargs): if plugin != 'web': return None if path == 'authFailed': return 'authFailed.html', {} if path == 'login': providers = [] for observer in self.authObservers: provider = observer.loginProvider() if provider is not None: providers.append(provider) if len(providers) == 1: # Only one provider. Use it raise cherrypy.HTTPRedirect(providers[0]['url']) return 'login.html', {'providers': providers} return None def isUrlAuthorized(self, request): if len(self.authObservers) == 0: raise cherrypy.HTTPRedirect( '/web/authFailed?reason=noAuthHandlersConfigured') for observer in self.authObservers: ret = observer.isUrlAuthorized(request) if ret is True: return True request.setSession( 'returnTo', '%s?%s' % (cherrypy.request.path_info, cherrypy.request.query_string)) raise cherrypy.HTTPRedirect('/web/login') @staticmethod def matchRequest(plugin, path): if plugin != 'web': return False if path in ['authFailed', 'login']: return True return False @staticmethod def requireAuthentication(plugin, __path): return plugin != 'web'
class Led(Plugin): implements(ITelldusLiveObserver) def __init__(self): self.gpio = Gpio(self.context) self.live = TelldusLive(self.context) self.gpio.initPin('status:red') self.gpio.initPin('status:green') self.setNetworkLed() def liveConnected(self): self.setNetworkLed() def liveRegistered(self, __msg, __refreshRequired): self.setNetworkLed() def liveDisconnected(self): self.setNetworkLed() def setNetworkLed(self): if self.live.isRegistered(): # We check live status first since we might have connection on another network interface self.gpio.setPin('status:red', 0) self.gpio.setPin('status:green', 1, brightness=50) return if self.live.isConnected(): self.gpio.setPin('status:red', 0) self.gpio.setPin('status:green', 1, brightness=50, freq=1) return if Led.__getIp(Board.networkInterface()) is None: self.gpio.setPin('status:red', 1, freq=1) self.gpio.setPin('status:green', 0) return self.gpio.setPin('status:red', 1, brightness=50) self.gpio.setPin('status:green', 0) @staticmethod def __getIp(iface): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sockfd = sock.fileno() SIOCGIFADDR = 0x8915 # pylint:disable=C0103 ifreq = struct.pack('16sH14s', str(iface), socket.AF_INET, '\x00' * 14) try: res = fcntl.ioctl(sockfd, SIOCGIFADDR, ifreq) except Exception as __error: return None ipAddr = struct.unpack('16sH2x4s8x', res)[2] return socket.inet_ntoa(ipAddr)
class React(Plugin): implements(IWebRequestHandler) observers = ObserverCollection(IWebReactHandler) def __init__(self): pass def getTemplatesDirs(self): return [resource_filename('telldus', 'templates')] def handleRequest(self, plugin, path, params, request): if path == '' and plugin == '': return WebResponseHtml('react.html') if plugin == 'telldus': if path == 'reactPlugins': return WebResponseJson(self.routes()) fullPath = '/%s/%s' % (plugin, path) if path is not '' else '/%s' % plugin for route in self.routes(): if route['path'] == fullPath: return WebResponseHtml('react.html') return None def matchRequest(self, plugin, path): if path == '' and plugin == '': return True if plugin == 'telldus' and path in ['reactPlugins']: return True # Check if we match a react route fullPath = '/%s/%s' % (plugin, path) if path is not '' else '/%s' % plugin for route in self.routes(): if route['path'] == fullPath: return True return False def routes(self): plugins = [] for o in self.observers: routes = o.getReactRoutes() if type(routes) != list: continue for route in routes: if 'path' not in route: route['path'] = '/%s' % route['name'] plugins.extend(routes) return plugins
class Developer(Plugin): implements(IWebRequestAuthenticationHandler) def __init__(self): self.running = True self.mtimes = {} self.thread = Thread(target=self.run).start() signal.signal(signal.SIGUSR1, self.debugshell) # Register handler Application().registerShutdown(self.stop) def checkModifiedFiles(self): for filename in self.sysfiles(): oldtime = self.mtimes.get(filename) try: mtime = os.stat(filename).st_mtime except OSError: # Probably deleted mtime = None if oldtime is None: # First check self.mtimes[filename] = mtime elif mtime is None or mtime > oldtime: # File was changed or deleted print("Restarting because %s changed." % filename) Application().quit() def debugshell(self, sig, frame): """Interrupt running process, and provide a python prompt for interactive debugging.""" d={'_frame':frame} # Allow access to frame object. d.update(frame.f_globals) # Unless shadowed by global d.update(frame.f_locals) i = code.InteractiveConsole(d) message = "Signal received : entering python shell.\nTraceback:\n" message += ''.join(traceback.format_stack(frame)) i.interact(message) def isUrlAuthorized(self, request): return True def run(self): while self.running: try: self.checkModifiedFiles() except Exception as e: exc_type, exc_value, exc_traceback = sys.exc_info() print(e) for f in traceback.extract_tb(exc_traceback): print(f) time.sleep(1) def stop(self): self.running = False def sysfiles(self): files = set() for k, m in list(sys.modules.items()): if hasattr(m, '__loader__') and hasattr(m.__loader__, 'archive'): f = m.__loader__.archive else: f = getattr(m, '__file__', None) if f is not None and not os.path.isabs(f): # ensure absolute paths so a os.chdir() in the app # doesn't break me f = os.path.normpath(os.path.join(_module__file__base, f)) if f is not None: files.add(f) if f.endswith('.pyc'): f = f[:-1] if os.path.exists(f): files.add(f) return files
class WebFrontend(Plugin): implements(IWebRequestHandler) implements(IWebReactHandler) implements(ITelldusLiveObserver) def getReactComponents(self): # pylint: disable=R0201 return { 'plugins': { 'title': 'Plugins (beta)', 'script': 'pluginloader/plugins.js', 'tags': ['menu'] }, 'plugins/oauth2': { 'script': 'pluginloader/oauth2.js' } } def matchRequest(self, plugin, path): # pylint: disable=R0201 if plugin != 'pluginloader': return False if path in [ 'oauth2', 'icon', 'import', 'importkey', 'installStorePlugin', 'keys', 'reboot', 'refreshStorePlugins', 'remove', 'plugins', 'saveConfiguration', 'storePlugins', 'upload' ]: return True return False def handleOauth2Request(self, params, request): plugin = params['pluginname'] pluginClass = params['pluginclass'] pluginConfig = params['config'] configuration = Loader(self.context).configurationForPlugin( plugin, pluginClass, pluginConfig) if not configuration: return WebResponseJson({ 'success': False, 'msg': 'Configuration not found' }) if 'code' not in params: redirectUri = params['redirectUri'] if not hasattr(configuration, 'activate'): return WebResponseJson({ 'success': False, 'msg': 'Configuration cannot be activated' }) url = configuration.activate(redirectUri) return WebResponseJson({'success': True, 'url': url}) params = configuration.activateCode(params['code']) try: config = {pluginClass: {pluginConfig: params}} Loader(self.context).saveConfiguration(plugin, config) except Exception as error: return WebResponseJson({'success': False, 'msg': str(error)}) return WebResponseRedirect('%s/plugins?settings=%s' % (request.base(), plugin)) def handleRequest(self, plugin, path, params, request, **kwargs): del kwargs if path == 'oauth2': return self.handleOauth2Request(params, request) if path == 'icon': for plugin in Loader(self.context).plugins: if plugin.name != params['name']: continue return WebResponseLocalFile('%s/%s' % (plugin.path, plugin.icon)) return None if path == 'import': filename = '%s/staging.zip' % (Board.pluginPath()) if os.path.isfile(filename): try: return WebResponseJson( Loader(self.context).importPlugin(filename)) except ImportError as error: os.unlink('%s/staging.zip' % (Board.pluginPath())) return WebResponseJson({ 'success': False, 'msg': 'Error importing plugin: %s' % error }) return WebResponseJson({ 'success': False, 'msg': 'Error importing plugin: No plugin uploaded' }) if path == 'importkey': if 'discard' in params: os.unlink('%s/staging.zip' % (Board.pluginPath())) return WebResponseJson({'success': True}) return WebResponseJson( Loader(self.context).importKey(params['key'] if 'key' in params else None)) if path == 'installStorePlugin': if 'pluginname' not in params: return WebResponseJson({ 'success': False, 'msg': 'No plugin specified' }) return WebResponseJson( self.installStorePlugin(params['pluginname'])) if path == 'reboot': retval = os.system('/usr/sbin/telldus-helper reboot') if retval == 0: return WebResponseJson({'success': True}) return WebResponseJson({'success': False}) if path == 'refreshStorePlugins': Loader(self.context).updatePluginsList() return WebResponseJson({'success': True}) if path == 'remove': if 'pluginname' in params: Loader(self.context).removePlugin(params['pluginname']) elif 'key' in params and 'fingerprint' in params: Loader(self.context).removeKey(params['key'], params['fingerprint']) else: return WebResponseJson({ 'success': False, 'msg': 'No plugin or key specified' }) return WebResponseJson({'success': True}) if path == 'plugins': return WebResponseJson([ plugin.infoObject() for plugin in Loader(self.context).plugins ]) if path == 'keys': return WebResponseJson([{ 'uids': key['uids'], 'keyid': key['keyid'], 'fingerprint': key['fingerprint'] } for key in Loader(self.context).keys]) if path == 'saveConfiguration' and request.method() == 'POST': plugin = params['pluginname'] configuration = json.loads(params['configuration']) try: Loader(self.context).saveConfiguration(plugin, configuration) except Exception as error: return WebResponseJson({'success': False, 'msg': str(error)}) return WebResponseJson({'success': True}) if path == 'storePlugins': if not os.path.exists('%s/plugins.yml' % Board.pluginPath()): return WebResponseJson([]) return WebResponseJson(self.storePlugins()) if path == 'upload' and request.method() == 'POST': self.uploadPlugin(params['pluginfile']) filename = '%s/staging.zip' % (Board.pluginPath()) try: return WebResponseJson( Loader(self.context).importPlugin(filename)) except ImportError as error: os.unlink(filename) return WebResponseJson({'success': False, 'msg': str(error)}) def installStorePlugin(self, pluginName): for plugin in yaml.safe_load( open('%s/plugins.yml' % Board.pluginPath(), 'r').read()): if plugin['name'] == pluginName: Loader(self.context).installRemotePlugin( plugin['name'], plugin['file']['url'], plugin['file']['size'], plugin['file']['sha1']) return {'success': True} return {'success': False, 'msg': 'Plugin was not found in the store'} @TelldusLive.handler('plugins') def __webMessage(self, msg): live = TelldusLive(self.context) # pylint: disable=too-many-function-args data = msg.argument(0).toNative() print("Store", data) loader = Loader(self.context) if data['action'] == 'getState': live.pushToWeb( 'plugins', 'state', { 'plugins': [plugin.infoObject() for plugin in loader.plugins], 'suggested': list(loader.suggestedPlugins), }) return if data['action'] == 'getStorePlugins': live.pushToWeb('plugins', 'storePlugins', self.storePlugins()) return if data['action'] == 'install': live.pushToWeb('plugins', 'installStatus', self.installStorePlugin(data.get('plugin'))) return @staticmethod def storePlugins(): return [{ 'author': plugin['author'], 'author-email': plugin['author-email'], 'category': plugin.get('category', 'other'), 'color': plugin.get('color', None), 'name': plugin['name'], 'icon': plugin['icon'] if 'icon' in plugin else '', 'description': plugin['description'], 'size': plugin['file']['size'], 'version': plugin['version'], } for plugin in yaml.load( open('%s/plugins.yml' % Board.pluginPath(), 'r').read())] @staticmethod def uploadPlugin(fileobject): with open('%s/staging.zip' % (Board.pluginPath()), 'w') as fd: fd.write(fileobject.file.read())
class Scheduler(Plugin): implements(ITelldusLiveObserver, IDeviceChange) def __init__(self): self.running = False #self.runningJobsLock = threading.Lock() #TODO needed? self.jobsLock = threading.Lock() self.maintenanceJobsLock = threading.Lock() self.maintenanceJobs = [] self.lastMaintenanceJobId = 0 self.runningJobs = {} #id:s as keys self.settings = Settings('telldus.scheduler') Application().registerShutdown(self.stop) Application().registerMaintenanceJobHandler( self.addMaintenanceJobGeneric) self.timezone = self.settings.get('tz', 'UTC') self.latitude = self.settings.get('latitude', '55.699592') self.longitude = self.settings.get('longitude', '13.187836') self.jobs = [] self.fetchLocalJobs() self.live = TelldusLive(self.context) self.deviceManager = DeviceManager(self.context) if self.live.isRegistered(): #probably not practically possible to end up here self.requestJobsFromServer() self.thread = threading.Thread(target=self.run) self.thread.start() def addMaintenanceJobGeneric(self, job): self.addMaintenanceJob(job['nextRunTime'], job['callback'], job['recurrence']) def addMaintenanceJob(self, nextRunTime, timeoutCallback, recurrence=0): """ nextRunTime - GMT timestamp, timeoutCallback - the method to run, recurrence - when to repeat it, in seconds Returns: An id for the newly added job (for removal and whatnot) Note, if the next nextRunTime needs to be calculated, it's better to do that in the callback-method, and add a new job from there, instead of using "recurrence" """ jobData = { 'nextRunTime': nextRunTime, 'callback': timeoutCallback, 'recurrence': recurrence } with self.maintenanceJobsLock: self.lastMaintenanceJobId = self.lastMaintenanceJobId + 1 jobData[ 'id'] = self.lastMaintenanceJobId # add an ID, make it possible to remove it someday self.maintenanceJobs.append(jobData) self.maintenanceJobs.sort( key=lambda jobData: jobData['nextRunTime']) return self.lastMaintenanceJobId def calculateJobs(self, jobs): """Calculate nextRunTime for all jobs in the supplied list, order it and assign it to self.jobs""" newJobs = [] for job in jobs: self.checkNewlyLoadedJob(job) if self.calculateNextRunTime(job): newJobs.append(job) newJobs.sort(key=lambda job: job['nextRunTime']) with self.jobsLock: self.jobs = newJobs def calculateNextRunTime(self, job): """Calculates nextRunTime for a job, depending on time, weekday and timezone.""" if not job['active'] or not job['weekdays']: job['nextRunTime'] = 253402214400 #set to max value, only run just before the end of time # just delete the job, until it's possible to edit schedules locally, inactive jobs has # no place at all here self.deleteJob(job['id']) return False today = datetime.now(timezone(self.timezone)).weekday() # normalize? weekdays = [int(n) for n in job['weekdays'].split(',')] runToday = False firstWeekdayToRun = None nextWeekdayToRun = None runDate = None for weekday in weekdays: weekday = weekday - 1 #weekdays in python: 0-6, weekdays in our database: 1-7 if weekday == today: runToday = True elif today < weekday and (nextWeekdayToRun is None or nextWeekdayToRun > weekday): nextWeekdayToRun = weekday elif today > weekday and (firstWeekdayToRun is None or weekday < firstWeekdayToRun): firstWeekdayToRun = weekday todayDate = datetime.now(timezone(self.timezone)).date() # normalize? if runToday: #this weekday is included in the ones that this schedule should be run on runTimeToday = self.calculateRunTimeForDay(todayDate, job) if runTimeToday > time.time(): job['nextRunTime'] = runTimeToday + random.randint( 0, job['random_interval']) * 60 return True elif len(weekdays) == 1: #this job should only run on this weekday, since it has already passed today, run it next week runDate = todayDate + timedelta(days=7) if not runDate: if nextWeekdayToRun is not None: runDate = self.calculateNextWeekday(todayDate, nextWeekdayToRun) else: runDate = self.calculateNextWeekday(todayDate, firstWeekdayToRun) if not runDate: #something is wrong, no weekday to run job['nextRunTime'] = 253402214400 # just delete the job, until it's possible to edit schedules locally, inactive jobs # has no place at all here self.deleteJob(job['id']) return False job['nextRunTime'] = self.calculateRunTimeForDay(runDate, job) \ + random.randint(0, job['random_interval']) * 60 return True @staticmethod def calculateNextWeekday(todayDate, weekday): days_ahead = weekday - todayDate.weekday() if days_ahead <= 0: # Target day already happened this week days_ahead += 7 return todayDate + timedelta(days_ahead) def calculateRunTimeForDay(self, runDate, job): """ Calculates and returns a timestamp for when this job should be run next. Takes timezone into consideration. """ runDate = datetime(runDate.year, runDate.month, runDate.day) if job['type'] == 'time': # TODO, sending timezone from the server now, but it's really a client setting, can I # get it from somewhere else? tzone = timezone(self.timezone) # won't random here, since this time may also be used to see if it's passed today or not runDate = runDate + timedelta(hours=job['hour'], minutes=job['minute']) # returning a timestamp, corrected for timezone settings return timegm(tzone.localize(runDate).utctimetuple()) elif job['type'] == 'sunrise': sunCalc = SunCalculator() riseSet = sunCalc.nextRiseSet(timegm(runDate.utctimetuple()), float(self.latitude), float(self.longitude)) return riseSet['sunrise'] + job['offset'] * 60 elif job['type'] == 'sunset': sunCalc = SunCalculator() riseSet = sunCalc.nextRiseSet(timegm(runDate.utctimetuple()), float(self.latitude), float(self.longitude)) return riseSet['sunset'] + job['offset'] * 60 def checkNewlyLoadedJob(self, job): """Checks if any of the jobs (local or initially loaded) should be running right now""" if not job['active'] or not job['weekdays']: return weekdays = [int(n) for n in job['weekdays'].split(',')] i = 0 while i < 2: #Check today and yesterday (might be around 12 in the evening) currentDate = date.today() + timedelta(days=-i) if (currentDate.weekday() + 1) in weekdays: #check for this day (today or yesterday) runTime = self.calculateRunTimeForDay(currentDate, job) runTimeMax = runTime + job['reps'] * 3 \ + job['retry_interval'] * 60 * (job['retries'] + 1) \ + 70 \ + job['random_interval'] * 60 jobId = job['id'] executedJobs = self.settings.get('executedJobs', {}) if (str(jobId) not in executedJobs or executedJobs[str(jobId)] < runTime) \ and time.time() > runTime \ and time.time() < runTimeMax: # run time for this job was passed during downtime, but it was passed within the # max-runtime, and the last time it was executed (successfully) was before this # run time, so it should be run again... jobCopy = copy.deepcopy(job) jobCopy['originalRepeats'] = job['reps'] jobCopy['nextRunTime'] = runTime jobCopy[ 'maxRunTime'] = runTimeMax #approximate maxRunTime, sanity check self.runningJobs[jobId] = jobCopy return i = i + 1 def deleteJob(self, jobId): with self.jobsLock: # Test this! It should be fast and keep original reference, they say (though it will # iterate all, even if it could end after one) self.jobs[:] = [x for x in self.jobs if x['id'] != jobId] if jobId in self.runningJobs: #TODO this might require a lock too? self.runningJobs[jobId]['retries'] = 0 executedJobs = self.settings.get('executedJobs', {}) if str(jobId) in executedJobs: del executedJobs[str(jobId)] self.settings['executedJobs'] = executedJobs def deviceRemoved(self, deviceId): jobsToDelete = [] for job in self.jobs: if job['id'] == deviceId: jobsToDelete.append[job['id']] for jobId in jobsToDelete: self.deleteJob(jobId) def fetchLocalJobs(self): """Fetch local jobs from settings""" try: jobs = self.settings.get('jobs', []) except ValueError: jobs = [ ] #something bad has been stored, just ignore it and continue? print "WARNING: Could not fetch schedules from local storage" self.calculateJobs(jobs) def liveRegistered(self, msg): if 'latitude' in msg: self.latitude = msg['latitude'] if 'longitude' in msg: self.longitude = msg['longitude'] if 'tz' in msg: self.timezone = msg['tz'] self.requestJobsFromServer() @TelldusLive.handler('scheduler-remove') def removeOneJob(self, msg): if len(msg.argument(0).toNative()) != 0: scheduleDict = msg.argument(0).toNative() jobId = scheduleDict['id'] self.deleteJob(jobId) self.settings['jobs'] = self.jobs #save to storage self.live.pushToWeb('scheduler', 'removed', jobId) @TelldusLive.handler('scheduler-report') def receiveJobsFromServer(self, msg): """Receive list of jobs from server, saves to settings and calculate nextRunTimes""" if len(msg.argument(0).toNative()) == 0: jobs = [] else: scheduleDict = msg.argument(0).toNative() jobs = scheduleDict['jobs'] self.settings['jobs'] = jobs self.calculateJobs(jobs) @TelldusLive.handler('scheduler-update') def receiveOneJobFromServer(self, msg): """Receive one job from server, add or edit, save to settings and calculate nextRunTime""" if len(msg.argument(0).toNative()) == 0: jobs = [] else: scheduleDict = msg.argument(0).toNative() job = scheduleDict['job'] active = self.calculateNextRunTime(job) self.deleteJob( job['id']) #delete the job if it already exists (update) if active: with self.jobsLock: self.jobs.append(job) self.jobs.sort(key=lambda job: job['nextRunTime']) self.settings['jobs'] = self.jobs #save to storage # TODO is this a good idea? Trying to avoid cache problems where updates haven't come through? # But this may not work if the same schedule is saved many times in a row, or if changes # wasn't saved correctly to the database (not possible yet, only one database for schedules) # self.live.pushToWeb('scheduler', 'updated', job['id']) def requestJobsFromServer(self): self.live.send(LiveMessage("scheduler-requestjob")) def run(self): self.running = True while self.running: maintenanceJob = None with self.maintenanceJobsLock: if len( self.maintenanceJobs ) > 0 and self.maintenanceJobs[0]['nextRunTime'] < time.time(): maintenanceJob = self.maintenanceJobs.pop(0) self.runMaintenanceJob(maintenanceJob) jobCopy = None with self.jobsLock: if len(self.jobs ) > 0 and self.jobs[0]['nextRunTime'] < time.time(): #a job has passed its nextRunTime job = self.jobs[0] jobId = job['id'] jobCopy = copy.deepcopy( job) #make a copy, don't edit the original job if jobCopy: jobCopy['originalRepeats'] = job['reps'] # approximate maxRunTime, sanity check jobCopy['maxRunTime'] = jobCopy['nextRunTime'] \ + jobCopy['reps'] * 3 \ + jobCopy['retry_interval'] * 60 * (jobCopy['retries'] + 1) \ + 70 \ + jobCopy['random_interval'] * 60 self.runningJobs[jobId] = jobCopy self.calculateNextRunTime(job) with self.jobsLock: self.jobs.sort(key=lambda job: job['nextRunTime']) jobsToRun = [ ] # jobs to run in a separate list, to avoid deadlocks (necessary?) # Iterating using .keys(9 since we are modifiyng the dict while iterating for runningJobId in self.runningJobs.keys(): # pylint: disable=C0201 runningJob = self.runningJobs[runningJobId] if runningJob['nextRunTime'] < time.time(): if runningJob['maxRunTime'] > time.time(): if 'client_device_id' not in runningJob: print "Missing client_device_id, this is an error, perhaps refetch jobs? " print runningJob continue device = self.deviceManager.device( runningJob['client_device_id']) if not device: print "Missing device, b: " + str( runningJob['client_device_id']) continue if device.typeString( ) == '433' and runningJob['originalRepeats'] > 1: #repeats for 433-devices only runningJob['reps'] = int(runningJob['reps']) - 1 if runningJob['reps'] >= 0: runningJob['nextRunTime'] = time.time() + 3 jobsToRun.append(runningJob) continue if runningJob['retries'] > 0: runningJob['nextRunTime'] = time.time() + ( runningJob['retry_interval'] * 60) runningJob['retries'] = runningJob['retries'] - 1 runningJob['reps'] = runningJob['originalRepeats'] jobsToRun.append(runningJob) continue del self.runningJobs[ runningJobId] #max run time passed or out of retries for jobToRun in jobsToRun: self.runJob(jobToRun) # TODO decide on a time (how often should we check for jobs to run, what resolution?) time.sleep(5) def stop(self): self.running = False def successfulJobRun(self, jobId, state, stateValue): """ Called when job run was considered successful (acked by Z-Wave or sent away from 433), repeats should still be run """ del state, stateValue # save timestamp for when this was executed, to avoid rerun within maxRunTime on restart # TODO is this too much writing? executedJobs = self.settings.get('executedJobs', {}) executedJobs[str(jobId)] = time.time( ) #doesn't work well with int type, for some reason self.settings['executedJobs'] = executedJobs #executedJobsTest = self.settings.get('executedJobs', {}) if jobId in self.runningJobs: self.runningJobs[jobId]['retries'] = 0 @mainthread def runJob(self, jobData): device = self.deviceManager.device(jobData['client_device_id']) if not device: print "Missing device: " + str(jobData['client_device_id']) return method = jobData['method'] value = None if 'value' in jobData: value = jobData['value'] device.command(method, value=value, origin='Scheduler', success=self.successfulJobRun, callbackArgs=[jobData['id']]) @mainthread def runMaintenanceJob(self, jobData): if not jobData: return if jobData['recurrence']: # readd the job for another run self.addMaintenanceJob(time.time() + jobData['recurrence'], jobData['callback'], jobData['recurrence']) jobData['callback']()
class RF433(Plugin): implements(ITelldusLiveObserver) fwVersions = {'18F25K50': 1} def __init__(self): self.version = 0 self.hwVersion = None self.devices = [] self.sensors = [] self.rawEnabled = False self.rawEnabledAt = 0 self.dev = Adapter(self, Board.rf433Port()) deviceNode = DeviceNode(self.dev) self.deviceManager = DeviceManager(self.context) self.registerSensorCleanup() for d in self.deviceManager.retrieveDevices('433'): p = d.params() if 'type' not in p: continue if p['type'] == 'sensor': device = SensorNode() self.sensors.append(device) elif p['type'] == 'device': device = DeviceNode(self.dev) self.devices.append(device) else: continue device.setNodeId(d.id()) device.setParams(p) if p['type'] == 'sensor': device._packageCount = 7 # already loaded, keep it that way! device._sensorValues = d._sensorValues device.batteryLevel = d.batteryLevel self.deviceManager.addDevice(device) self.deviceManager.finishedLoading('433') self.dev.queue( RF433Msg('V', success=self.__version, failure=self.__noVersion)) self.dev.queue( RF433Msg('H', success=self.__hwVersion, failure=self.__noHWVersion)) self.live = TelldusLive(self.context) def addDevice(self, protocol, model, name, params): device = DeviceNode(self.dev) device.setName(name) device.setParams({ 'protocol': protocol, 'model': model, 'protocolParams': params }) self.devices.append(device) self.deviceManager.addDevice(device) def cleanupSensors(self): numberOfSensorsBefore = len(self.sensors) for i, sensor in enumerate(self.sensors): if not sensor.isValid(): self.deviceManager.removeDevice(sensor.id()) del self.sensors[i] self.deviceManager.sensorsUpdated() @TelldusLive.handler('rf433') def __handleCommand(self, msg): data = msg.argument(0).toNative() action = data['action'] if action == 'addDevice': self.addDevice(data['protocol'], data['model'], data['name'], data['parameters']) elif action == 'deviceInfo': deviceId = data['device'] for device in self.devices: if device.id() == deviceId: params = device.params() params['deviceId'] = deviceId self.live.pushToWeb('rf433', 'deviceInfo', params) return elif action == 'editDevice': deviceId = data['device'] for device in self.devices: if device.id() == deviceId: device.setParams({ 'protocol': data['protocol'], 'model': data['model'], 'protocolParams': data['parameters'] }) device.paramUpdated('') break elif action == 'remove': deviceId = data['device'] for device in self.devices: if device.id() == deviceId: self.deviceManager.removeDevice(deviceId) self.devices.remove(device) return elif action == 'rawEnabled': if data['value']: self.rawEnabled = True self.rawEnabledAt = time.time() else: self.rawEnabled = False else: logging.warning("Unknown rf433 command %s", action) @signal('rf433RawData') def decode(self, msg): """ Signal send on any raw data received from 433 receiver. Please note that the TellStick must contain a receiver for this signal to be sent. Not all models contains a receiver. """ if 'class' in msg and msg['class'] == 'sensor': self.decodeSensor(msg) return msg = Protocol.decodeData(msg) for m in msg: self.decodeCommandData(m) if self.rawEnabled: if self.rawEnabledAt < (time.time() - 600): # timeout, only allow scan for 10 minutes at a time self.rawEnabled = False continue self.live.pushToWeb('client', 'rawData', m) def decodeCommandData(self, msg): protocol = msg['protocol'] model = msg['model'] method = msg['method'] methods = Protocol.methodsForProtocol(protocol, model) if not method & methods: return for device in self.devices: params = device.params() if params['protocol'] != protocol: continue if not method & device.methods(): continue deviceParams = params['protocolParams'] thisDevice = True for parameter in Protocol.parametersForProtocol(protocol, model): if parameter not in msg: thisDevice = False break if parameter not in deviceParams: thisDevice = False break if msg[parameter] != deviceParams[parameter]: thisDevice = False break if thisDevice: device.setState(method, None) def decodeData(self, cmd, params): if cmd == 'W': self.decode(params) elif cmd == 'V': # New version received, probably after firmware upload self.__version(params) else: logging.debug("Unknown data: %s", str(cmd)) def decodeSensor(self, msg): protocol = Protocol.protocolInstance(msg['protocol']) if not protocol: logging.error("No known protocol for %s", msg['protocol']) return data = protocol.decodeData(msg) if not data: return p = data['protocol'] m = data['model'] sensorId = data['id'] sensorData = data['values'] sensor = None for s in self.sensors: if s.compare(p, m, sensorId): sensor = s break if sensor is None: sensor = SensorNode() sensor.setParams({'protocol': p, 'model': m, 'sensorId': sensorId}) sensor.setManager(self.deviceManager) self.sensors.append(sensor) if 'battery' in data: sensor.batteryLevel = data['battery'] sensor.updateValues(sensorData) """ Register scheduled job to clean up sensors that have not been updated for a while""" def registerSensorCleanup(self): Application().registerScheduledTask(self.cleanupSensors, hours=12) # every 12th hour t = Timer(10, self.cleanupSensors) # run a first time after 10 minutes t.daemon = True t.name = 'Sensor cleanup' t.start() def __noVersion(self): logging.warning( "Could not get firmware version for RF433, force upgrade") def __noHWVersion(self): logging.warning("Could not get hw version for RF433") def __hwVersion(self, version): logging.debug("Got HW version %s", version) self.hwVersion = version if version not in RF433.fwVersions: return fwVersion = RF433.fwVersions[self.hwVersion] if fwVersion != self.version: logging.info("Version %i is to old, update firmware", self.version) # TODO: implement def __version(self, version): self.version = version logging.info("RF433 version: %i", self.version)
class Shelly(Plugin): implements(IWebReactHandler) implements(IWebRequestHandler) def __init__(self): self.last_sent_data = None self.stop_ping_loop = threading.Event() self.deviceManager = DeviceManager(self.context) Application().registerShutdown(self.shutdown) settings = Settings('tellduslive.config') self.uuid = settings['uuid'] self.logHandler = ShellyLogger(self.uuid) LOGGER.addHandler(self.logHandler) self.setupPing() LOGGER.info('Init Shelly ' + __version__) self._initPyShelly() def setupPing(self): self.ping_count = 0 self.ping_interval = PING_INTERVAL self.ping() def loop(): while not self.stop_ping_loop.wait(self.ping_interval): self.ping() self.ping_thread = threading.Thread(target=loop) self.ping_thread.daemon = True self.ping_thread.start() def _read_settings(self): settings = Settings(CONFIG) pys = self.pyShelly pys.set_cloud_settings(settings["cloud_server"], settings["cloud_auth_key"]) pys.update_status_interval = timedelta(seconds=30) pys.only_device_id = settings["only_device_id"] pys.mdns_enabled = False #settings.get('mdns', True) if not settings['logger']: LOGGER.setLevel(logging.WARNING) def _initPyShelly(self): try: self.pyShelly.close() except: pass pys = self.pyShelly = pyShelly() pys.igmpFixEnabled = True #Enable IGMP fix for ZNet pys.cb_device_added.append(self._device_added) pys.update_status_interval = timedelta(seconds=30) self._read_settings() ###### # pys.cb_block_added.append(self._block_added) # pys.cb_device_added.append(self._device_added) # pys.cb_device_removed.append(self._device_removed) pys.cb_save_cache = self._save_cache pys.cb_load_cache = self._load_cache # pys.username = conf.get(CONF_USERNAME) # pys.password = conf.get(CONF_PASSWORD) # pys.cloud_auth_key = conf.get(CONF_CLOUD_AUTH_KEY) # pys.cloud_server = conf.get(CONF_CLOUD_SERVER) # if zeroconf_async_get_instance: # pys.zeroconf = await zeroconf_async_get_instance(self.hass) # tmpl_name = conf.get(CONF_TMPL_NAME) # if tmpl_name: # pys.tmpl_name = tmpl_name # if additional_info: # pys.update_status_interval = timedelta(seconds=update_interval) # if pys.only_device_id: # pys.only_device_id = pys.only_device_id.upper() # pys.igmp_fix_enabled = conf.get(CONF_IGMPFIX) # pys.mdns_enabled = conf.get(CONF_MDNS) ### pys.start() pys.discover() def _save_cache(self, name, data): settings = Settings('Shelly.cache') settings[name] = data def _load_cache(self, name): settings = Settings('Shelly.cache') return json.loads(settings[name]) def ping(self): try: headers = { "Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain", "Connection": "close" } self.ping_count += 1 params = urllib.urlencode({ 'shelly': __version__, 'pyShelly': self.pyShelly.version(), 'uuid': self.uuid, 'pluginid': 1, 'ping': self.ping_count, 'devices': len(self.pyShelly.blocks), 'level': self.logHandler.logLevel, 'interval': self.ping_interval }) conn = httplib.HTTPConnection("api.tarra.se") conn.request("POST", "/telldus/ping", params, headers) resp = conn.getresponse() body = resp.read() resp = json.loads(body) self.logHandler.logLevel = resp['level'] self.ping_interval = resp['interval'] conn.close() except: pass @staticmethod def getReactComponents(): return { 'shelly': { 'title': 'Shelly', 'script': 'shelly/shelly.js', 'tags': ['menu'], } } def matchRequest(self, plugin, path): LOGGER.debug("MATCH %s %s", plugin, path) if plugin != 'shelly': return False #if path in ['reset', 'state']: #return True return True def _getConfig(self): settings = Settings(CONFIG) return { 'cloud_server': settings["cloud_server"], 'cloud_auth_key': settings["cloud_auth_key"] } def _getData(self, all_devs=False): shellyDevices = \ self.deviceManager.retrieveDevices(None if all_devs else "Shelly") devices = [] for d in shellyDevices: try: buttons = {} methods = d.methods() if methods & Device.TURNON: buttons["on"] = True buttons["off"] = True if methods & Device.UP: buttons["up"] = True buttons["down"] = True buttons["stop"] = True buttons["firmware"] = getattr(d, "has_firmware_update", False) dev = { 'id': d.id(), 'localid': d.localId(), 'name': d.name(), 'isDevice': d.isDevice(), 'state': d.state()[0], 'params': d.params(), 'available': False, 'buttons': buttons, 'typeName': getattr(d, 'type_name', '') } if hasattr(d, 'dev'): _dev = d.dev dev["available"] = _dev.available() if (hasattr(_dev, 'rgb') and _dev.rgb is not None): dev['rgb'] = '#' + ''.join('%02x' % v for v in _dev.rgb) if hasattr(_dev, "get_dim_value"): dev["brightness"] = _dev.get_dim_value() sensors = {} values = d.sensorValues() if 1 in values: sensors['temp'] = \ "%.1f" % float(values[1][0]['value']) if 2 in values: sensors['hum'] = \ "%.1f" % float(values[2][0]['value']) if 256 in values: sensors['consumption'] = \ "%.1f" % float(values[256][0]['value']) if sensors: dev["sensors"] = sensors devices.append(dev) except Exception as ex: LOGGER.exception("Error reading cache") devices.sort(key=lambda x: x['name']) return { 'devices': devices, 'pyShellyVer': self.pyShelly.version() if self.pyShelly else "", 'ver': __version__, 'id': self.uuid } def refreshClient(self): data = self._getData() if self.last_sent_data != data: self.last_sent_data = data Server(self.context).webSocketSend('shelly', 'refresh', data) def handleRequest(self, plugin, path, __params, **__kwargs): if path == 'list': return WebResponseJson(self._getData()) if path == "config": if __params: settings = Settings(CONFIG) for param in __params: if param in ['cloud_server', 'cloud_auth_key']: settings[param] = __params[param] self._read_settings() return WebResponseJson(self._getConfig()) if path == 'devices': devices = list( map( lambda d: { 'id': d.id, 'name': d.friendly_name(), 'unit_id': d.unit_id, 'type': d.type, 'ip_addr': d.ip_addr, 'is_device': d.is_device, 'is_sensor': d.is_sensor, 'sub_name': d.sub_name, 'state_values': d.state_values, 'state': d.state, 'device_type': d.device_type, 'device_sub_type': d.device_sub_type, 'device_nr': d.device_nr, 'master_unit': d.master_unit, 'ext_sensor': d.ext_sensor, 'info_values': d.info_values, 'friendly_name': d.friendly_name() }, self.pyShelly.devices)) return WebResponseJson(devices) if path == 'blocks': blocks = list( map( lambda d: { 'id': d.id, 'unit_id': d.unit_id, 'type': d.type, 'ip_addr': d.ip_addr, 'info_values': d.info_values }, self.pyShelly.blocks.values())) return WebResponseJson(blocks) if path == 'dump': shellyDevices = self.deviceManager.retrieveDevices() devices = list( map( lambda d: { 'id': d.id(), 'localid': d.localId(), 'name': d.name(), 'state': d.state(), 'params': d.params(), 'stateValues': d.stateValues(), 'sensorValues': d.sensorValues(), 'isDevice': d.isDevice(), 'isSensor': d.isSensor(), 'methods': d.methods(), 'parameters': d.parameters(), 'metadata': d.metadata(), 'type': d.typeString() }, shellyDevices)) return WebResponseJson({'devices': devices}) if path in [ 'turnon', 'turnoff', 'up', 'down', 'stop', 'firmware_update' ]: LOGGER.info('Request ' + path) id = __params['id'] device = self.deviceManager.device(int(id)) if path == 'turnon': if hasattr(device.dev, 'brightness'): device.dev.turn_on(brightness=100) else: device.dev.turn_on() elif path == 'turnoff': device.dev.turn_off() elif path == 'up': device.dev.up() elif path == 'down': device.dev.down() elif path == 'stop': device.dev.stop() elif path == 'firmware_update': if device.dev.block: device.dev.block.update_firmware() return WebResponseJson({}) if path == "rgb": id = __params['id'] r = __params['r'] g = __params['g'] b = __params['b'] device = self.deviceManager.device(int(id)) device.dev.set_values(rgb=[r, g, b]) self.refreshClient() return WebResponseJson({}) if path == "rename": id = __params['id'] name = __params['name'] device = self.deviceManager.device(int(id)) device.local_name = name device.update_name() self.refreshClient() return WebResponseJson({}) if path == "clean": self.deviceManager.removeDevicesByType('Shelly') self._initPyShelly() self.refreshClient() return WebResponseJson({'msg': 'Clean done'}) if path == "discover": self.pyShelly.discover() return WebResponseJson({}) if path == "addMember": LOGGER.debug("Add membership") import socket import struct mreq = struct.pack("=4sl", socket.inet_aton("224.0.1.187"), socket.INADDR_ANY) self.pyShelly._socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) return WebResponseJson({}) if path == "dropMember": LOGGER.debug("Drop membership") import socket import struct mreq = struct.pack("=4sl", socket.inet_aton("224.0.1.187"), socket.INADDR_ANY) self.pyShelly._socket.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, mreq) return WebResponseJson({}) if path == "initSocket": self.pyShelly.init_socket() return WebResponseJson({'msg': 'init socket done'}) def _device_added(self, dev, code): LOGGER.info('Add device ' + dev.id + ' ' + str(code)) if (dev.device_type != "POWERMETER" and dev.device_type != "SWITCH" \ and dev.device_sub_type != "humidity") \ or dev.master_unit or dev.major_unit: device = ShellyDevice(dev, self) self.deviceManager.addDevice(device) def shutdown(self): if self.pyShelly is not None: self.pyShelly.close() self.pyShelly = None if self.stop_ping_loop is not None: self.stop_ping_loop.set() def tearDown(self): deviceManager = DeviceManager(self.context) deviceManager.removeDevicesByType('shelly')
class ApiManager(Plugin): implements(IWebRequestHandler) observers = ObserverCollection(IApiCallHandler) def __init__(self): self.tokens = {} self.tokenKey = None def getTemplatesDirs(self): return [resource_filename('api', 'templates')] def matchRequest(self, plugin, path): if plugin != 'api': return False return True def handleRequest(self, plugin, path, params, request, **kwargs): if path == '': methods = {} for o in self.observers: for module, actions in getattr(o, '_apicalls', {}).iteritems(): for action, fn in actions.iteritems(): methods.setdefault(module, {})[action] = { 'doc': fn.__doc__ } return 'index.html', {'methods': methods} if path == 'token': if request.method() == 'PUT': token = uuid.uuid4().hex self.tokens[token] = { 'app': request.post('app'), 'authorized': False, } return WebResponseJson({ 'authUrl': '%s/api/authorize?token=%s' % (request.base(), token), 'token': token }) elif request.method() == 'GET': token = params.get('token', None) if token is None: return WebResponseJson({'error': 'No token specified'}, statusCode=400) if token not in self.tokens: return WebResponseJson({'error': 'No such token'}, statusCode=404) if self.tokens[token]['authorized'] is not True: return WebResponseJson( {'error': 'Token is not authorized'}, statusCode=403) claims = { 'aud': self.tokens[token]['app'], 'exp': int(time.time() + self.tokens[token]['ttl']), } body = {} if self.tokens[token]['allowRenew'] == True: body['renew'] = True body['ttl'] = self.tokens[token]['ttl'] accessToken = jwt.encode(body, self.__tokenKey(), algorithm='HS256', headers=claims) resp = WebResponseJson({ 'token': accessToken, 'expires': claims['exp'], 'allowRenew': self.tokens[token]['allowRenew'], }) del self.tokens[token] return resp if path == 'authorize': if 'token' not in params: return WebResponseJson({'error': 'No token specified'}, statusCode=400) token = params['token'] if token not in self.tokens: return WebResponseJson({'error': 'No such token'}, statusCode=404) if request.method() == 'POST': self.tokens[token]['authorized'] = True self.tokens[token]['allowRenew'] = bool( request.post('extend', False)) self.tokens[token]['ttl'] = int(request.post('ttl', 0)) * 60 return 'authorize.html', {'token': self.tokens[token]} # Check authorization token = request.header('Authorization') if token is None: return WebResponseJson( {'error': 'No token was found in the request'}, statusCode=401) if not token.startswith('Bearer '): return WebResponseJson( { 'error': 'The autorization token must be supplied as a bearer token' }, statusCode=401) token = token[7:] try: body = jwt.decode(token, self.__tokenKey(), algorithms='HS256') except JWSError as e: return WebResponseJson({'error': str(e)}, statusCode=401) claims = jwt.get_unverified_headers(token) if 'exp' not in claims or claims['exp'] < time.time(): return WebResponseJson({'error': 'The token has expired'}, statusCode=401) if 'aud' not in claims or claims['aud'] is None: return WebResponseJson( {'error': 'No app was configured in the token'}, statusCode=401) aud = claims['aud'] if path == 'refreshToken': if 'renew' not in body or body['renew'] != True: return WebResponseJson( {'error': 'The token is not authorized for refresh'}, statusCode=403) if 'ttl' not in body: return WebResponseJson( {'error': 'No TTL was specified in the token'}, statusCode=401) ttl = body['ttl'] exp = int(time.time() + ttl) accessToken = jwt.encode({ 'renew': True, 'ttl': ttl }, self.__tokenKey(), algorithm='HS256', headers={ 'aud': aud, 'exp': exp, }) return WebResponseJson({ 'token': accessToken, 'expires': exp, }) paths = path.split('/') if len(paths) < 2: return None module = paths[0] action = paths[1] for o in self.observers: fn = getattr(o, '_apicalls', {}).get(module, {}).get(action, None) if fn is None: continue try: params['app'] = aud retval = fn(o, **params) except Exception as e: logging.exception(e) return WebResponseJson({'error': str(e)}) if retval == True: retval = {'status': 'success'} return WebResponseJson(retval) return WebResponseJson( {'error': 'The method %s/%s does not exist' % (module, action)}, statusCode=404) def requireAuthentication(self, plugin, path): if plugin != 'api': return if path in ['', 'authorize']: return True return False def __tokenKey(self): if self.tokenKey is not None: return self.tokenKey password = Board.secret() s = Settings('telldus.api') tokenKey = s.get('tokenKey', '') if tokenKey == '': self.tokenKey = os.urandom(32) # Store it salt = os.urandom(16) key = PBKDF2(password, salt).read(32) pwhash = crypt(password) s['salt'] = base64.b64encode(salt) s['pw'] = pwhash # Encrypt token key cipher = AES.new(key, AES.MODE_ECB, '') s['tokenKey'] = base64.b64encode(cipher.encrypt(self.tokenKey)) else: # Decode it salt = base64.b64decode(s.get('salt', '')) pwhash = s.get('pw', '') if crypt(password, pwhash) != pwhash: logging.warning('Could not decrypt token key, wrong password') return None key = PBKDF2(password, salt).read(32) enc = base64.b64decode(tokenKey) cipher = AES.new(key, AES.MODE_ECB, '') self.tokenKey = cipher.decrypt(enc) return self.tokenKey @staticmethod def apicall(module, action): def call(fn): import sys frame = sys._getframe(1) frame.f_locals.setdefault('_apicalls', {}).setdefault(module, {})[action] = fn return fn return call
class WebFrontend(Plugin): implements(IWebRequestHandler) implements(IWebReactHandler) def getReactRoutes(self): return [{ 'name': 'plugins', 'title': 'Plugins (beta)', 'script': 'pluginloader/plugins.js' }] def importKey(self, acceptKeyId): filename = '%s/staging.zip' % Board.pluginPath() if not os.path.exists(filename): return {'success': False, 'msg': 'No plugin uploaded'} try: gpg = loadGPG() with zipfile.ZipFile(filename, 'r') as z: cfg = yaml.load(z.read('manifest.yml')) k = z.extract(cfg['key'], '/tmp/') keys = gpg.scan_keys(k) if len(keys) != 1: raise Exception('Key must only contain exactly one public key') key = keys[0] name = key['uids'] fingerprint = key['fingerprint'] keyid = key['keyid'] if keyid != acceptKeyId: return {'name': name, 'fingerprint': fingerprint, 'keyid': keyid} result = gpg.import_keys(open(k).read()) os.unlink(k) # Reload loaded keys Loader(self.context).initializeKeychain() except Exception as e: os.unlink(filename) return {'success': False, 'msg': str(e)} return {'success': True} def importPlugin(self): filename = '%s/staging.zip' % Board.pluginPath() z = None try: z = zipfile.ZipFile(filename, 'r') try: info = z.getinfo('manifest.yml') except KeyError: raise ImportError('Malformed plugin. No manifest found.') cfg = yaml.load(z.read('manifest.yml')) if 'name' not in cfg: raise ImportError('Malformed plugin. Plugin has no name.') if cfg['name'] == 'staging': raise ImportError('Plugin name cannot be "staging", this is a reserved name') if 'packages' not in cfg: raise ImportError('Malformed plugin. Manifest does not list any packages.') gpg = loadGPG() packages = [] for p in cfg['packages']: f = z.extract(p, '/tmp/') s = z.getinfo('%s.asc' % p) packages.append((f, s,)) result = gpg.verify_file(z.open(s), f) if result.valid is True: continue # remove unpackaged files for p, s in packages: os.unlink(p) if result.pubkey_fingerprint is None and result.username is None: # No public key for this plugin return {'success': False, 'key': self.importKey(None)} raise ImportError('Could not verify plugin') path = '%s/%s' % (Board.pluginPath(), cfg['name']) if os.path.exists(path): # Wipe any old plugin shutil.rmtree(path) os.mkdir(path) for p, s in packages: shutil.move(p, '%s/%s' % (path, os.path.basename(p))) z.extract(s, path) manifest = z.extract(info, path) except zipfile.BadZipfile: raise ImportError('Uploaded file was not a Zip file') finally: if z is not None: z.close() os.unlink(filename) loader = Loader(self.context) loader.loadPlugin(manifest) return {'success': True, 'msg': 'Plugin was imported'} def uploadPlugin(self, f): with open('%s/staging.zip' % (Board.pluginPath()), 'w') as wf: wf.write(f.file.read()) def getTemplatesDirs(self): return [pkg_resources.resource_filename('pluginloader', 'templates')] def matchRequest(self, plugin, path): if plugin != 'pluginloader': return False if path in ['import', 'importkey', 'keys', 'remove', 'plugins', 'saveConfiguration', 'upload']: return True return False def handleRequest(self, plugin, path, params, request, **kwargs): if path == 'import': if os.path.isfile('%s/staging.zip' % (Board.pluginPath())): try: return WebResponseJson(self.importPlugin()) except ImportError as e: os.unlink('%s/staging.zip' % (Board.pluginPath())) return WebResponseJson({'success': False, 'msg':'Error importing plugin: %s' % e}) return WebResponseJson({'success': False, 'msg':'Error importing plugin: No plugin uploaded'}) if path == 'importkey': if 'discard' in params: os.unlink('%s/staging.zip' % (Board.pluginPath())) return WebResponseJson({'success': True}) return WebResponseJson(self.importKey(params['key'] if 'key' in params else None)) if path == 'remove': if 'pluginname' in params: Loader(self.context).removePlugin(params['pluginname']) elif 'key' in params and 'fingerprint' in params: Loader(self.context).removeKey(params['key'], params['fingerprint']) else: return WebResponseJson({'success': False, 'msg': 'No plugin or key specified'}) return WebResponseJson({'success': True}) if path == 'plugins': return WebResponseJson([plugin.infoObject() for plugin in Loader(self.context).plugins]) if path == 'keys': return WebResponseJson([ { 'uids': key['uids'], 'keyid': key['keyid'], 'fingerprint': key['fingerprint'] } for key in Loader(self.context).keys ]) if path == 'saveConfiguration' and request.method() == 'POST': plugin = params['pluginname'] configuration = json.loads(params['configuration']) try: Loader(self.context).saveConfiguration(plugin, configuration) except Exception as e: return WebResponseJson({'success': False, 'msg': str(e)}) return WebResponseJson({'success': True}) if path == 'upload' and request.method() == 'POST': self.uploadPlugin(params['pluginfile']) try: return WebResponseJson(self.importPlugin()) except ImportError as e: os.unlink('%s/staging.zip' % (Board.pluginPath())) return WebResponseJson({'success': False, 'msg': str(e)}) return 'pluginloader.html', {'msg':'', 'loader': Loader(self.context)}
class DeviceManager(Plugin): """The devicemanager holds and manages all the devices in the server""" implements(ITelldusLiveObserver) observers = ObserverCollection(IDeviceChange) public = True def __init__(self): self.devices = [] self.settings = Settings('telldus.devicemanager') self.nextId = self.settings.get('nextId', 0) self.live = TelldusLive(self.context) self.registered = False self.__load() @mainthread def addDevice(self, device): """ Call this function to register a new device to the device manager. .. note:: The :func:`Device.localId() <telldus.Device.localId>` function in the device must return a unique id for the transport type returned by :func:`Device.typeString() <telldus.Device.localId>` """ cachedDevice = None for i, delDevice in enumerate(self.devices): # Delete the cached device from loaded devices, since it is replaced # by a confirmed/specialised one if delDevice.localId() == device.localId() \ and device.typeString() == delDevice.typeString() \ and not delDevice.confirmed(): cachedDevice = delDevice del self.devices[i] break self.devices.append(device) device.setManager(self) if not cachedDevice: # New device, not stored in local cache self.nextId = self.nextId + 1 device.setId(self.nextId) else: # Transfer parameters from the loaded one device.loadCached(cachedDevice) self.save() if not cachedDevice: self.__deviceAdded(device) if self.live.registered and device.isDevice(): (state, stateValue) = device.state() deviceDict = { 'id': device.id(), 'name': device.name(), 'methods': device.methods(), 'state': state, 'stateValue': stateValue, 'protocol': device.protocol(), 'model': device.model(), 'transport': device.typeString() } msg = LiveMessage("DeviceAdded") msg.append(deviceDict) self.live.send(msg) else: # Previously cached device is now confirmed, TODO notify Live! about this too? self.observers.deviceConfirmed(device) def device(self, deviceId): """Retrieves a device. :param int deviceId: The id of the device to be returned. :returns: the device specified by `deviceId` or None of no device was found """ for device in self.devices: if device.id() == deviceId: return device return None def deviceParamUpdated(self, device, param): self.save() if param == 'name': if device.isDevice(): self.__sendDeviceReport() if device.isSensor: self.__sendSensorReport() def findByName(self, name): for device in self.devices: if device.name() == name: return device return None @mainthread def finishedLoading(self, deviceType): """ Finished loading all devices of this type. If there are any unconfirmed, these should be deleted """ for device in self.devices: if device.typeString() == deviceType and not device.confirmed(): self.removeDevice(device.id()) @mainthread def removeDevice(self, deviceId): """ Removes a device. .. warning:: This function may only be called by the module supplying the device since removing of a device may be transport specific. """ isDevice = True for i, device in enumerate(self.devices): if device.id() == deviceId: self.__deviceRemoved(deviceId) isDevice = self.devices[i].isDevice() del self.devices[i] break self.save() if self.live.registered and isDevice: msg = LiveMessage("DeviceRemoved") msg.append({'id': deviceId}) self.live.send(msg) @mainthread def removeDevicesByType(self, deviceType): """ .. versionadded:: 1.1.0 Remove all devices of a specific device type :param str deviceType: The type of devices to remove """ deviceIds = [] for device in self.devices: if device.typeString() == deviceType: deviceIds.append(device.id()) for deviceId in deviceIds: self.removeDevice(deviceId) def retrieveDevices(self, deviceType=None): """Retrieve a list of devices. :param deviceType: If this parameter is set only devices with this type is returned :type deviceType: str or None :returns: a list of devices """ lst = [] for device in self.devices: if deviceType is not None and device.typeString() != deviceType: continue lst.append(device) return lst @signal def sensorValueUpdated(self, device, valueType, value, scale): """ Called every time a sensors value is updated. """ if device.isSensor() is False: return self.observers.sensorValueUpdated(device, valueType, value, scale) if not self.live.registered or device.ignored(): # don't send if not connected to live or sensor is ignored return if valueType in device.lastUpdatedLive \ and (valueType in device.valueChangedTime \ and device.valueChangedTime[valueType] < device.lastUpdatedLive[valueType]) \ and device.lastUpdatedLive[valueType] > (int(time.time()) - 300): # no values have changed since the last live-update, and the last # time this sensor was sent to live was less than 5 minutes ago return msg = LiveMessage("SensorEvent") # pcc = packageCountChecked - already checked package count, # just accept it server side directly sensor = { 'name': device.name(), 'protocol': device.protocol(), 'model': device.model(), 'sensor_id': device.id(), 'pcc': 1, } battery = device.battery() if battery is not None: sensor['battery'] = battery msg.append(sensor) # small clarification: valueType etc that is sent in here is only used for sending # information about what have changed on to observers, below is instead all the values # of the sensor picked up and sent in a sensor event-message (the sensor values # have already been updated in other words) values = device.sensorValues() valueList = [] for valueType in values: for value in values[valueType]: valueList.append({ 'type': valueType, 'lastUp': str(value['lastUpdated']), 'value': str(value['value']), 'scale': value['scale'] }) msg.append(valueList) device.lastUpdatedLive[valueType] = int(time.time()) self.live.send(msg) def stateUpdated(self, device, ackId=None, origin=None): if device.isDevice() is False: return extras = {} if ackId: extras['ACK'] = ackId if origin: extras['origin'] = origin else: extras['origin'] = 'Incoming signal' (state, stateValue) = device.state() self.__deviceStateChanged(device, state, stateValue, extras['origin']) self.save() if not self.live.registered: return msg = LiveMessage("DeviceEvent") msg.append(device.id()) msg.append(state) msg.append(str(stateValue)) msg.append(extras) self.live.send(msg) def stateUpdatedFail(self, device, state, stateValue, reason, origin): if not self.live.registered: return if device.isDevice() is False: return extras = { 'reason': reason, } if origin: extras['origin'] = origin else: extras['origin'] = 'Unknown' (state, stateValue) = device.state() self.__deviceStateChanged(device, state, stateValue, extras['origin']) msg = LiveMessage('DeviceFailEvent') msg.append(device.id()) msg.append(state) msg.append(stateValue) msg.append(extras) self.live.send(msg) @TelldusLive.handler('command') def __handleCommand(self, msg): args = msg.argument(0).toNative() action = args['action'] value = args['value'] if 'value' in args else None deviceId = args['id'] device = None for dev in self.devices: if dev.id() == deviceId: device = dev break def success(state, stateValue): if 'ACK' in args: device.setState(state, stateValue, ack=args['ACK']) # Abort the DeviceEvent this triggered raise DeviceAbortException() def fail(reason): # We failed to set status for some reason, nack the server if 'ACK' in args: msg = LiveMessage('NACK') msg.append({ 'ackid': args['ACK'], 'reason': reason, }) self.live.send(msg) # Abort the DeviceEvent this triggered raise DeviceAbortException() device.command(action, value, success=success, failure=fail) @TelldusLive.handler('device') def __handleDeviceCommand(self, msg): args = msg.argument(0).toNative() if 'action' not in args: return if args['action'] == 'setName': if 'name' not in args or args['name'] == '': return for dev in self.devices: if dev.id() != args['device']: continue if isinstance(args['name'], int): dev.setName(str(args['name'])) else: dev.setName(args['name'].decode('UTF-8')) return @TelldusLive.handler('device-requestdata') def __handleDeviceParametersRequest(self, msg): args = msg.argument(0).toNative() device = self.device(args.get('id', 0)) if not device: return reply = LiveMessage('device-datareport') data = {'id': args['id']} if args.get('parameters', 0) == 1: parameters = json.dumps(device.allParameters(), separators=(',', ':'), sort_keys=True) data['parameters'] = parameters data['parametersHash'] = hashlib.sha1(parameters).hexdigest() if args.get('metadata', 0) == 1: metadata = json.dumps(device.metadata(), separators=(',', ':'), sort_keys=True) data['metadata'] = metadata data['metadataHash'] = hashlib.sha1(metadata).hexdigest() reply.append(data) self.live.send(reply) @TelldusLive.handler('reload') def __handleSensorUpdate(self, msg): reloadType = msg.argument(0).toNative() if reloadType != 'sensor': # not for us return data = msg.argument(1).toNative() if not msg.argument(2) or 'sensorId' not in msg.argument(2).toNative(): # nothing to do, might be an orphaned zwave sensor return sensorId = msg.argument(2).toNative()['sensorId'] updateType = data['type'] for dev in self.devices: if dev.id() == sensorId: if updateType == 'updateignored': value = data['ignored'] if dev.ignored() == value: return dev.setIgnored(value) self.__sendSensorChange(sensorId, updateType, value) return if updateType == 'updateignored' and len(self.devices) > 0: # we don't have this sensor, do something! (can't send sensor change # back (__sendSensorChange), because can't create message when # sensor is unknown (could create special workaround, but only do # that if it's still a problem in the future)) logging.warning( 'Requested ignore change for non-existing sensor %s', str(sensorId)) # send an updated sensor report, so that this sensor is hopefully # cleaned up self.__sendSensorReport() def liveRegistered(self, __msg): self.registered = True self.__sendDeviceReport() self.__sendSensorReport() def __load(self): self.store = self.settings.get('devices', []) for dev in self.store: if 'type' not in dev or 'localId' not in dev: continue # This should not be possible device = CachedDevice(dev) # If we have loaded this device from cache 5 times in a row it's # considered dead if device.loadCount() < 5: self.devices.append(device) @signal('deviceAdded') def __deviceAdded(self, device): """ Called every time a device is added/created """ self.observers.deviceAdded(device) @signal('deviceRemoved') def __deviceRemoved(self, deviceId): """ Called every time a device is removed. The parameter deviceId is the old device id. The ref to the device is no longer available """ self.observers.deviceRemoved(deviceId) @signal('deviceStateChanged') def __deviceStateChanged(self, device, state, stateValue, origin): """ Called every time the state of a device is changed. """ del origin # Remove pylint warning self.observers.stateChanged(device, state, stateValue) def save(self): data = [] for device in self.devices: (state, stateValue) = device.state() dev = { "id": device.id(), "loadCount": device.loadCount(), "localId": device.localId(), "type": device.typeString(), "name": device.name(), "params": device.params(), "methods": device.methods(), "state": state, "stateValue": stateValue, "ignored": device.ignored(), "isSensor": device.isSensor() } if len(device.sensorValues()) > 0: dev['sensorValues'] = device.sensorValues() battery = device.battery() if battery is not None: dev['battery'] = battery if hasattr(device, 'declaredDead') and device.declaredDead: dev['declaredDead'] = device.declaredDead data.append(dev) self.settings['devices'] = data self.settings['nextId'] = self.nextId def __sendDeviceReport(self): logging.warning("Send Devices Report") if not self.live.registered: return lst = [] for device in self.devices: if not device.isDevice(): continue (state, stateValue) = device.state() parametersHash = hashlib.sha1( json.dumps(device.allParameters(), separators=(',', ':'), sort_keys=True)) metadataHash = hashlib.sha1( json.dumps(device.metadata(), separators=(',', ':'), sort_keys=True)) dev = { 'id': device.id(), 'name': device.name(), 'methods': device.methods(), 'state': state, 'stateValue': str(stateValue), 'protocol': device.protocol(), 'model': device.model(), 'parametersHash': parametersHash.hexdigest(), 'metadataHash': metadataHash.hexdigest(), 'transport': device.typeString(), 'ignored': device.ignored() } battery = device.battery() if battery is not None: dev['battery'] = battery lst.append(dev) msg = LiveMessage("DevicesReport") logging.warning("DR %s", lst) msg.append(lst) self.live.send(msg) def __sendSensorChange(self, sensorid, valueType, value): msg = LiveMessage("SensorChange") device = None for dev in self.devices: if dev.id() == sensorid: device = dev break if not device: return sensor = { 'protocol': device.typeString(), 'model': device.model(), 'sensor_id': device.id(), } msg.append(sensor) msg.append(valueType) msg.append(value) self.live.send(msg) def __sendSensorReport(self): if not self.live.registered: return lst = [] for device in self.devices: if device.isSensor() is False: continue sensorFrame = [] sensor = { 'name': device.name(), 'protocol': device.protocol(), 'model': device.model(), 'sensor_id': device.id(), } if device.params() and 'sensorId' in device.params(): sensor['channelId'] = device.params()['sensorId'] battery = device.battery() if battery is not None: sensor['battery'] = battery if hasattr(device, 'declaredDead') and device.declaredDead: # Sensor shouldn't be removed for a while, but don't update it on server side sensor['declaredDead'] = 1 sensorFrame.append(sensor) valueList = [] values = device.sensorValues() for valueType in values: for value in values[valueType]: valueList.append({ 'type': valueType, 'lastUp': str(value['lastUpdated']), 'value': str(value['value']), 'scale': value['scale'] }) # Telldus Live! does not aknowledge sensorreportupdates yet, # so don't count this yet (wait for Cassandra only) # device.lastUpdatedLive[valueType] = int(time.time()) sensorFrame.append(valueList) lst.append(sensorFrame) msg = LiveMessage("SensorsReport") msg.append(lst) self.live.send(msg) def sensorsUpdated(self): self.__sendSensorReport()
class HomeKit(Plugin): implements(IDeviceChange) implements(IWebReactHandler) def __init__(self): self.httpServer = None self.bonjour = None self.port = 0 Application().queue(self.start) self.clients = self.config('clients') self.configurationNumber = self.config('configurationNumber') self.longTermKey = self.config('longTermKey') self.password = self.config('password') if self.password is None: # Generate password code = ''.join([str(random.randint(0, 9)) for _i in range(8)]) self.password = '******' % (code[0:3], code[3:5], code[5:8]) self.setConfig('password', self.password) @staticmethod def getReactComponents(): return { 'homekit': { 'title': 'HomeKit', 'script': 'homekit/homekit.js', } } def start(self): self.port = random.randint(8000, 8079) sf = 1 if len(self.clients) == 0 else 0 # pylint: disable=invalid-name self.bonjour = Bonjour(port=self.port, c=self.configurationNumber, sf=sf) self.httpServer = HTTPDServer(port=self.port, context=self.context) def addPairing(self, identifier, publicKey, permissions): self.clients[identifier] = { 'publicKey': publicKey, 'admin': permissions } self.setConfig('clients', self.clients) # Non discoverable self.bonjour.updateRecord(sf=0) return True def removePairing(self, identifier): if identifier not in self.clients: return True del self.clients[identifier] self.setConfig('clients', self.clients) if len(self.clients) == 0: # Discoverable again self.bonjour.updateRecord(sf=1) return True def newConnection(self, conn): if self.longTermKey == '': # No public key, generate signingKey, _verifyingKey = ed25519.create_keypair() self.longTermKey = signingKey.to_ascii(encoding='hex') self.setConfig('longTermKey', self.longTermKey) conn.setLongTermKey(self.longTermKey, self.password) def increaseConfigurationNumber(self): self.configurationNumber += 1 if self.configurationNumber >= 4294967295: self.configurationNumber = 1 self.setConfig('configurationNumber', self.configurationNumber) self.bonjour.updateRecord(c=self.configurationNumber) # IDeviceChange def deviceAdded(self, device): if self.httpServer is None: # Too early, we have not started yet return if len(self.httpServer.connections) == 0: # No connections, ignore return for conn in self.httpServer.connections: conn.deviceAdded(device) self.increaseConfigurationNumber() # IDeviceChange def deviceConfirmed(self, device): self.deviceAdded(device) # IDeviceChange def deviceRemoved(self, deviceId): if self.httpServer is None: # Too early, we have not started yet return if len(self.httpServer.connections) == 0: # No connections, ignore return for conn in self.httpServer.connections: conn.deviceRemoved(deviceId) self.increaseConfigurationNumber() # IDeviceChange def sensorValueUpdated(self, device, valueType, value, scale): if self.httpServer is None: # Too early, we have not started yet return newValue = False for conn in self.httpServer.connections: if conn.sensorValueUpdated(device, valueType, value, scale): newValue = True if newValue: self.increaseConfigurationNumber() # IDeviceChange def stateChanged(self, device, state, statevalue): if self.httpServer is None: # Too early, we have not started yet return for conn in self.httpServer.connections: conn.deviceStateChanged(device, state, statevalue)
class Client(Plugin): implements(ISignalObserver) def __init__(self): self._ready = False self._running = True self._knownDevices = None Application().registerShutdown(self.onShutdown) self.client = mqtt.Client(userdata=self) self.client_on_disconnect = self.onDisconnect self.client.on_connect = self.onConnect self.client.on_message = self.onMessage if self.config('hostname') != '': Application().queue(self.connect) def onShutdown(self): self._running = False self.client.loop_stop() self.client.disconnect() def getKnownDevices(self): if not self._knownDevices: if self.config('devices_configured'): self._knownDevices = [ tuple(x) for x in json.loads(self.config('devices_configured')) ] else: self._knownDevices = [] return self._knownDevices def setKnownDevices(self, devices): try: self.debug('Setting knownDevices to : %s' % devices) self._knownDevices = devices self.setConfig('devices_configured', json.dumps(devices)) except Exception as e: self.debug('setKnownDevices error %s' % str(e)) def isKnownDevice(self, type, devId, deviceId): devices = self.getKnownDevices() return (type, str(devId), str(deviceId)) in devices def addKnownDevice(self, type, devId, deviceId): devices = self.getKnownDevices() devices.append((type, str(devId), str(deviceId))) self.setKnownDevices(devices) def configWasUpdated(self, key, value): if not key in ['devices_configured']: self.connect() def tearDown(self): try: for type, id, fullId in self.getKnownDevices(): deviceTopic = self.getDeviceTopic(type, fullId) self.client.publish('%s/config' % deviceTopic, '', retain=True) self.client.publish('%s/state' % deviceTopic, '', retain=True) except Exception as e: self.debug('tearDown %s' % str(e)) def connect(self): username = self.config('username') password = self.config('password') base_topic = self.config('base_topic') device_name = self.config('device_name') hostname = self.config('hostname') port = self.config('port') if username != '': self.client.username_pw_set(username, password) self.client.will_set( '%s/%s/available' % (base_topic, device_name) if base_topic \ else '%s/available' % device_name, 'offline', 0, True ) self.client.connect_async(hostname, port, keepalive=10) self.client.loop_start() def debug(self, msg): base_topic = self.config('base_topic') device_name = self.config('device_name') debugTopic = ( '%s/%s/debug' % (base_topic, device_name) if base_topic \ else '%s/debug' % device_name ) self.client.publish(debugTopic, msg) def getDeviceType(self, device): capabilities = device.methods() if capabilities & Device.DIM: return 'light' elif capabilities & Device.TURNON: return 'switch' elif capabilities & Device.UP: return 'cover' elif capabilities & Device.BELL: return 'switch' else: return 'binary_sensor' def getDeviceTopic(self, type, id): discoverTopic = self.config('discovery_topic') telldusName = self.config('device_name') or 'telldus' return '%s/%s/%s/%s' % (discoverTopic, type, telldusName, id) def getSensorId(self, deviceId, valueType, scale): return '%s_%s_%s' % (deviceId, valueType, scale) def getBatteryId(self, device): return '%s_battery' % device.id() def formatBattery(self, battery): if battery == Device.BATTERY_LOW: return 1 elif battery == Device.BATTERY_UNKNOWN: return None elif battery == Device.BATTERY_OK: return 100 else: return int(battery) def formatScale(self, type, scale): return ScaleConverter.get(type, {}).get(scale, '') def deviceState(self, device): try: state, stateValue = device.state() deviceType = self.getDeviceType(device) if not deviceType: return self.debug('deviceState %s, state: %s, value: %s' % (device.id(), state, stateValue)) stateTopic = '%s/state' % self.getDeviceTopic( deviceType, device.id()) payload = '' if deviceType in ['light']: if state == Device.DIM: payload = json.dumps({ 'state': 'ON' if stateValue and int(stateValue) > 0 else 'OFF', 'brightness': int(stateValue) if stateValue else 0 }) else: payload = json.dumps({ 'state': 'ON' if state == Device.TURNON else 'OFF', 'brightness': (int(stateValue) if stateValue else 255) if state == Device.TURNON else 0 }) elif deviceType in ['switch']: payload = 'ON' if state in [Device.TURNON, Device.BELL ] else 'OFF' elif deviceType in ['binary_sensor']: payload = 'ON' if state in [Device.TURNON] else 'OFF' elif deviceType in ['cover']: payload = 'OPEN' if state == Device.UP else 'CLOSED' if state == Device.DOWN else 'STOP' self.client.publish(stateTopic, payload, retain=True) if state == Device.BELL: self.client.publish(stateTopic, 'OFF', retain=True) except Exception as e: self.debug('deviceState exception %s' % e.message) def sensorState(self, device, valueType, scale): try: sensorId = self.getSensorId(device.id(), valueType, scale) for sensor in device.sensorValues()[valueType]: if sensor['scale'] == scale: self.debug('sensorState %s' % sensor) payload = { 'value': sensor['value'], 'lastUpdated': sensor.get('lastUpdated') } self.client.publish( '%s/state' % self.getDeviceTopic('sensor', sensorId), json.dumps(payload), retain=True) except Exception as e: self.debug('sensorState exception %s' % e.message) def batteryState(self, device): try: self.client.publish( '%s/state' % self.getDeviceTopic('sensor', self.getBatteryId(device)), self.formatBattery(device.battery()), retain=True) except Exception as e: self.debug('batteryState exception %s' % e.message) def discover(self, device, type, deviceId, config): base_topic = self.config('base_topic') device_name = self.config('device_name') config.update({ 'unique_id': '%s_%s' % (device_name, deviceId), 'availability_topic': ( '%s/%s/available' % (base_topic, device_name) if base_topic \ else '%s/available' % device_name ), 'device': { 'identifiers': getMacAddr(Board.networkInterface()), 'connections': [['mac', getMacAddr(Board.networkInterface(), False)]], 'manufacturer': 'Telldus Technologies', 'model': Board.product(), 'name': 'telldus', 'sw_version': Board.firmwareVersion(), } }) self.client.publish('%s/config' % self.getDeviceTopic(type, deviceId), json.dumps(config), retain=True) return (type, str(device.id()), str(deviceId)) def undiscover(self, type, devId, fullId): deviceTopic = self.getDeviceTopic(type, fullId) self.debug('undiscover device %s,%s,%s : %s' % (type, devId, fullId, deviceTopic)) self.client.publish('%s/config' % deviceTopic, '', retain=True) self.client.publish('%s/state' % deviceTopic, '', retain=True) def discoverBattery(self, device): try: sensorConfig = { 'name': '%s - Battery' % device.name(), 'unit_of_measurement': '%', 'device_class': 'battery' } return self.discover(device, 'sensor', self.getBatteryId(device), sensorConfig) except Exception as e: self.debug('discoverBattery %s' % str(e)) def discoverSensor(self, device, valueType, scale): sensorId = self.getSensorId(device.id(), valueType, scale) try: sensorConfig = { 'name': '%s %s - %s' % (device.name(), Device.sensorTypeIntToStr(valueType), self.formatScale(valueType, scale)), 'value_template': '{{ value_json.value }}', 'json_attributes_topic': '%s/state' % self.getDeviceTopic("sensor", sensorId), 'unit_of_measurement': self.formatScale(valueType, scale), } if ClassConverter.get(valueType, None): sensorConfig.update( {'device_class': ClassConverter.get(valueType, None)}) sensorId = self.getSensorId(device.id(), valueType, scale) return self.discover(device, 'sensor', sensorId, sensorConfig) except Exception as e: self.debug('discoverSensor %s' % str(e)) def discoverDevice(self, device): try: deviceType = self.getDeviceType(device) if not deviceType: return None deviceTopic = self.getDeviceTopic(deviceType, device.id()) deviceConfig = {'name': device.name()} if deviceType in ['switch', 'light', 'cover']: deviceConfig.update({'command_topic': '%s/set' % deviceTopic}) if deviceType == 'light': deviceConfig.update({'schema': 'json', 'brightness': True}) if deviceType == 'switch' and (device.methods() & Device.BELL): deviceConfig.update({'payload_on': 'BELL'}) self.debug('device is device: %s' % json.dumps({ 'deviceType': deviceType, 'deviceTopic': deviceTopic, 'deviceConfig': deviceConfig })) return self.discover(device, deviceType, device.id(), deviceConfig) except Exception as e: self.debug('discoverDevice %s' % str(e)) def discovery(self, device): result = [] try: if device.battery() and device.battery() != Device.BATTERY_UNKNOWN: self.debug('device %s has battery' % device.id()) self.discoverBattery(device) result.append(self.batteryState(device)) if device.isSensor(): self.debug('device %s has sensors' % device.id()) for type, sensors in device.sensorValues().items(): self.debug('sensortype %s has %s' % (type, sensors)) for sensor in sensors: result.append( self.discoverSensor(device, type, sensor['scale'])) self.sensorState(device, type, sensor['scale']) if device.isDevice(): self.debug('device %s is a device' % device.id()) result.append(self.discoverDevice(device)) self.deviceState(device) except Exception as e: self.debug('discovery %s' % str(e)) return [x for x in result if x] def run_discovery(self): self.debug('discover devices') try: # publish devices publishedDevices = [] deviceManager = DeviceManager(self.context) devices = deviceManager.retrieveDevices() for device in devices: try: self.debug( json.dumps({ 'deviceId': device.id(), 'type': self.getDeviceType(device), 'name': device.name(), 'isDevice': device.isDevice(), 'isSensor': device.isSensor(), 'methods': device.methods(), 'battery': device.battery(), 'parameters': device.allParameters() if hasattr( device, 'allParameters') else device.parameters(), 'type': device.typeString(), 'sensors': device.sensorValues(), 'state': device.state() })) publishedDevices.extend(self.discovery(device)) except Exception as e: self.debug('run_discovery device exception %s' % e.message) for type, devId, fullId in list( set(self.getKnownDevices()) - set(publishedDevices)): self.undiscover(type, devId, fullId) self.setKnownDevices(publishedDevices) except Exception as e: self.debug('run_discovery exception %s' % e.message) def onConnect(self, client, userdata, flags, result): base_topic = userdata.config('base_topic') device_name = userdata.config('device_name') client.publish( '%s/%s/available' % (base_topic, device_name) if base_topic \ else '%s/available' % device_name, 'online', 0, True ) userdata.debug('Hello from %s, connected!' % (device_name)) try: userdata.debug('KnownDevices: %s' % userdata.getKnownDevices()) userdata.run_discovery() #subscribe to commands userdata.debug('subscribing') client.subscribe('%s/+/%s/+/set' % (userdata.config('discovery_topic'), device_name)) userdata._ready = True except Exception as e: userdata.debug('OnConnect error %s' % str(e)) def onDisconnect(self, client, userdata, rc): userdata._ready = False @slot('deviceAdded') def onDeviceAdded(self, device): try: self.debug('Device added %s' % device.id()) devices = self.getKnownDevices() devices.extend(self.discovery(device)) self.setKnownDevices(devices) except Exception as e: self.debug('onDeviceAdded error %s' % str(e)) @slot('deviceRemoved') def onDeviceRemoved(self, deviceId): try: self.debug('Device removed %s' % deviceId) devices = self.getKnownDevices() for type, devId, fullId in devices: if devId == str(deviceId): self.undiscover(type, devId, fullId) devices = [x for x in devices if x[1] != str(deviceId)] self.setKnownDevices(devices) except Exception as e: self.debug('onDeviceRemoved error %s' % str(e)) @slot('deviceUpdated') def onDeviceUpdated(self, device): try: self.debug('Device updated %s' % device.id()) devices = self.getKnownDevices() for type, devId, fullId in devices: if devId == str(deviceId): self.undiscover(type, devId, fullId) devices = [x for x in devices if x[1] != str(device.id())] devices.extend(self.discovery(device)) self.setKnownDevices(devices) except Exception as e: self.debug('onDeviceUpdated error %s' % str(e)) @slot('rf433RawData') def onRawData(self, data, *__args, **__kwargs): self.debug(json.dumps(data)) @slot('sensorValueUpdated') def onSensorValueUpdated(self, device, valueType, value, scale): if not self._ready: return self.debug( json.dumps({ 'type': 'sensorValueUpdated', 'deviceId': device.id(), 'valueType': valueType, 'value': value, 'scale': scale, 'battery': device.battery() })) sensorId = self.getSensorId(device.id(), valueType, scale) if not self.isKnownDevice('sensor', device.id(), sensorId): self.debug('A wild sensor appeared! deviceId: %s, sensorId: %s' % (device.id(), sensorId)) type, devId, deviceId = self.discoverSensor( device, valueType, scale) self.addKnownDevice(type, devId, deviceId) self.sensorState(device, valueType, scale) if device.battery() and device.battery() != Device.BATTERY_UNKNOWN: self.batteryState(device) @slot('deviceStateChanged') def onDeviceStateChanged(self, device, state, stateValue, origin=None): if not self._ready: return self.debug( json.dumps({ 'type': 'deviceStateChanged', 'deviceId': device.id(), 'state': state, 'stateValue': stateValue, 'origin': origin })) deviceType = self.getDeviceType(device) if not deviceType: return if not self.isKnownDevice(deviceType, device.id(), device.id()): self.debug('A wild device appeared! type: %s, deviceId: %s' % (deviceType, device.id())) type, devId, deviceId = self.discoverDevice(device) self.addKnownDevice(type, devId, deviceId) self.deviceState(device) if device.battery(): self.batteryState(device) def onMessage(self, client, userdata, msg): try: topic = msg.topic payload = msg.payload topicType = topic.split('/')[-1] deviceManager = DeviceManager(userdata.context) device_id = int(msg.topic.split('/')[-2]) device = deviceManager.device(device_id) deviceType = userdata.getDeviceType(device) if not deviceType: return userdata.debug( json.dumps({ 'type': 'command', 'device_id': device_id, 'device_type': deviceType, 'command': payload })) if deviceType == 'light': payload = json.loads(payload) if 'brightness' in payload: if int(payload['brightness']) == 0: device.command(Device.TURNOFF, origin='mqtt_hass') else: device.command(Device.DIM, value=int(payload['brightness']), origin='mqtt_hass') else: device.command( Device.TURNON if payload['state'] == 'ON' \ else Device.TURNOFF, value = 255, origin = 'mqtt_hass' ) elif deviceType == 'switch': device.command( Device.TURNON if payload == 'ON' \ else Device.BELL if payload == 'BELL' \ else Device.TURNOFF, origin = 'mqtt_hass' ) elif deviceType == 'cover': device.command( Device.UP if payload == 'OPEN' \ else Device.DOWN if payload == 'CLOSE' else \ Device.STOP, origin = 'mqtt_hass' ) except Exception as e: userdata.debug('onMessage exception %s' % e.message)
class RoomManager(Plugin): """The roommanager holds and manages all the rooms in the server""" implements(ISignalObserver) implements(ITelldusLiveObserver) public = True def __init__(self): self.rooms = {} self.settings = Settings('telldus.rooms') self.rooms = self.settings.get('rooms', {}) self.roomlistEmpty = self.settings.get('roomlistEmpty', False) def getResponsibleRooms(self, responsible=None): if not responsible: live = TelldusLive(self.context) responsible = live.uuid rooms = {} for roomUUID in self.rooms: room = self.rooms[roomUUID] if room['responsible'] == responsible: rooms[roomUUID] = room return rooms def liveRegistered(self, msg, refreshRequired): if refreshRequired: self.syncRoom() def reportRooms(self, rooms, removedRooms=None): report = {} if not rooms and not self.roomlistEmpty and not removedRooms: # only allow empty room reports if we know it has been # explicitly emptied return if rooms or self.roomlistEmpty: report['rooms'] = rooms if removedRooms: report['removedRooms'] = removedRooms msg = LiveMessage('RoomReport') msg.append(report) TelldusLive(self.context).send(msg) def roomChanged(self, room1, room2): for prop in room1: if not room1[prop] == room2[prop]: return True return False def setMode(self, roomId, mode, setAlways=1): """ Set a room to a new mode """ room = self.rooms.get(roomId, None) if not room: return setAlways = int(setAlways) if setAlways or room['mode'] != mode: if room['mode'] != mode: room['mode'] = mode self.settings['rooms'] = self.rooms live = TelldusLive(self.context) if live.registered and room.get('responsible', '') == live.uuid: # Notify live if we are the owner msg = LiveMessage('RoomModeSet') msg.append({'id': roomId, 'mode': mode}) live.send(msg) self.__modeChanged(roomId, mode, 'room', room.get('name', '')) def syncRoom(self): TelldusLive(self.context).send(LiveMessage("roomsync-request")) @signal('modeChanged') def __modeChanged(self, objectId, modeId, objectType, objectName): """ Called every time the mode changes for a room """ pass @TelldusLive.handler('room') def __handleRoom(self, msg): data = msg.argument(0).toNative() if 'name' in data: if isinstance(data['name'], int): data['name'] = str(data['name']) else: data['name'] = data['name'].decode('UTF-8') live = TelldusLive(self.context) if data['action'] == 'set': oldResponsible = '' if data['id'] in self.rooms: # existing room room = self.rooms[data['id']] oldResponsible = room['responsible'] validKeys = ['name', 'color', 'content', 'icon', 'responsible'] for key in validKeys: if key in data: room[key] = data.get(key, '') if 'mode' in data and room['mode'] != data.get('mode', ''): room['mode'] = data.get('mode', '') self.__modeChanged(data['id'], room['mode'], 'room', room['name']) self.rooms[data['id']] = room else: # new room self.rooms[data['id']] = { 'name': data.get('name', ''), 'parent': data.get('parent', ''), 'color': data.get('color', ''), 'content': data.get('content', ''), 'icon': data.get('icon', ''), 'responsible': data['responsible'], 'mode': data.get('mode', ''), } if live.registered and \ (data['responsible'] == live.uuid or oldResponsible == live.uuid): room = self.rooms[data['id']] msg = LiveMessage('RoomSet') msg.append({ # No need to call get() on room here since we know every value has at least a # default value above 'id': data['id'], 'name': room['name'], 'parent': room['parent'], 'color': room['color'], 'content': room['content'], 'icon': room['icon'], 'responsible': room['responsible'], 'mode': room['mode'], }) live.send(msg) self.settings['rooms'] = self.rooms return if data['action'] == 'remove': room = self.rooms.pop(data['id'], None) if room is None: return if live.registered and room['responsible'] == live.uuid: msg = LiveMessage('RoomRemoved') msg.append({'id': data['id']}) live.send(msg) if len(self.getResponsibleRooms()) == 0: self.settings['roomlistEmpty'] = True self.roomlistEmpty = True self.settings['rooms'] = self.rooms return if data['action'] == 'setMode': self.setMode(data.get('id', None), data.get('mode', ''), data.get('setAlways', 1)) return if data['action'] == 'sync': rooms = data['rooms'] responsibleRooms = self.getResponsibleRooms() if not rooms and responsibleRooms: # list from server was completely empty but we have rooms locally, # this might be an error in the fetching, or we have added rooms locally # when offline. In any case, don't sync this time, just post our rooms # for next time self.reportRooms(responsibleRooms) return changedRooms = {} newRooms = {} removedRooms = [] for roomUUID in rooms: room = rooms[roomUUID] if room['responsible'] == live.uuid: # we are responsible for this room if roomUUID not in self.rooms: # this room does not exist locally anymore removedRooms.append(roomUUID) continue localRoom = self.rooms[roomUUID] if self.roomChanged(room, localRoom): changedRooms[roomUUID] = localRoom else: newRooms[roomUUID] = room newRooms.update(responsibleRooms) self.rooms = newRooms self.reportRooms(changedRooms, removedRooms) self.settings['rooms'] = self.rooms
class DeviceManager(Plugin): """The devicemanager holds and manages all the devices in the server""" implements(ITelldusLiveObserver) observers = ObserverCollection(IDeviceChange) def __init__(self): self.devices = [] self.s = Settings('telldus.devicemanager') self.nextId = self.s.get('nextId', 0) self.live = TelldusLive(self.context) self.registered = False self.__load() @mainthread def addDevice(self, device): """Call this function to register a new device to the device manager. .. note:: The :func:`localId` function in the device must return a unique id for the transport type returned by :func:`typeString` """ cachedDevice = None for i, delDevice in enumerate(self.devices): # Delete the cached device from loaded devices, since it is replaced by a confirmed/specialised one if delDevice.localId() == device.localId() and device.typeString( ) == delDevice.typeString() and not delDevice.confirmed(): cachedDevice = delDevice del self.devices[i] break self.devices.append(device) device.setManager(self) if not cachedDevice: # New device, not stored in local cache self.nextId = self.nextId + 1 device.setId(self.nextId) else: # Transfer parameters from the loaded one device.loadCached(cachedDevice) self.save() if not cachedDevice: self.__deviceAdded(device) if self.live.registered and device.isDevice(): (state, stateValue) = device.state() deviceDict = { 'id': device.id(), 'name': device.name(), 'methods': device.methods(), 'state': state, 'stateValue': stateValue, 'protocol': device.protocol(), 'model': device.model(), 'transport': device.typeString() } msg = LiveMessage("DeviceAdded") msg.append(deviceDict) self.live.send(msg) else: # Previously cached device is now confirmed, TODO notify Live! about this too? self.observers.deviceConfirmed(device) def device(self, deviceId): """Retrieves a device. Returns: the device specified by `deviceId` or None of no device was found """ for d in self.devices: if d.id() == deviceId: return d return None def findByName(self, name): for d in self.devices: if d.name() == name: return d return None def finishedLoading(self, type): """ Finished loading all devices of this type. If there are any unconfirmed, these should be deleted """ for device in self.devices: if device.typeString() == type and not device.confirmed(): self.removeDevice(device.id()) def removeDevice(self, deviceId): """Removes a device. .. warning:: This function may only be called by the module supplying the device since removing of a device may be transport specific. """ isDevice = True for i, device in enumerate(self.devices): if device.id() == deviceId: self.__deviceRemoved(deviceId) isDevice = self.devices[i].isDevice() del self.devices[i] break self.save() if self.live.registered and isDevice: msg = LiveMessage("DeviceRemoved") msg.append({'id': deviceId}) self.live.send(msg) def retrieveDevices(self, deviceType=None): """Retrieve a list of devices. Args: :deviceType: If this parameter is set only devices with this type is returned Returns: Returns a list of devices """ l = [] for d in self.devices: if deviceType is not None and d.typeString() != deviceType: continue l.append(d) return l @signal def sensorValueUpdated(self, device, valueType, value, scale): """ Called every time a sensors value is updated. """ if device.isSensor() == False: return self.observers.sensorValueUpdated(device, valueType, value, scale) if not self.live.registered or device.ignored(): # don't send if ignored return msg = LiveMessage("SensorEvent") sensor = { 'name': device.name(), 'protocol': device.protocol(), 'model': device.model(), 'sensor_id': device.id(), } battery = device.battery() if battery is not None: sensor['battery'] = battery msg.append(sensor) values = device.sensorValues() valueList = [] for valueType in values: for value in values[valueType]: valueList.append({ 'type': valueType, 'lastUp': str(int(time.time())), 'value': str(value['value']), 'scale': value['scale'] }) msg.append(valueList) self.live.send(msg) def stateUpdated(self, device, ackId=None, origin=None): if device.isDevice() == False: return extras = {} if ackId: extras['ACK'] = ackId if origin: extras['origin'] = origin else: extras['origin'] = 'Incoming signal' (state, stateValue) = device.state() self.__deviceStateChanged(device, state, stateValue) self.save() if not self.live.registered: return msg = LiveMessage("DeviceEvent") msg.append(device.id()) msg.append(state) msg.append(str(stateValue)) msg.append(extras) self.live.send(msg) def stateUpdatedFail(self, device, state, stateValue, reason, origin): if not self.live.registered: return if device.isDevice() == False: return extras = { 'reason': reason, } if origin: extras['origin'] = origin else: extras['origin'] = 'Unknown' (state, stateValue) = device.state() self.__deviceStateChanged(device, state, stateValue) msg = LiveMessage('DeviceFailEvent') msg.append(device.id()) msg.append(state) msg.append(stateValue) msg.append(extras) self.live.send(msg) @TelldusLive.handler('command') def __handleCommand(self, msg): args = msg.argument(0).toNative() action = args['action'] value = args['value'] if 'value' in args else None id = args['id'] device = None for dev in self.devices: if dev.id() == id: device = dev break def success(state, stateValue): if 'ACK' in args: device.setState(state, stateValue, ack=args['ACK']) # Abort the DeviceEvent this triggered raise DeviceAbortException() def fail(reason): # We failed to set status for some reason, nack the server if 'ACK' in args: msg = LiveMessage('NACK') msg.append({ 'ackid': args['ACK'], 'reason': reason, }) self.live.send(msg) # Abort the DeviceEvent this triggered raise DeviceAbortException() device.command(action, value, success=success, failure=fail) @TelldusLive.handler('device') def __handleDeviceCommand(self, msg): args = msg.argument(0).toNative() if 'action' not in args: return if args['action'] == 'setName': if 'name' not in args or args['name'] == '': return for dev in self.devices: if dev.id() != args['device']: continue if type(args['name']) is int: dev.setName(str(args['name'])) else: dev.setName(args['name'].decode('UTF-8')) if dev.isDevice(): self.__sendDeviceReport() if dev.isSensor: self.__sendSensorReport( True) # force name change even for ignored sensor return @TelldusLive.handler('reload') def __handleSensorUpdate(self, msg): reloadType = msg.argument(0).toNative() if reloadType != 'sensor': # not for us return data = msg.argument(1).toNative() if not msg.argument(2) or 'sensorId' not in msg.argument(2).toNative(): # nothing to do, might be an orphaned zwave sensor return sensorId = msg.argument(2).toNative()['sensorId'] updateType = data['type'] for dev in self.devices: if dev.id() == sensorId: if updateType == 'updateignored': value = data['ignored'] if dev.ignored() == value: return dev.setIgnored(value) self.__sendSensorChange(sensorId, updateType, value) return def liveRegistered(self, msg): self.registered = True self.__sendDeviceReport() self.__sendSensorReport() def __load(self): self.store = self.s.get('devices', []) for dev in self.store: if 'type' not in dev or 'localId' not in dev: continue # This should not be possible d = CachedDevice(dev) # If we have loaded this device from cache 5 times in a row it's # considered dead if d.loadCount() < 5: self.devices.append(d) @signal('deviceAdded') def __deviceAdded(self, device): """ Called every time a device is added/created """ self.observers.deviceAdded(device) @signal('deviceRemoved') def __deviceRemoved(self, deviceId): """ Called every time a device is removed. The parameter deviceId is the old device id. The ref to the device is no longer available """ self.observers.deviceRemoved(deviceId) @signal('deviceStateChanged') def __deviceStateChanged(self, device, state, stateValue): """ Called every time the state of a device is changed. """ self.observers.stateChanged(device, state, stateValue) def save(self): data = [] for d in self.devices: (state, stateValue) = d.state() data.append({ "id": d.id(), "loadCount": d.loadCount(), "localId": d.localId(), "type": d.typeString(), "name": d.name(), "params": d.params(), "methods": d.methods(), "state": state, "stateValue": stateValue, "ignored": d.ignored() }) self.s['devices'] = data self.s['nextId'] = self.nextId def __sendDeviceReport(self): if not self.live.registered: return l = [] for d in self.devices: if not d.isDevice(): continue (state, stateValue) = d.state() device = { 'id': d.id(), 'name': d.name(), 'methods': d.methods(), 'state': state, 'stateValue': stateValue, 'protocol': d.protocol(), 'model': d.model(), 'transport': d.typeString(), 'ignored': d.ignored() } battery = d.battery() if battery is not None: device['battery'] = battery l.append(device) msg = LiveMessage("DevicesReport") msg.append(l) self.live.send(msg) def __sendSensorChange(self, sensorid, valueType, value): msg = LiveMessage("SensorChange") device = None for d in self.devices: if d.id() == sensorid: device = d break if not device: return sensor = { 'protocol': device.typeString(), 'model': device.model(), 'sensor_id': device.id(), } msg.append(sensor) msg.append(valueType) msg.append(value) self.live.send(msg) def __sendSensorReport(self, forceIgnored=False): if not self.live.registered: return l = [] for d in self.devices: if d.isSensor() == False or (d.ignored() and not forceIgnored): continue sensorFrame = [] sensor = { 'name': d.name(), 'protocol': d.protocol(), 'model': d.model(), 'sensor_id': d.id(), } battery = d.battery() if battery is not None: sensor['battery'] = battery sensorFrame.append(sensor) valueList = [] # TODO(micke): Add current values sensorFrame.append(valueList) l.append(sensorFrame) msg = LiveMessage("SensorsReport") msg.append(l) self.live.send(msg)
class SceneManager(Plugin): implements(ITelldusLiveObserver) def __init__(self): self.scenes = {} self.deviceManager = DeviceManager(self.context) for sceneId in self.config('scenes'): device = SceneDevice(sceneId) self.scenes[sceneId] = device self.deviceManager.addDevice(device) self.deviceManager.finishedLoading('scene') def addDevice(self, name, devices): if type(devices) != dict: return sceneId = str(uuid.uuid4()) device = SceneDevice(sceneId) device.setName(name) device.setParams({'devices': devices}) self.scenes[sceneId] = device self.deviceManager.addDevice(device) scenes = self.config('scenes') scenes.append(sceneId) self.setConfig('scenes', scenes) @TelldusLive.handler('scene') def __handleCommand(self, msg): data = msg.argument(0).toNative() action = data['action'] if action == 'addScene': self.addDevice(data['name'], data['devices']) elif action == 'editScene': deviceId = data['device'] for sceneId in self.scenes: device = self.scenes[sceneId] if device.id() == deviceId: device.setParams({ 'devices': data['devices'], }) device.paramUpdated('') break elif action == 'sceneInfo': deviceId = data['device'] for sceneId in self.scenes: device = self.scenes[sceneId] if device.id() == deviceId: params = device.params() params['deviceId'] = deviceId live = TelldusLive(self.context) live.pushToWeb('scene', 'sceneInfo', params) return elif action == 'remove': deviceId = data['device'] for sceneId in self.scenes: device = self.scenes[sceneId] if device.id() == deviceId: self.deviceManager.removeDevice(deviceId) del self.scenes[sceneId] return
class Client(Plugin): implements(ISignalObserver) def __init__(self): self._ready = False self._running = True self._knownDevices = None Application().registerShutdown(self.onShutdown) self.client = mqtt.Client(userdata = self) self.client.on_disconnect = self.onDisconnect self.client.on_connect = self.onConnect self.client.on_message = self.onMessage if self.config('hostname'): Application().queue(self.connect) def getSlugifiedConfig(self, name): return slugify(self.config(name)) def getBaseTopic(self): return self.getSlugifiedConfig('base_topic') def getDeviceName(self): return self.getSlugifiedConfig('device_name') def onShutdown(self): self._running = False self.disconnect() def updateConfig(self): self.debug('Updating config.devices_configured to : %s' % self._knownDevices) try: self.setConfig('devices_configured', self._knownDevices) except Exception as e: self.debug('updateConfig error %s' % str(e)) def getKnownDevices(self): if not self._knownDevices: if self.config('devices_configured'): self._knownDevices = [tuple(x) for x in json.loads(self.config('devices_configured'))] else: self._knownDevices = [] return self._knownDevices def setKnownDevices(self, devices): self._knownDevices = devices self.updateConfig() def isKnownDevice(self, type, devId, deviceId): devices = self.getKnownDevices() return (type, str(devId), str(deviceId)) in devices def addKnownDevice(self, type, devId, deviceId): devices = self.getKnownDevices() devices.append((type, str(devId), str(deviceId))) self.setKnownDevices(devices) def delKnownDevice(self, type, devId, deviceId): devices = self.getKnownDevices() devices.remove((type, devId, deviceId)) self.setKnownDevices(devices) def configWasUpdated(self, key, value): self.debug('config updated %s %s' % (key, value)) if key == 'state_retain' and value == '0': # clear retained messages try: for type, _, fullId in self.getKnownDevices(): deviceTopic = self.getDeviceTopic(type, fullId) self.debug('clear retain for %s/state' % deviceTopic) self.client.publish('%s/state' % deviceTopic, None, retain = True) except Exception as e: self.debug('clear retain error %s' % str(e)) if not key in ['devices_configured', 'state_retain']: self.disconnect() Application().queue(self.connect) def tearDown(self): try: for type, _, fullId in self.getKnownDevices(): deviceTopic = self.getDeviceTopic(type, fullId) self.client.publish('%s/config' % deviceTopic, '', retain = True) self.client.publish('%s/state' % deviceTopic, '', retain = True) except Exception as e: self.debug('tearDown %s' % str(e)) self._knownDevices = [] self.updateConfig() self.disconnect() def disconnect(self): #self.client.disconnect() self.client.loop_stop() self._running = False self._ready = False def connect(self): username = self.config('username') password = self.config('password') base_topic = self.getBaseTopic() device_name = self.getDeviceName() hostname = self.config('hostname') port = self.config('port') if username != '': self.client.username_pw_set(username, password) self.client.will_set( '%s/%s/available' % (base_topic, device_name) if base_topic \ else '%s/available' % device_name, 'offline', 0, True ) self.client.connect_async(hostname, port, keepalive=10) self.client.loop_start() def debug(self, msg): logging.info('HASS DBG: %s', msg) base_topic = self.getBaseTopic() device_name = self.getDeviceName() debugTopic = ( '%s/%s/debug' % (base_topic, device_name) if base_topic \ else '%s/debug' % device_name ) time = strftime('%H:%M:%S', gmtime()) self.client.publish(debugTopic, '%s - %s' % (time, msg)) def getDeviceType(self, device): capabilities = device.methods() devicetype = device.allParameters().get('devicetype') if devicetype == Device.TYPE_THERMOSTAT: return 'climate' elif devicetype == Device.TYPE_REMOTE_CONTROL: return 'remote' elif capabilities & Device.DIM: return 'light' elif capabilities & Device.TURNON: return 'switch' elif capabilities & Device.UP: return 'cover' elif capabilities & Device.BELL: return 'switch' else: return 'binary_sensor' def getDeviceTopic(self, type, id): discoverTopic = self.getSlugifiedConfig('discovery_topic') telldusName = self.getDeviceName() or 'telldus' if type in ['remote']: type = 'binary_sensor' return '%s/%s/%s/%s' % (discoverTopic, type, telldusName, id) def getSensorId(self, deviceId, valueType, scale): return slugify('%s_%s_%s' % (deviceId, valueType, scale)) def getBatteryId(self, device): return slugify('%s_%s_battery' % (getMacAddr(), device.id())) def formatBattery(self, battery): return { Device.BATTERY_LOW: 1, Device.BATTERY_UNKNOWN: None, Device.BATTERY_OK: 100 }.get(battery, int(battery)) def formatScale(self, type, scale): return ScaleConverter.get(type, {}).get(scale, '') def getClimateModes(self, device): params = device.allParameters() if hasattr(device, 'allParameters') else device.parameters() modes = params.get('thermostat', {}).get('modes', ['auto']) return modes def getClimateMode(self, device): state, stateValue = device.state() thermoValues = device.stateValue(Device.THERMOSTAT) availModes = self.getClimateModes(device) return thermoValues.get('mode') or availModes[0] def getClimateSetPoint(self, device, mode = None): thermoValues = device.stateValue(Device.THERMOSTAT) setpoint = thermoValues.get('setpoint') if isinstance(setpoint, dict) and mode: setpoint = setpoint.get(mode) return setpoint def climateState(self, device): try: if self.getDeviceType(device) != "climate": return thermoValues = device.stateValue(Device.THERMOSTAT) sensorValues = device.sensorValues() tempValues = sensorValues[Device.TEMPERATURE] mode = self.getClimateMode(device) setpoint = self.getClimateSetPoint(device, mode) self.debug( 'climateState %s, sensorValues: %s, thermovalues: %s, setPoint: %s, mode: %s' % \ (device.id(), sensorValues, thermoValues, setpoint, mode) ) payload = { 'setpoint': setpoint, 'mode': { Thermostat.MODE_FAN: 'fan_only' }.get(mode, mode), } if device.isSensor() and sensorValues[Device.TEMPERATURE]: value = tempValues[0] if isinstance(tempValues, list) else tempValues payload.update({ 'temperature': value.get('value'), }) stateTopic = '%s/state' % self.getDeviceTopic("climate", device.id()) self.client.publish(stateTopic, json.dumps(payload), retain = True) except Exception as e: self.debug('climateState exception %s' % str(e)) def deviceState(self, device): try: state, stateValue = device.state() deviceType = self.getDeviceType(device) if not deviceType: return self.debug('deviceState %s, state: %s, value: %s' % (device.id(), state, stateValue)) stateTopic = '%s/state' % self.getDeviceTopic(deviceType, device.id()) payload = '' retain = True if deviceType in ['light']: if state == Device.DIM: payload = json.dumps({ 'state': 'ON' if stateValue and int(stateValue) > 0 else 'OFF', 'brightness': int(stateValue) if stateValue else 0 }) else: payload = json.dumps({ 'state': 'ON' if state == Device.TURNON else 'OFF', 'brightness': (int(stateValue) if stateValue else 255) if state == Device.TURNON else 0 }) elif deviceType in ['remote']: payload = 'ON' if state in [Device.TURNON] else 'OFF' retain = False elif deviceType in ['switch']: payload = 'ON' if state in [Device.TURNON, Device.BELL] else 'OFF' elif deviceType in ['binary_sensor']: payload = 'ON' if state in [Device.TURNON] else 'OFF' elif deviceType in ['cover']: payload = 'OPEN' if state == Device.UP else 'CLOSED' if state == Device.DOWN else 'STOP' use_retain = retain and (self.config('state_retain') == 1) self.client.publish(stateTopic, payload, retain = use_retain) if state == Device.BELL: self.client.publish(stateTopic, 'OFF', retain = use_retain) except Exception as e: self.debug('deviceState exception %s' % str(e)) def sensorState(self, device, valueType, scale): try: sensorId = self.getSensorId(device.id(), valueType, scale) for sensor in device.sensorValues()[valueType]: if sensor['scale'] == scale: self.debug('sensorState %s' % sensor) payload = { 'value': sensor['value'], 'lastUpdated': sensor.get('lastUpdated') } self.client.publish( '%s/state' % self.getDeviceTopic('sensor', sensorId), json.dumps(payload), retain = True ) except Exception as e: self.debug('sensorState exception %s' % str(e)) def batteryState(self, device): try: self.client.publish( '%s/state' % self.getDeviceTopic('sensor', self.getBatteryId(device)), self.formatBattery(device.battery()), retain = True ) except Exception as e: self.debug('batteryState exception %s' % str(e)) def publish_discovery(self, device, type, deviceId, config): base_topic = self.getBaseTopic() device_name = self.getDeviceName() config.update({ 'unique_id': '%s_%s' % (getMacAddr(), deviceId), 'availability_topic': ( '%s/%s/available' % (base_topic, device_name) if base_topic \ else '%s/available' % device_name ), 'device': { 'identifiers': device.getOrCreateUUID(), 'manufacturer': device.protocol().title(), 'model': device.model().title(), # Model is always 'n/a' but is supposed to be updated. 'name': device.name(), 'via_device': getMacAddr(), } }) self.client.publish( '%s/config' % self.getDeviceTopic(type, deviceId), json.dumps(config), retain = True ) return (type, str(device.id()), str(deviceId)) def remove_discovery(self, type, devId, fullId): deviceTopic = self.getDeviceTopic(type, fullId) self.debug('remove discovered device %s,%s,%s : %s' % (type, devId, fullId, deviceTopic)) self.client.publish('%s/config' % deviceTopic, '', retain = True) self.client.publish('%s/state' % deviceTopic, '', retain = True) def discoverClimate(self, device): deviceTopic = self.getDeviceTopic('climate', device.id()) try: sensorValues = device.sensorValues() thermoValues = device.stateValue(Device.THERMOSTAT) availModes = self.getClimateModes(device) climateConfig = { 'name': device.name(), 'temperature_command_topic': '%s/set/setpoint' % deviceTopic, 'json_attributes_topic': '%s/attr' % deviceTopic, 'json_attributes_template': '{{ json_value }}', } if device.isSensor() and sensorValues[Device.TEMPERATURE]: climateConfig.update({ 'current_temperature_topic': '%s/state' % deviceTopic, 'current_temperature_template': '{{ value_json.temperature }}', # Only after https://github.com/home-assistant/home-assistant/pull/30602 #'unit_of_measurement': ScaleConverter.get(Device.TEMPERATURE).get() }) if availModes: climateConfig.update({ 'modes': availModes, 'mode_command_topic': '%s/set/mode' % deviceTopic, 'mode_state_topic': '%s/state' % deviceTopic, 'mode_state_template': '{{ value_json.mode }}', }) if thermoValues.get('setpoint', None) is not None: climateConfig.update({ 'temperature_state_topic': '%s/state' % deviceTopic, 'temperature_state_template': '{{ value_json.setpoint }}', }) self.client.publish( '%s/attr' % deviceTopic, json.dumps({ 'modes': availModes }), retain = True ) return self.publish_discovery(device, 'climate', device.id(), climateConfig) except Exception as e: self.debug('discoverThermostat %s' % str(e)) def discoverBattery(self, device): deviceTopic = self.getDeviceTopic('sensor', self.getBatteryId(device)) try: sensorConfig = { 'name': '%s - Battery' % device.name(), 'unit_of_measurement': '%', 'device_class': 'battery', 'state_topic': '%s/state' % deviceTopic } return self.publish_discovery(device, 'sensor', self.getBatteryId(device), sensorConfig) except Exception as e: self.debug('discoverBattery %s' % str(e)) def discoverSensor(self, device, valueType, scale): sensorId = self.getSensorId(device.id(), valueType, scale) deviceTopic = self.getDeviceTopic("sensor", sensorId) try: sensorConfig = { 'name': '%s %s - %s' % ( device.name(), Device.sensorTypeIntToStr(valueType), self.formatScale(valueType, scale) ), 'state_topic': '%s/state' % deviceTopic, 'value_template': '{{ value_json.value }}', 'json_attributes_topic': '%s/state' % deviceTopic, 'unit_of_measurement': self.formatScale(valueType, scale), } if ClassConverter.get(valueType, None): sensorConfig.update({ 'device_class': ClassConverter.get(valueType, None) }) sensorId = self.getSensorId(device.id(), valueType, scale) return self.publish_discovery(device, 'sensor', sensorId, sensorConfig) except Exception as e: self.debug('discoverSensor %s' % str(e)) def discoverDevice(self, device): try: deviceType = self.getDeviceType(device) if not deviceType: return None deviceTopic = self.getDeviceTopic(deviceType, device.id()) deviceConfig = { 'name': device.name(), 'state_topic': '%s/state' % deviceTopic } if deviceType in ['remote']: deviceConfig.update({ 'expire_after': 1 }) if deviceType in ['switch', 'light', 'cover']: deviceConfig.update({ 'command_topic': '%s/set' % deviceTopic }) if deviceType == 'light': deviceConfig.update({ 'schema': 'json', 'brightness': True }) if deviceType == 'switch' and (device.methods() & Device.BELL): deviceConfig.update({ 'payload_on': 'BELL' }) self.debug('device is device: %s' % json.dumps({ 'deviceType': deviceType, 'deviceTopic': deviceTopic, 'deviceConfig': deviceConfig })) return self.publish_discovery(device, deviceType, device.id(), deviceConfig) except Exception as e: self.debug('discoverDevice %s' % str(e)) def discovery(self, device): result = [] try: if device.battery() and device.battery() != Device.BATTERY_UNKNOWN: self.debug('device %s has battery' % device.id()) result.append(self.discoverBattery(device)) self.batteryState(device) if device.deviceType() == Device.TYPE_THERMOSTAT: self.debug('device %s is climate' % device.id()) result.append(self.discoverClimate(device)) self.climateState(device) else: if device.isSensor(): self.debug('device %s has sensors' % device.id()) for type, sensors in device.sensorValues().items(): self.debug('sensortype %s has %s' % (type, sensors)) for sensor in sensors: result.append(self.discoverSensor(device, type, sensor['scale'])) self.sensorState(device, type, sensor['scale']) if device.isDevice(): self.debug('device %s is a device' % device.id()) item = self.discoverDevice(device) result.append(item) if item[0] != "remote": self.deviceState(device) except Exception as e: self.debug('discovery %s' % str(e)) return [x for x in result if x] def publish_hub_device(self): base_topic = self.getBaseTopic() device_name = self.getDeviceName() deviceId = 'hub' config = { 'name': device_name, 'state_topic': ( '%s/%s/available' % (base_topic, device_name) if base_topic \ else '%s/available' % device_name ), 'payload_on': 'online', 'payload_off': 'offline', 'device_class': 'connectivity', 'unique_id': '%s_%s' % (getMacAddr(), deviceId), 'availability_topic': ( '%s/%s/available' % (base_topic, device_name) if base_topic \ else '%s/available' % device_name ), 'device': { 'identifiers': getMacAddr(), 'connections': [['mac', getMacAddr(False)]], 'manufacturer': 'Telldus Technologies', 'model': Board.product().replace('-', ' ').title().replace(' ', '_'), 'name': device_name, 'sw_version': Board.firmwareVersion() } } self.client.publish( '%s/config' % self.getDeviceTopic('binary_sensor', deviceId), json.dumps(config), retain = True ) return (deviceId, deviceId, deviceId) def run_discovery(self): self.debug('discover devices') try: # publish devices publishedDevices = [self.publish_hub_device()] deviceManager = DeviceManager(self.context) devices = deviceManager.retrieveDevices() for device in devices: try: self.debug(json.dumps({ 'deviceId': device.id(), 'type': self.getDeviceType(device), 'name': device.name(), 'isDevice': device.isDevice(), 'isSensor': device.isSensor(), 'methods': device.methods(), 'battery': device.battery(), 'parameters': device.allParameters() if hasattr(device, 'allParameters') else device.parameters(), 'typeStr': device.typeString(), 'sensors': device.sensorValues(), 'state': device.state() })) publishedDevices.extend(self.discovery(device)) except Exception as e: self.debug('run_discovery device exception %s' % str(e)) for type, devId, fullId in list(set(self.getKnownDevices()) - set(publishedDevices)): self.remove_discovery(type, devId, fullId) self.setKnownDevices(publishedDevices) except Exception as e: self.debug('run_discovery exception %s' % str(e)) def onConnect(self, client, userdata, flags, result): base_topic = userdata.config('base_topic') device_name = userdata.config('device_name') client.publish( '%s/%s/available' % (base_topic, device_name) if base_topic \ else '%s/available' % device_name, 'online', 0, True ) try: userdata.run_discovery() #subscribe to commands userdata.debug('subscribing') client.subscribe('%s/+/%s/+/set' % (userdata.config('discovery_topic'), device_name)) client.subscribe('%s/+/%s/+/set/+' % (userdata.config('discovery_topic'), device_name)) userdata._ready = True except Exception as e: userdata.debug('OnConnect error %s' % str(e)) def onDisconnect(self, client, userdata, rc): self.debug("Mqtt disconnected") userdata._ready = False @slot('deviceAdded') def onDeviceAdded(self, device): if not self._running: return try: self.debug('Device added %s' % device.id()) devices = self.getKnownDevices() devices.extend(self.discovery(device)) self.setKnownDevices(devices) except Exception as e: self.debug('onDeviceAdded error %s' % str(e)) @slot('deviceRemoved') def onDeviceRemoved(self, deviceId): if not self._running: return try: self.debug('Device removed %s' % deviceId) devices = self.getKnownDevices() for type, devId, fullId in devices: if devId == str(deviceId): self.remove_discovery(type, devId, fullId) devices = [x for x in devices if x[1] != str(deviceId)] self.setKnownDevices(devices) except Exception as e: self.debug('onDeviceRemoved error %s' % str(e)) @slot('deviceUpdated') def onDeviceUpdated(self, device): if not self._running: return try: self.debug('Device updated %s' % device.id()) devices = self.getKnownDevices() for type, devId, fullId in devices: if devId == str(device.id()): self.remove_discovery(type, devId, fullId) devices = [x for x in devices if x[1] != str(device.id())] devices.extend(self.discovery(device)) self.setKnownDevices(devices) except Exception as e: self.debug('onDeviceUpdated error %s' % str(e)) @slot('rf433RawData') def onRawData(self, data,*__args, **__kwargs): if not self._running: return self.debug(json.dumps(data)) @slot('sensorValueUpdated') def onSensorValueUpdated(self, device, valueType, value, scale): if not self._ready or not self._running: return self.debug(json.dumps({ 'type': 'sensorValueUpdated', 'deviceId': device.id(), 'valueType': valueType, 'value': value, 'scale': scale, 'battery': device.battery() })) if self.getDeviceType(device) == 'climate': if not self.isKnownDevice('climate', device.id(), device.id()): self.debug('A wild climate device appeared! deviceId: %s' % device.id()) type, devId, deviceId = self.discoverClimate(device) self.addKnownDevice(type, devId, deviceId) self.climateState(device) else: sensorId = self.getSensorId(device.id(), valueType, scale) if not self.isKnownDevice('sensor', device.id(), sensorId): self.debug('A wild sensor appeared! deviceId: %s, sensorId: %s' % (device.id(), sensorId)) type, devId, deviceId = self.discoverSensor(device, valueType, scale) self.addKnownDevice(type, devId, deviceId) self.sensorState(device, valueType, scale) if device.battery() and device.battery() != Device.BATTERY_UNKNOWN: self.batteryState(device) @slot('deviceStateChanged') def onDeviceStateChanged(self, device, state, stateValue, origin=None): if not self._ready or not self._running: return deviceType = self.getDeviceType(device) self.debug(json.dumps({ 'type': 'deviceStateChanged', 'deviceId': device.id(), 'state': state, 'stateValue': stateValue, 'origin': origin, 'devicetype': deviceType })) if not deviceType: return if self.getDeviceType(device) == 'climate': if not self.isKnownDevice('climate', device.id(), device.id()): self.debug('A wild climate device appeared! deviceId: %s' % device.id()) type, devId, deviceId = self.discoverClimate(device) self.addKnownDevice(type, devId, deviceId) self.climateState(device) else: if not self.isKnownDevice(deviceType, device.id(), device.id()): self.debug('A wild device appeared! type: %s, deviceId: %s' % (deviceType, device.id())) type, devId, deviceId = self.discoverDevice(device) self.addKnownDevice(type, devId, deviceId) self.deviceState(device) if device.battery(): self.batteryState(device) def onMessage(self, client, userdata, msg): try: topic = msg.topic payload = msg.payload if topic.split('/')[-1] == 'set': topicType = 'set' else: topicType = topic.split('/')[-1] deviceManager = DeviceManager(userdata.context) device_id = int(msg.topic.split('/')[3]) device = deviceManager.device(device_id) deviceType = userdata.getDeviceType(device) if not deviceType: return userdata.debug(json.dumps({ 'type': 'command', 'device_id': device_id, 'device_type': deviceType, 'command': payload })) def failed(reason, **__kwargs): self.debug('Device command failed: %s' % reason) if deviceType == 'climate': if topicType == 'mode': mode = { 'fan_only': Thermostat.MODE_FAN }.get(payload, payload) setpoint = self.getClimateSetPoint(device, mode) if setpoint is not None: value = { 'mode': mode, 'changeMode': True, 'temperature': self.getClimateSetPoint(device, mode) } self.debug('Command THERMOSTAT value: %s' % value) device.command( Device.THERMOSTAT, value = value, origin = 'mqtt_hass', failure = failed ) else: self.debug('Can not set mode, setpoint none') if topicType == 'setpoint': setpoint = float(payload) if payload else None if setpoint is not None: value = { 'mode': self.getClimateMode(device), 'changeMode': False, 'temperature': setpoint } self.debug('Command THERMOSTAT value: %s' % value) device.command( Device.THERMOSTAT, value = value, origin = 'mqtt_hass', failure = failed ) else: self.debug('Can not update setpoint, setpoint none (%s)' % payload) elif deviceType == 'light': payload = json.loads(payload) if 'brightness' in payload: if int(payload['brightness']) == 0: device.command( Device.TURNOFF, origin = 'mqtt_hass', failure = failed ) else: device.command( Device.DIM, value = int(payload['brightness']), origin = 'mqtt_hass', failure = failed ) else: device.command( Device.TURNON if payload['state'].upper() == 'ON' \ else Device.TURNOFF, value = 255, origin = 'mqtt_hass', failure = failed ) elif deviceType == 'switch': device.command( Device.TURNON if payload.upper() == 'ON' \ else Device.BELL if payload.upper() == 'BELL' \ else Device.TURNOFF, origin = 'mqtt_hass', failure = failed ) elif deviceType == 'cover': device.command( Device.UP if payload.upper() == 'OPEN' \ else Device.DOWN if payload.upper() == 'CLOSE' else \ Device.STOP, origin = 'mqtt_hass', failure = failed ) except Exception as e: userdata.debug('onMessage exception %s' % str(e))
class Lua(Plugin): implements(IWebRequestHandler) implements(IWebReactHandler) implements(ISignalObserver) implements(IApiCallHandler) def __init__(self): self.scripts = [] self.load() self.fileobserver = Observer() self.fileobserver.schedule(FileChangedHandler(self), Board.luaScriptPath()) self.fileobserver.start() Application().registerShutdown(self.shutdown) @apicall('lua', 'call') def apiCallFunction(self, script, function, **kwargs): """ Calls a lua function in a script. Required methods: script: The name of the script. With or witout the .lua extension. function: The name of the function to call. Any extra parameters will be sent to the function as a lua table as the first parameter. """ if not script.endswith('.lua'): script = '%s.lua' % script for s in self.scripts: if s.name != script: continue if not s.call(function, kwargs): raise Exception('Script %s does not define function "%s"' % (script, function)) return True raise Exception('Script %s not found' % script) def fileCreated(self, filename): if not filename.endswith('.lua'): return for script in self.scripts: if script.filename == filename: # Already loaded return script = LuaScript(filename, self.context) self.scripts.append(script) script.load() def fileRemoved(self, filename): for i, script in enumerate(self.scripts): if script.filename == filename: script.shutdown() del self.scripts[i] break def getReactComponents(self): return { 'lua': { 'title': 'Lua scripts (beta)', 'script': 'lua/lua.js', 'tags': ['menu'], } } def matchRequest(self, plugin, path): if plugin != 'lua': return False if path in ['delete', 'new', 'save', 'script', 'scripts', 'signals']: return True return False def handleRequest(self, plugin, path, params, **kwargs): script = None if path == 'save': if 'script' not in cherrypy.request.body.params or 'code' not in cherrypy.request.body.params: return WebResponseJson({ 'error': 'Malformed request, parameter script or code missing' }) self.saveScript(cherrypy.request.body.params['script'], cherrypy.request.body.params['code']) return WebResponseJson({'success': True}) elif path == 'new': okChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-' if 'name' not in params: return WebResponseJson({'error': 'Invalid script name'}) name = ''.join([c for c in params['name'] if c in okChars]) if len(name) == 0: return WebResponseJson({'error': 'Invalid script name'}) filename = '%s/%s.lua' % (Board.luaScriptPath(), name) with open(filename, 'w') as f: f.write( '-- File: %s.lua\n\nfunction onInit()\n\tprint("Hello world")\nend' % name) self.fileCreated(filename) return WebResponseJson({'success': True, 'name': '%s.lua' % name}) elif path == 'delete': if 'name' not in params: return WebResponseJson({'error': 'Invalid script name'}) for s in self.scripts: if s.name == params['name']: os.remove(s.filename) self.fileRemoved(s.filename) break return WebResponseJson({'success': True}) elif path == 'script': for s in self.scripts: if s.name == params['name']: return WebResponseJson({ 'name': params['name'], 'code': s.code, }) return WebResponseJson({}) elif path == 'scripts': return WebResponseJson([{ 'name': script.name } for script in sorted(self.scripts, key=lambda s: s.name.lower()) ]) elif path == 'signals': return WebResponseJson(self.signals()) elif 'edit' in params: for s in self.scripts: if s.name == params['edit']: script = s break return None def load(self): for f in glob.glob('%s/*.lua' % Board.luaScriptPath()): self.scripts.append(LuaScript(f, self.context)) for s in self.scripts: s.load() def saveScript(self, scriptName, code): for script in self.scripts: if script.name != scriptName: continue with open(script.filename, 'w') as f: f.write(code) # overlayfs does not support inofify for filechanges so we need to signal manually script.reload() break def signals(self): signals = [{ 'name': 'on%s%s' % (x[0].upper(), x[1:]), 'doc': SignalManager.signals[x].doc(), 'args': SignalManager.signals[x].args(), } for x in SignalManager.signals] signals.append({ 'name': 'onInit', 'doc': 'Called when the script is loaded', 'args': [], }) return signals @slot() def slot(self, message, *args, **kwargs): name = 'on%s%s' % (message[0].upper(), message[1:]) for script in self.scripts: script.call(name, *args) def shutdown(self): self.fileobserver.stop() self.fileobserver.join() for script in self.scripts: script.shutdown()
class Netatmo(Plugin): implements(IWebRequestHandler) implements(IWebReactHandler) supportedTypes = { 'Temperature': (Sensor.TEMPERATURE, Sensor.SCALE_TEMPERATURE_CELCIUS), 'Humidity': (Sensor.HUMIDITY, Sensor.SCALE_HUMIDITY_PERCENT), #'CO2': (Sensor.UNKNOWN, Sensor.SCALE_UNKNOWN), #'Noise':, 'Pressure': (Sensor.BAROMETRIC_PRESSURE, Sensor.SCALE_BAROMETRIC_PRESSURE_KPA), 'Rain': (Sensor.RAINRATE, Sensor.SCALE_RAINRATE_MMH), 'sum_rain_24': (Sensor.RAINTOTAL, Sensor.SCALE_RAINTOTAL_MM), 'WindAngle': (Sensor.WINDDIRECTION, Sensor.SCALE_WIND_DIRECTION), 'WindStrength': (Sensor.WINDAVERAGE, Sensor.SCALE_WIND_VELOCITY_MS), 'GustStrength': (Sensor.WINDGUST, Sensor.SCALE_WIND_VELOCITY_MS), } products = { # 'NAMain': {} # Base station 'NAModule1': { 'batteryMax': 6000, 'batteryMin': 3600 }, # Outdoor module 'NAModule4': { 'batteryMax': 6000, 'batteryMin': 4200 }, # Additional indoor module 'NAModule3': { 'batteryMax': 6000, 'batteryMin': 3600 }, # Rain gauge 'NAModule2': { 'batteryMax': 6000, 'batteryMin': 3950 }, # Wind gauge # 'NAPlug': {}, # Thermostat relay/plug # 'NATherm1': {}, # Thermostat module } def __init__(self): self.deviceManager = DeviceManager(self.context) self.sensors = {} self.loaded = False self.clientId = '' self.clientSecret = '' config = self.config('oauth') self.accessToken = config.get('accessToken', '') self.refreshToken = config.get('refreshToken', '') if self.accessToken is not '': self.configuration['oauth'].activated = True self.tokenTTL = config.get('tokenTTL', 0) Application().registerScheduledTask(self.__requestNewValues, minutes=10, runAtOnce=True) def getReactComponents(self): return { 'netatmo': { 'title': 'Netatmo', 'script': 'netatmo/netatmo.js', } } def matchRequest(self, plugin, path): if plugin != 'netatmo': return False if path in ['activate', 'code', 'logout']: return True return False def handleRequest(self, plugin, path, params, request, **kwargs): # Web requests if path in ['activate', 'code']: service = rauth.OAuth2Service( client_id=self.clientId, client_secret=self.clientSecret, access_token_url='https://api.netatmo.net/oauth2/token', authorize_url='https://api.netatmo.net/oauth2/authorize') if path == 'activate': params = { 'redirect_uri': '%s/netatmo/code' % request.base(), 'response_type': 'code' } url = service.get_authorize_url(**params) return WebResponseJson({'url': url}) if path == 'code': data = { 'code': params['code'], 'grant_type': 'authorization_code', 'redirect_uri': '%s/netatmo/code' % request.base() } session = service.get_auth_session( data=data, decoder=self.__decodeAccessToken) return WebResponseRedirect('%s/plugins?settings=netatmo' % request.base()) if path == 'logout': self.accessToken = '' self.refreshToken = '' self.tokenTTL = 0 self.setConfig( 'oauth', { 'accessToken': self.accessToken, 'refreshToken': self.refreshToken, 'tokenTTL': self.tokenTTL, }) self.configuration['oauth'].activated = False return WebResponseJson({'success': True}) return None def __addUpdateDevice(self, data): if data['_id'] not in self.sensors: sensor = NetatmoModule(data['_id'], data['module_name'], data['type']) self.deviceManager.addDevice(sensor) self.sensors[data['_id']] = sensor else: sensor = self.sensors[data['_id']] for dataType in Netatmo.supportedTypes: if dataType not in data['dashboard_data']: continue valueType, scale = Netatmo.supportedTypes[dataType] value = data['dashboard_data'][dataType] if dataType == 'WindStrength' or dataType == 'GustStrength': value = round(value / 3.6, 2) # Data is reported in km/h, we want m/s elif dataType == 'Pressure': value = round(value / 10.0) # Data is reported in mbar, we want kPa sensor.setSensorValue(valueType, value, scale) if 'battery_vp' in data and data['type'] in Netatmo.products: product = Netatmo.products[data['type']] battery = 1.0 * max(min(data['battery_vp'], product['batteryMax']), product['batteryMin']) sensor.batteryLevel = int( (battery - product['batteryMin']) / (product['batteryMax'] - product['batteryMin']) * 100) @mainthread def __parseValues(self, data): if 'body' not in data: return body = data['body'] if 'devices' not in body: return devices = body['devices'] for device in devices: self.__addUpdateDevice(device) for module in device['modules']: self.__addUpdateDevice(module) if self.loaded == False: self.loaded = True self.deviceManager.finishedLoading('netatmo') def __requestNewValues(self): if self.accessToken == '': return def backgroundTask(): service = rauth.OAuth2Service( client_id=self.clientId, client_secret=self.clientSecret, access_token_url='https://api.netatmo.net/oauth2/token') if time.time() > self.tokenTTL: session = self.__requestSession(service) else: session = rauth.OAuth2Session(self.clientId, self.clientSecret, access_token=self.accessToken, service=service) response = session.get( 'https://api.netatmo.com/api/getstationsdata') data = response.json() if 'error' in data and data['error']['code'] in [2, 3]: # Token is expired. Request new session = self.__requestSession(service) response = session.get( 'https://api.netatmo.com/api/getstationsdata') data = response.json() self.__parseValues(data) Thread(target=backgroundTask).start() def __requestSession(self, service): data = { 'grant_type': 'refresh_token', 'refresh_token': self.refreshToken } session = service.get_auth_session(data=data, decoder=self.__decodeAccessToken) return session def __decodeAccessToken(self, data): response = json.loads(data) self.accessToken = response['access_token'] self.refreshToken = response['refresh_token'] self.tokenTTL = int(time.time()) + response['expires_in'] self.setConfig( 'oauth', { 'accessToken': self.accessToken, 'refreshToken': self.refreshToken, 'tokenTTL': self.tokenTTL, }) self.configuration['oauth'].activated = True return response