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 __handleRoom(self, msg): data = msg.argument(0).toNative() if 'name' in data: if isinstance(data['name'], int): data['name'] = str(data['name']) else: data['name'] = data['name'].decode('UTF-8') live = TelldusLive(self.context) if data['action'] == 'set': oldResponsible = '' if data['id'] in self.rooms: # existing room room = self.rooms[data['id']] oldResponsible = room['responsible'] validKeys = ['name', 'color', 'content', 'icon', 'responsible'] for key in validKeys: if key in data: room[key] = data.get(key, '') if 'mode' in data and room['mode'] != data.get('mode', ''): room['mode'] = data.get('mode', '') self.__modeChanged(data['id'], room['mode'], 'room', room['name']) self.rooms[data['id']] = room else: # new room self.rooms[data['id']] = { 'name': data.get('name', ''), 'parent': data.get('parent', ''), 'color': data.get('color', ''), 'content': data.get('content', ''), 'icon': data.get('icon', ''), 'responsible': data['responsible'], 'mode': data.get('mode', ''), } if live.registered and \ (data['responsible'] == live.uuid or oldResponsible == live.uuid): room = self.rooms[data['id']] msg = LiveMessage('RoomSet') msg.append({ # No need to call get() on room here since we know every value has at least a # default value above 'id': data['id'], 'name': room['name'], 'parent': room['parent'], 'color': room['color'], 'content': room['content'], 'icon': room['icon'], 'responsible': room['responsible'], 'mode': room['mode'], }) live.send(msg) self.settings['rooms'] = self.rooms return if data['action'] == 'remove': room = self.rooms.pop(data['id'], None) if room is None: return if live.registered and room['responsible'] == live.uuid: msg = LiveMessage('RoomRemoved') msg.append({'id': data['id']}) live.send(msg) if len(self.getResponsibleRooms()) == 0: self.settings['roomlistEmpty'] = True self.roomlistEmpty = True self.settings['rooms'] = self.rooms return if data['action'] == 'setMode': self.setMode(data.get('id', None), data.get('mode', ''), data.get('setAlways', 1)) return if data['action'] == 'sync': rooms = data['rooms'] responsibleRooms = self.getResponsibleRooms() if not rooms and responsibleRooms: # list from server was completely empty but we have rooms locally, # this might be an error in the fetching, or we have added rooms locally # when offline. In any case, don't sync this time, just post our rooms # for next time self.reportRooms(responsibleRooms) return changedRooms = {} newRooms = {} removedRooms = [] for roomUUID in rooms: room = rooms[roomUUID] if room['responsible'] == live.uuid: # we are responsible for this room if roomUUID not in self.rooms: # this room does not exist locally anymore removedRooms.append(roomUUID) continue localRoom = self.rooms[roomUUID] if self.roomChanged(room, localRoom): changedRooms[roomUUID] = localRoom else: newRooms[roomUUID] = room newRooms.update(responsibleRooms) self.rooms = newRooms self.reportRooms(changedRooms, removedRooms) self.settings['rooms'] = self.rooms
class DeviceManager(Plugin): """The devicemanager holds and manages all the devices in the server""" implements(ITelldusLiveObserver) observers = ObserverCollection(IDeviceChange) def __init__(self): self.devices = [] self.s = Settings('telldus.devicemanager') self.nextId = self.s.get('nextId', 0) self.live = TelldusLive(self.context) self.registered = False self.__load() @mainthread def addDevice(self, device): """Call this function to register a new device to the device manager. .. note:: The :func:`localId` function in the device must return a unique id for the transport type returned by :func:`typeString` """ cachedDevice = None for i, delDevice in enumerate(self.devices): # Delete the cached device from loaded devices, since it is replaced by a confirmed/specialised one if delDevice.localId() == device.localId() and device.typeString( ) == delDevice.typeString() and not delDevice.confirmed(): cachedDevice = delDevice del self.devices[i] break self.devices.append(device) device.setManager(self) if not cachedDevice: # New device, not stored in local cache self.nextId = self.nextId + 1 device.setId(self.nextId) else: # Transfer parameters from the loaded one device.loadCached(cachedDevice) self.save() if not cachedDevice: self.__deviceAdded(device) if self.live.registered and device.isDevice(): (state, stateValue) = device.state() deviceDict = { 'id': device.id(), 'name': device.name(), 'methods': device.methods(), 'state': state, 'stateValue': stateValue, 'protocol': device.protocol(), 'model': device.model(), 'transport': device.typeString() } msg = LiveMessage("DeviceAdded") msg.append(deviceDict) self.live.send(msg) else: # Previously cached device is now confirmed, TODO notify Live! about this too? self.observers.deviceConfirmed(device) def device(self, deviceId): """Retrieves a device. Returns: the device specified by `deviceId` or None of no device was found """ for d in self.devices: if d.id() == deviceId: return d return None def findByName(self, name): for d in self.devices: if d.name() == name: return d return None def finishedLoading(self, type): """ Finished loading all devices of this type. If there are any unconfirmed, these should be deleted """ for device in self.devices: if device.typeString() == type and not device.confirmed(): self.removeDevice(device.id()) def removeDevice(self, deviceId): """Removes a device. .. warning:: This function may only be called by the module supplying the device since removing of a device may be transport specific. """ isDevice = True for i, device in enumerate(self.devices): if device.id() == deviceId: self.__deviceRemoved(deviceId) isDevice = self.devices[i].isDevice() del self.devices[i] break self.save() if self.live.registered and isDevice: msg = LiveMessage("DeviceRemoved") msg.append({'id': deviceId}) self.live.send(msg) def retrieveDevices(self, deviceType=None): """Retrieve a list of devices. Args: :deviceType: If this parameter is set only devices with this type is returned Returns: Returns a list of devices """ l = [] for d in self.devices: if deviceType is not None and d.typeString() != deviceType: continue l.append(d) return l @signal def sensorValueUpdated(self, device, valueType, value, scale): """ Called every time a sensors value is updated. """ if device.isSensor() == False: return self.observers.sensorValueUpdated(device, valueType, value, scale) if not self.live.registered or device.ignored(): # don't send if ignored return msg = LiveMessage("SensorEvent") sensor = { 'name': device.name(), 'protocol': device.protocol(), 'model': device.model(), 'sensor_id': device.id(), } battery = device.battery() if battery is not None: sensor['battery'] = battery msg.append(sensor) values = device.sensorValues() valueList = [] for valueType in values: for value in values[valueType]: valueList.append({ 'type': valueType, 'lastUp': str(int(time.time())), 'value': str(value['value']), 'scale': value['scale'] }) msg.append(valueList) self.live.send(msg) def stateUpdated(self, device, ackId=None, origin=None): if device.isDevice() == False: return extras = {} if ackId: extras['ACK'] = ackId if origin: extras['origin'] = origin else: extras['origin'] = 'Incoming signal' (state, stateValue) = device.state() self.__deviceStateChanged(device, state, stateValue) self.save() if not self.live.registered: return msg = LiveMessage("DeviceEvent") msg.append(device.id()) msg.append(state) msg.append(str(stateValue)) msg.append(extras) self.live.send(msg) def stateUpdatedFail(self, device, state, stateValue, reason, origin): if not self.live.registered: return if device.isDevice() == False: return extras = { 'reason': reason, } if origin: extras['origin'] = origin else: extras['origin'] = 'Unknown' (state, stateValue) = device.state() self.__deviceStateChanged(device, state, stateValue) msg = LiveMessage('DeviceFailEvent') msg.append(device.id()) msg.append(state) msg.append(stateValue) msg.append(extras) self.live.send(msg) @TelldusLive.handler('command') def __handleCommand(self, msg): args = msg.argument(0).toNative() action = args['action'] value = args['value'] if 'value' in args else None id = args['id'] device = None for dev in self.devices: if dev.id() == id: device = dev break def success(state, stateValue): if 'ACK' in args: device.setState(state, stateValue, ack=args['ACK']) # Abort the DeviceEvent this triggered raise DeviceAbortException() def fail(reason): # We failed to set status for some reason, nack the server if 'ACK' in args: msg = LiveMessage('NACK') msg.append({ 'ackid': args['ACK'], 'reason': reason, }) self.live.send(msg) # Abort the DeviceEvent this triggered raise DeviceAbortException() device.command(action, value, success=success, failure=fail) @TelldusLive.handler('device') def __handleDeviceCommand(self, msg): args = msg.argument(0).toNative() if 'action' not in args: return if args['action'] == 'setName': if 'name' not in args or args['name'] == '': return for dev in self.devices: if dev.id() != args['device']: continue if type(args['name']) is int: dev.setName(str(args['name'])) else: dev.setName(args['name'].decode('UTF-8')) if dev.isDevice(): self.__sendDeviceReport() if dev.isSensor: self.__sendSensorReport( True) # force name change even for ignored sensor return @TelldusLive.handler('reload') def __handleSensorUpdate(self, msg): reloadType = msg.argument(0).toNative() if reloadType != 'sensor': # not for us return data = msg.argument(1).toNative() if not msg.argument(2) or 'sensorId' not in msg.argument(2).toNative(): # nothing to do, might be an orphaned zwave sensor return sensorId = msg.argument(2).toNative()['sensorId'] updateType = data['type'] for dev in self.devices: if dev.id() == sensorId: if updateType == 'updateignored': value = data['ignored'] if dev.ignored() == value: return dev.setIgnored(value) self.__sendSensorChange(sensorId, updateType, value) return def liveRegistered(self, msg): self.registered = True self.__sendDeviceReport() self.__sendSensorReport() def __load(self): self.store = self.s.get('devices', []) for dev in self.store: if 'type' not in dev or 'localId' not in dev: continue # This should not be possible d = CachedDevice(dev) # If we have loaded this device from cache 5 times in a row it's # considered dead if d.loadCount() < 5: self.devices.append(d) @signal('deviceAdded') def __deviceAdded(self, device): """ Called every time a device is added/created """ self.observers.deviceAdded(device) @signal('deviceRemoved') def __deviceRemoved(self, deviceId): """ Called every time a device is removed. The parameter deviceId is the old device id. The ref to the device is no longer available """ self.observers.deviceRemoved(deviceId) @signal('deviceStateChanged') def __deviceStateChanged(self, device, state, stateValue): """ Called every time the state of a device is changed. """ self.observers.stateChanged(device, state, stateValue) def save(self): data = [] for d in self.devices: (state, stateValue) = d.state() data.append({ "id": d.id(), "loadCount": d.loadCount(), "localId": d.localId(), "type": d.typeString(), "name": d.name(), "params": d.params(), "methods": d.methods(), "state": state, "stateValue": stateValue, "ignored": d.ignored() }) self.s['devices'] = data self.s['nextId'] = self.nextId def __sendDeviceReport(self): if not self.live.registered: return l = [] for d in self.devices: if not d.isDevice(): continue (state, stateValue) = d.state() device = { 'id': d.id(), 'name': d.name(), 'methods': d.methods(), 'state': state, 'stateValue': stateValue, 'protocol': d.protocol(), 'model': d.model(), 'transport': d.typeString(), 'ignored': d.ignored() } battery = d.battery() if battery is not None: device['battery'] = battery l.append(device) msg = LiveMessage("DevicesReport") msg.append(l) self.live.send(msg) def __sendSensorChange(self, sensorid, valueType, value): msg = LiveMessage("SensorChange") device = None for d in self.devices: if d.id() == sensorid: device = d break if not device: return sensor = { 'protocol': device.typeString(), 'model': device.model(), 'sensor_id': device.id(), } msg.append(sensor) msg.append(valueType) msg.append(value) self.live.send(msg) def __sendSensorReport(self, forceIgnored=False): if not self.live.registered: return l = [] for d in self.devices: if d.isSensor() == False or (d.ignored() and not forceIgnored): continue sensorFrame = [] sensor = { 'name': d.name(), 'protocol': d.protocol(), 'model': d.model(), 'sensor_id': d.id(), } battery = d.battery() if battery is not None: sensor['battery'] = battery sensorFrame.append(sensor) valueList = [] # TODO(micke): Add current values sensorFrame.append(valueList) l.append(sensorFrame) msg = LiveMessage("SensorsReport") msg.append(l) self.live.send(msg)
class 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 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()