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'].upper() == 'ON' \ else Device.TURNOFF, value = 255, origin = 'mqtt_hass' ) elif deviceType == 'switch': device.command( Device.TURNON if payload.upper() == 'ON' \ else Device.BELL if payload.upper() == 'BELL' \ else Device.TURNOFF, origin = 'mqtt_hass' ) elif deviceType == 'cover': device.command( Device.UP if payload.upper() == 'OPEN' \ else Device.DOWN if payload.upper() == 'CLOSE' else \ Device.STOP, origin = 'mqtt_hass' ) except Exception as e: userdata.debug('onMessage exception %s' % str(e))
class Scheduler(Plugin): implements(ITelldusLiveObserver, IDeviceChange) def __init__(self): self.running = False #self.runningJobsLock = threading.Lock() #TODO needed? self.jobsLock = threading.Lock() self.maintenanceJobsLock = threading.Lock() self.maintenanceJobs = [] self.lastMaintenanceJobId = 0 self.runningJobs = {} #id:s as keys self.settings = Settings('telldus.scheduler') Application().registerShutdown(self.stop) Application().registerMaintenanceJobHandler( self.addMaintenanceJobGeneric) self.timezone = self.settings.get('tz', 'UTC') self.latitude = self.settings.get('latitude', '55.699592') self.longitude = self.settings.get('longitude', '13.187836') self.jobs = [] self.fetchLocalJobs() self.live = TelldusLive(self.context) self.deviceManager = DeviceManager(self.context) if self.live.isRegistered(): #probably not practically possible to end up here self.requestJobsFromServer() self.thread = threading.Thread(target=self.run) self.thread.start() def addMaintenanceJobGeneric(self, job): self.addMaintenanceJob(job['nextRunTime'], job['callback'], job['recurrence']) def addMaintenanceJob(self, nextRunTime, timeoutCallback, recurrence=0): """ nextRunTime - GMT timestamp, timeoutCallback - the method to run, recurrence - when to repeat it, in seconds Returns: An id for the newly added job (for removal and whatnot) Note, if the next nextRunTime needs to be calculated, it's better to do that in the callback-method, and add a new job from there, instead of using "recurrence" """ jobData = { 'nextRunTime': nextRunTime, 'callback': timeoutCallback, 'recurrence': recurrence } with self.maintenanceJobsLock: self.lastMaintenanceJobId = self.lastMaintenanceJobId + 1 jobData[ 'id'] = self.lastMaintenanceJobId # add an ID, make it possible to remove it someday self.maintenanceJobs.append(jobData) self.maintenanceJobs.sort( key=lambda jobData: jobData['nextRunTime']) return self.lastMaintenanceJobId def calculateJobs(self, jobs): """Calculate nextRunTime for all jobs in the supplied list, order it and assign it to self.jobs""" newJobs = [] for job in jobs: self.checkNewlyLoadedJob(job) if self.calculateNextRunTime(job): newJobs.append(job) newJobs.sort(key=lambda job: job['nextRunTime']) with self.jobsLock: self.jobs = newJobs def calculateNextRunTime(self, job): """Calculates nextRunTime for a job, depending on time, weekday and timezone.""" if not job['active'] or not job['weekdays']: job['nextRunTime'] = 253402214400 #set to max value, only run just before the end of time # just delete the job, until it's possible to edit schedules locally, inactive jobs has # no place at all here self.deleteJob(job['id']) return False today = datetime.now(timezone(self.timezone)).weekday() # normalize? weekdays = [int(n) for n in job['weekdays'].split(',')] runToday = False firstWeekdayToRun = None nextWeekdayToRun = None runDate = None for weekday in weekdays: weekday = weekday - 1 #weekdays in python: 0-6, weekdays in our database: 1-7 if weekday == today: runToday = True elif today < weekday and (nextWeekdayToRun is None or nextWeekdayToRun > weekday): nextWeekdayToRun = weekday elif today > weekday and (firstWeekdayToRun is None or weekday < firstWeekdayToRun): firstWeekdayToRun = weekday todayDate = datetime.now(timezone(self.timezone)).date() # normalize? if runToday: #this weekday is included in the ones that this schedule should be run on runTimeToday = self.calculateRunTimeForDay(todayDate, job) if runTimeToday > time.time(): job['nextRunTime'] = runTimeToday + random.randint( 0, job['random_interval']) * 60 return True elif len(weekdays) == 1: #this job should only run on this weekday, since it has already passed today, run it next week runDate = todayDate + timedelta(days=7) if not runDate: if nextWeekdayToRun is not None: runDate = self.calculateNextWeekday(todayDate, nextWeekdayToRun) else: runDate = self.calculateNextWeekday(todayDate, firstWeekdayToRun) if not runDate: #something is wrong, no weekday to run job['nextRunTime'] = 253402214400 # just delete the job, until it's possible to edit schedules locally, inactive jobs # has no place at all here self.deleteJob(job['id']) return False job['nextRunTime'] = self.calculateRunTimeForDay(runDate, job) \ + random.randint(0, job['random_interval']) * 60 return True @staticmethod def calculateNextWeekday(todayDate, weekday): days_ahead = weekday - todayDate.weekday() if days_ahead <= 0: # Target day already happened this week days_ahead += 7 return todayDate + timedelta(days_ahead) def calculateRunTimeForDay(self, runDate, job): """ Calculates and returns a timestamp for when this job should be run next. Takes timezone into consideration. """ runDate = datetime(runDate.year, runDate.month, runDate.day) if job['type'] == 'time': # TODO, sending timezone from the server now, but it's really a client setting, can I # get it from somewhere else? tzone = timezone(self.timezone) # won't random here, since this time may also be used to see if it's passed today or not runDate = runDate + timedelta(hours=job['hour'], minutes=job['minute']) # returning a timestamp, corrected for timezone settings return timegm(tzone.localize(runDate).utctimetuple()) elif job['type'] == 'sunrise': sunCalc = SunCalculator() riseSet = sunCalc.nextRiseSet(timegm(runDate.utctimetuple()), float(self.latitude), float(self.longitude)) return riseSet['sunrise'] + job['offset'] * 60 elif job['type'] == 'sunset': sunCalc = SunCalculator() riseSet = sunCalc.nextRiseSet(timegm(runDate.utctimetuple()), float(self.latitude), float(self.longitude)) return riseSet['sunset'] + job['offset'] * 60 def checkNewlyLoadedJob(self, job): """Checks if any of the jobs (local or initially loaded) should be running right now""" if not job['active'] or not job['weekdays']: return weekdays = [int(n) for n in job['weekdays'].split(',')] i = 0 while i < 2: #Check today and yesterday (might be around 12 in the evening) currentDate = date.today() + timedelta(days=-i) if (currentDate.weekday() + 1) in weekdays: #check for this day (today or yesterday) runTime = self.calculateRunTimeForDay(currentDate, job) runTimeMax = runTime + job['reps'] * 3 \ + job['retry_interval'] * 60 * (job['retries'] + 1) \ + 70 \ + job['random_interval'] * 60 jobId = job['id'] executedJobs = self.settings.get('executedJobs', {}) if (str(jobId) not in executedJobs or executedJobs[str(jobId)] < runTime) \ and time.time() > runTime \ and time.time() < runTimeMax: # run time for this job was passed during downtime, but it was passed within the # max-runtime, and the last time it was executed (successfully) was before this # run time, so it should be run again... jobCopy = copy.deepcopy(job) jobCopy['originalRepeats'] = job['reps'] jobCopy['nextRunTime'] = runTime jobCopy[ 'maxRunTime'] = runTimeMax #approximate maxRunTime, sanity check self.runningJobs[jobId] = jobCopy return i = i + 1 def deleteJob(self, jobId): with self.jobsLock: # Test this! It should be fast and keep original reference, they say (though it will # iterate all, even if it could end after one) self.jobs[:] = [x for x in self.jobs if x['id'] != jobId] if jobId in self.runningJobs: #TODO this might require a lock too? self.runningJobs[jobId]['retries'] = 0 executedJobs = self.settings.get('executedJobs', {}) if str(jobId) in executedJobs: del executedJobs[str(jobId)] self.settings['executedJobs'] = executedJobs def deviceRemoved(self, deviceId): jobsToDelete = [] for job in self.jobs: if job['id'] == deviceId: jobsToDelete.append[job['id']] for jobId in jobsToDelete: self.deleteJob(jobId) def fetchLocalJobs(self): """Fetch local jobs from settings""" try: jobs = self.settings.get('jobs', []) except ValueError: jobs = [ ] #something bad has been stored, just ignore it and continue? print "WARNING: Could not fetch schedules from local storage" self.calculateJobs(jobs) def liveRegistered(self, msg): if 'latitude' in msg: self.latitude = msg['latitude'] if 'longitude' in msg: self.longitude = msg['longitude'] if 'tz' in msg: self.timezone = msg['tz'] self.requestJobsFromServer() @TelldusLive.handler('scheduler-remove') def removeOneJob(self, msg): if len(msg.argument(0).toNative()) != 0: scheduleDict = msg.argument(0).toNative() jobId = scheduleDict['id'] self.deleteJob(jobId) self.settings['jobs'] = self.jobs #save to storage self.live.pushToWeb('scheduler', 'removed', jobId) @TelldusLive.handler('scheduler-report') def receiveJobsFromServer(self, msg): """Receive list of jobs from server, saves to settings and calculate nextRunTimes""" if len(msg.argument(0).toNative()) == 0: jobs = [] else: scheduleDict = msg.argument(0).toNative() jobs = scheduleDict['jobs'] self.settings['jobs'] = jobs self.calculateJobs(jobs) @TelldusLive.handler('scheduler-update') def receiveOneJobFromServer(self, msg): """Receive one job from server, add or edit, save to settings and calculate nextRunTime""" if len(msg.argument(0).toNative()) == 0: jobs = [] else: scheduleDict = msg.argument(0).toNative() job = scheduleDict['job'] active = self.calculateNextRunTime(job) self.deleteJob( job['id']) #delete the job if it already exists (update) if active: with self.jobsLock: self.jobs.append(job) self.jobs.sort(key=lambda job: job['nextRunTime']) self.settings['jobs'] = self.jobs #save to storage # TODO is this a good idea? Trying to avoid cache problems where updates haven't come through? # But this may not work if the same schedule is saved many times in a row, or if changes # wasn't saved correctly to the database (not possible yet, only one database for schedules) # self.live.pushToWeb('scheduler', 'updated', job['id']) def requestJobsFromServer(self): self.live.send(LiveMessage("scheduler-requestjob")) def run(self): self.running = True while self.running: maintenanceJob = None with self.maintenanceJobsLock: if len( self.maintenanceJobs ) > 0 and self.maintenanceJobs[0]['nextRunTime'] < time.time(): maintenanceJob = self.maintenanceJobs.pop(0) self.runMaintenanceJob(maintenanceJob) jobCopy = None with self.jobsLock: if len(self.jobs ) > 0 and self.jobs[0]['nextRunTime'] < time.time(): #a job has passed its nextRunTime job = self.jobs[0] jobId = job['id'] jobCopy = copy.deepcopy( job) #make a copy, don't edit the original job if jobCopy: jobCopy['originalRepeats'] = job['reps'] # approximate maxRunTime, sanity check jobCopy['maxRunTime'] = jobCopy['nextRunTime'] \ + jobCopy['reps'] * 3 \ + jobCopy['retry_interval'] * 60 * (jobCopy['retries'] + 1) \ + 70 \ + jobCopy['random_interval'] * 60 self.runningJobs[jobId] = jobCopy self.calculateNextRunTime(job) with self.jobsLock: self.jobs.sort(key=lambda job: job['nextRunTime']) jobsToRun = [ ] # jobs to run in a separate list, to avoid deadlocks (necessary?) # Iterating using .keys(9 since we are modifiyng the dict while iterating for runningJobId in self.runningJobs.keys(): # pylint: disable=C0201 runningJob = self.runningJobs[runningJobId] if runningJob['nextRunTime'] < time.time(): if runningJob['maxRunTime'] > time.time(): if 'client_device_id' not in runningJob: print "Missing client_device_id, this is an error, perhaps refetch jobs? " print runningJob continue device = self.deviceManager.device( runningJob['client_device_id']) if not device: print "Missing device, b: " + str( runningJob['client_device_id']) continue if device.typeString( ) == '433' and runningJob['originalRepeats'] > 1: #repeats for 433-devices only runningJob['reps'] = int(runningJob['reps']) - 1 if runningJob['reps'] >= 0: runningJob['nextRunTime'] = time.time() + 3 jobsToRun.append(runningJob) continue if runningJob['retries'] > 0: runningJob['nextRunTime'] = time.time() + ( runningJob['retry_interval'] * 60) runningJob['retries'] = runningJob['retries'] - 1 runningJob['reps'] = runningJob['originalRepeats'] jobsToRun.append(runningJob) continue del self.runningJobs[ runningJobId] #max run time passed or out of retries for jobToRun in jobsToRun: self.runJob(jobToRun) # TODO decide on a time (how often should we check for jobs to run, what resolution?) time.sleep(5) def stop(self): self.running = False def successfulJobRun(self, jobId, state, stateValue): """ Called when job run was considered successful (acked by Z-Wave or sent away from 433), repeats should still be run """ del state, stateValue # save timestamp for when this was executed, to avoid rerun within maxRunTime on restart # TODO is this too much writing? executedJobs = self.settings.get('executedJobs', {}) executedJobs[str(jobId)] = time.time( ) #doesn't work well with int type, for some reason self.settings['executedJobs'] = executedJobs #executedJobsTest = self.settings.get('executedJobs', {}) if jobId in self.runningJobs: self.runningJobs[jobId]['retries'] = 0 @mainthread def runJob(self, jobData): device = self.deviceManager.device(jobData['client_device_id']) if not device: print "Missing device: " + str(jobData['client_device_id']) return method = jobData['method'] value = None if 'value' in jobData: value = jobData['value'] device.command(method, value=value, origin='Scheduler', success=self.successfulJobRun, callbackArgs=[jobData['id']]) @mainthread def runMaintenanceJob(self, jobData): if not jobData: return if jobData['recurrence']: # readd the job for another run self.addMaintenanceJob(time.time() + jobData['recurrence'], jobData['callback'], jobData['recurrence']) jobData['callback']()
class 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')
def onMessage(self, client, userdata, msg): try: topic = msg.topic payload = msg.payload if topic.split('/')[-1] == 'set': topicType = 'set' else: topicType = topic.split('/')[-1] deviceManager = DeviceManager(userdata.context) device_id = int(msg.topic.split('/')[3]) device = deviceManager.device(device_id) deviceType = userdata.getDeviceType(device) if not deviceType: return userdata.debug(json.dumps({ 'type': 'command', 'device_id': device_id, 'device_type': deviceType, 'command': payload })) def failed(reason, **__kwargs): self.debug('Device command failed: %s' % reason) if deviceType == 'climate': if topicType == 'mode': mode = { 'fan_only': Thermostat.MODE_FAN }.get(payload, payload) setpoint = self.getClimateSetPoint(device, mode) if setpoint is not None: value = { 'mode': mode, 'changeMode': True, 'temperature': self.getClimateSetPoint(device, mode) } self.debug('Command THERMOSTAT value: %s' % value) device.command( Device.THERMOSTAT, value = value, origin = 'mqtt_hass', failure = failed ) else: self.debug('Can not set mode, setpoint none') if topicType == 'setpoint': setpoint = float(payload) if payload else None if setpoint is not None: value = { 'mode': self.getClimateMode(device), 'changeMode': False, 'temperature': setpoint } self.debug('Command THERMOSTAT value: %s' % value) device.command( Device.THERMOSTAT, value = value, origin = 'mqtt_hass', failure = failed ) else: self.debug('Can not update setpoint, setpoint none (%s)' % payload) elif deviceType == 'light': payload = json.loads(payload) if 'brightness' in payload: if int(payload['brightness']) == 0: device.command( Device.TURNOFF, origin = 'mqtt_hass', failure = failed ) else: device.command( Device.DIM, value = int(payload['brightness']), origin = 'mqtt_hass', failure = failed ) else: device.command( Device.TURNON if payload['state'].upper() == 'ON' \ else Device.TURNOFF, value = 255, origin = 'mqtt_hass', failure = failed ) elif deviceType == 'switch': device.command( Device.TURNON if payload.upper() == 'ON' \ else Device.BELL if payload.upper() == 'BELL' \ else Device.TURNOFF, origin = 'mqtt_hass', failure = failed ) elif deviceType == 'cover': device.command( Device.UP if payload.upper() == 'OPEN' \ else Device.DOWN if payload.upper() == 'CLOSE' else \ Device.STOP, origin = 'mqtt_hass', failure = failed ) except Exception as e: userdata.debug('onMessage exception %s' % str(e))
class TelldusCore(Plugin): TELLSTICK_SUCCESS = 0 TELLSTICK_ERROR_DEVICE_NOT_FOUND = -3 TELLSTICK_ERROR_UNKNOWN = -99 implements(IDeviceChange) def __init__(self): self.deviceManager = DeviceManager(self.context) self.clientListener = ConnectionListener('TelldusClient', self.clientMessage) self.events = ConnectionListener('TelldusEvents', self.eventMessage) Application().registerShutdown(self.shutdown) def clientMessage(self, client, msg): (func, msg) = TelldusCore.takeString(msg) if func == 'tdTurnOn': self.doCommand(msg, 'turnon', client) elif func == 'tdTurnOff': self.doCommand(msg, 'turnoff', client) elif func == 'tdBell': self.doCommand(msg, '', client) #elif func == 'tdDim': #pass elif func == 'tdExecute': self.doCommand(msg, 'execute', client) elif func == 'tdUp': self.doCommand(msg, 'up', client) elif func == 'tdDown': self.doCommand(msg, 'down', client) elif func == 'tdStop': self.doCommand(msg, 'stop', client) elif func == 'tdLearn': self.doCommand(msg, 'learn', client) elif func == 'tdLastSentCommand': (deviceId, msg) = TelldusCore.takeInt(msg) (supportedMethods, msg) = TelldusCore.takeInt(msg) device = self.deviceManager.device(deviceId) if not device: client.respond(TelldusCore.TELLSTICK_ERROR_DEVICE_NOT_FOUND) state, stateValue = device.state() client.respond(Device.maskUnsupportedMethods(state, supportedMethods)) #elif func == 'tdLastSentValue': #pass elif func == 'tdGetNumberOfDevices': client.respond(len(self.__filteredDevices())) elif func == 'tdGetDeviceId': (deviceIndex, msg) = TelldusCore.takeInt(msg) deviceList = self.__filteredDevices() if deviceIndex > len(deviceList) - 1: client.respond(TelldusCore.TELLSTICK_ERROR_DEVICE_NOT_FOUND) device = deviceList[deviceIndex] client.respond(device.id()) #elif func == 'tdGetDeviceType': #pass elif func == 'tdGetName': (deviceId, msg) = TelldusCore.takeInt(msg) device = self.deviceManager.device(deviceId) if not device: client.respond('') client.respond(device.name()) #elif func == 'tdSetName': #pass #elif func == 'tdGetProtocol': #pass #elif func == 'tdSetProtocol': #pass #elif func == 'tdGetModel': #pass #elif func == 'tdSetModel': #pass #elif func == 'tdGetDeviceParameter': #pass #elif func == 'tdSetDeviceParameter': #pass #elif func == 'tdAddDevice': #pass #elif func == 'tdRemoveDevice': #pass elif func == 'tdMethods': (deviceId, msg) = TelldusCore.takeInt(msg) (supportedMethods, msg) = TelldusCore.takeInt(msg) device = self.deviceManager.device(deviceId) if not device: client.respond(TelldusCore.TELLSTICK_ERROR_DEVICE_NOT_FOUND) client.respond(Device.maskUnsupportedMethods(device.methods(), supportedMethods)) #elif func == 'tdSendRawCommand': #pass #elif func == 'tdConnectTellStickController': #pass #elif func == 'tdDisconnectTellStickController': #pass #elif func == 'tdSensor': #pass #elif func == 'tdSensorValue': #pass #elif func == 'tdController': #pass #elif func == 'tdControllerValue': #pass #elif func == 'tdSetControllerValue': #pass #elif func == 'tdRemoveController': #pass else: client.respond(TelldusCore.TELLSTICK_ERROR_UNKNOWN) def doCommand(self, msg, action, client): (deviceId, msg) = TelldusCore.takeInt(msg) device = self.deviceManager.device(deviceId) if device is None: client.respond(TelldusCore.TELLSTICK_ERROR_DEVICE_NOT_FOUND) device.command(action, origin='TelldusCore') client.respond(TelldusCore.TELLSTICK_SUCCESS) def eventMessage(self, client, msg): pass def shutdown(self): self.clientListener.close() self.events.close() def stateChanged(self, device, method, statevalue): if statevalue is None: statevalue = '' self.events.broadcast('TDDeviceEvent', device.id(), method, statevalue) def __filteredDevices(self): return [x for x in self.deviceManager.devices if x.isDevice()] @staticmethod def takeInt(msg): if msg[0] != 'i': return ('', msg) index = msg.find('s') if (index < 0): return ('', msg) try: value = int(msg[1:index], 10) except: return ('', msg) return (value, msg[index+1:]) @staticmethod def takeString(msg): if not msg[0].isdigit(): return ('', msg) index = msg.find(':') if (index < 0): return ('', msg) try: length = int(msg[:index], 10) except: return ('', msg) value = msg[index+1:index+length+1] return (value, msg[index+length+1:])
class Client(Plugin): implements(ISignalObserver) def __init__(self): self.deviceManager = DeviceManager(self.context) self.client = mqtt.Client('telldus') 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): # TODO: handle other changes 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.will_set('%s/status' % self.config('topic'), payload='Offline', qos=0, retain=True) self.client.connect_async(self.config('hostname'), self.config('port')) self.client.loop_start() def subscribeDevice(self, deviceId): self.client.subscribe('%s/device/%s/cmd' % (self.config('topic'), deviceId)) def unsubscribeDevice(self, deviceId): self.client.unsubscribe('%s/device/%s/cmd' % (self.config('topic'), deviceId)) @slot('deviceAdded') def onDeviceAdded(self, device): self.subscribeDevice(device.id()) @slot('deviceRemoved') def onDeviceRemoved(self, deviceId): self.unsubscribeDevice(deviceId) @slot('deviceStateChanged') def onDeviceStateChanged(self, device, state, stateValue, origin=None): del origin self.client.publish( '%s/device/%s/state' % (self.config('topic'), device.id()), json.dumps({ 'name': device.name(), 'state': state, 'stateValue': stateValue, })) @slot('sensorValueUpdated') def onSensorValueUpdated(self, device, valueType, value, scale): self.client.publish( '%s/sensor/%s/value' % (self.config('topic'), device.id()), json.dumps({ 'name': device.name(), 'value': FloatWrapper(value), 'valueType': valueType, 'scale': scale, })) def onConnect(self, client, userdata, flags, result): for device in self.deviceManager.retrieveDevices(): self.subscribeDevice(device.id()) self.client.publish('%s/status' % self.config('topic'), payload='Online', qos=0, retain=True) def onMessage(self, client, userdata, msg): try: data = json.loads(str(msg.payload.decode('utf-8'))) deviceId = msg.topic.split('/')[-2] # _topic_/device/_id_/cmd device = self.deviceManager.device(int(deviceId)) if device: device.command(data.get('action'), data.get('value')) except ValueError as e: logging.error('Could not decode JSON payload %s', e) except Exception as e: logging.error('Could not perform a command %s', e) def onPublish(self, client, obj, mid): pass def onSubscribe(self, client, obj, mid, granted_qos): pass