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
Exemple #5
0
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
Exemple #6
0
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
Exemple #7
0
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
Exemple #10
0
class Webinterface(Plugin):
    implements(IWebReactHandler)

    @staticmethod
    def getReactComponents():
        return {
            'webinterface': {
                'title': 'Webinterface example',
                'script': 'webinterface/welcome.js',
                'tags': ['menu'],
            }
        }
Exemple #11
0
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
Exemple #12
0
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'
Exemple #13
0
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)
Exemple #14
0
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
Exemple #15
0
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
Exemple #16
0
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())
Exemple #17
0
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']()
Exemple #18
0
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)
Exemple #19
0
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
Exemple #21
0
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()
Exemple #23
0
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)
Exemple #24
0
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)
Exemple #27
0
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
Exemple #28
0
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))
Exemple #29
0
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