Example #1
0
    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', ''))
Example #2
0
    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)
Example #4
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']()
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()