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()
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()
class SuntimeCondition(Condition): def __init__(self, **kwargs): super(SuntimeCondition,self).__init__(**kwargs) self.sunStatus = None self.sunriseOffset = None self.sunsetOffset = None self.s = Settings('telldus.scheduler') self.latitude = self.s.get('latitude', '55.699592') self.longitude = self.s.get('longitude', '13.187836') def parseParam(self, name, value): if name == 'sunStatus': self.sunStatus = int(value) if name == 'sunriseOffset': self.sunriseOffset = int(value) if name == 'sunsetOffset': self.sunsetOffset = int(value) def validate(self, success, failure): if self.sunStatus is None or self.sunriseOffset is None or self.sunsetOffset is None: # condition has not finished loading, impossible to validate it correctly failure() return sunCalc = SunCalculator() currentDate = pytz.utc.localize(datetime.utcnow()) riseSet = sunCalc.nextRiseSet(timegm(currentDate.utctimetuple()), float(self.latitude), float(self.longitude)) currentStatus = 1 if (riseSet['sunrise'] + (self.sunriseOffset*60)) < (riseSet['sunset'] + (self.sunsetOffset*60)): # the sun is down (with offset) currentStatus = 0 if self.sunStatus == currentStatus: success() else: failure()
def __tokenKey(self): if self.tokenKey is not None: return self.tokenKey password = Board.secret() s = Settings('telldus.api') tokenKey = s.get('tokenKey', '') if tokenKey == '': self.tokenKey = os.urandom(32) # Store it salt = os.urandom(16) key = PBKDF2(password, salt).read(32) pwhash = crypt(password) s['salt'] = base64.b64encode(salt) s['pw'] = pwhash # Encrypt token key cipher = AES.new(key, AES.MODE_ECB, '') s['tokenKey'] = base64.b64encode(cipher.encrypt(self.tokenKey)) else: # Decode it salt = base64.b64decode(s.get('salt', '')) pwhash = s.get('pw', '') if crypt(password, pwhash) != pwhash: logging.warning('Could not decrypt token key, wrong password') return None key = PBKDF2(password, salt).read(32) enc = base64.b64decode(tokenKey) cipher = AES.new(key, AES.MODE_ECB, '') self.tokenKey = cipher.decrypt(enc) return self.tokenKey
def __init__(self, **kwargs): super(SuntimeCondition,self).__init__(**kwargs) self.sunStatus = None self.sunriseOffset = None self.sunsetOffset = None self.s = Settings('telldus.scheduler') self.latitude = self.s.get('latitude', '55.699592') self.longitude = self.s.get('longitude', '13.187836')
def __init__(self, **kwargs): super(TimeCondition,self).__init__(**kwargs) self.fromMinute = None self.fromHour = None self.toMinute = None self.toHour = None self.s = Settings('telldus.scheduler') self.timezone = self.s.get('tz', 'UTC')
def __init__(self): self.publicKey = '' self.privateKey = '' self.state = ServerConnection.CLOSED self.msgs = [] self.server = None s = Settings('tellduslive.config') self.useSSL = s.get('useSSL', True)
class SuntimeCondition(Condition): def __init__(self, **kwargs): super(SuntimeCondition, self).__init__(**kwargs) self.sunStatus = None self.sunriseOffset = None self.sunsetOffset = None self.settings = Settings('telldus.scheduler') self.latitude = self.settings.get('latitude', '55.699592') self.longitude = self.settings.get('longitude', '13.187836') def parseParam(self, name, value): if name == 'sunStatus': self.sunStatus = int(value) if name == 'sunriseOffset': self.sunriseOffset = int(value) if name == 'sunsetOffset': self.sunsetOffset = int(value) def validate(self, success, failure): if self.sunStatus is None or self.sunriseOffset is None or self.sunsetOffset is None: # condition has not finished loading, impossible to validate it correctly failure() return sunCalc = SunCalculator() currentDate = pytz.utc.localize(datetime.utcnow()) riseSet = sunCalc.nextRiseSet(timegm(currentDate.utctimetuple()), float(self.latitude), float(self.longitude)) currentStatus = 1 sunToday = sunCalc.riseset(timegm(currentDate.utctimetuple()), float(self.latitude), float(self.longitude)) sunRise = None sunSet = None if sunToday['sunrise']: sunRise = sunToday['sunrise'] + (self.sunriseOffset * 60) if sunToday['sunset']: sunSet = sunToday['sunset'] + (self.sunsetOffset * 60) if sunRise or sunSet: if (sunRise and time.time() < sunRise) or (sunSet and time.time() > sunSet): currentStatus = 0 else: # no sunset or sunrise, is it winter or summer? if riseSet['sunrise'] < riseSet['sunset']: # next is a sunrise, it's dark now (winter) if time.time() < (riseSet['sunrise'] + (self.sunriseOffset * 60)): currentStatus = 0 else: # next is a sunset, it's light now (summer) if time.time() > (riseSet['sunset'] + (self.sunriseOffset * 60)): currentStatus = 0 if self.sunStatus == currentStatus: success() else: failure()
def __init__(self, manager, **kwargs): super(TimeTrigger,self).__init__(**kwargs) self.manager = manager self.minute = None self.hour = None self.setHour = None # this is the hour actually set (not recalculated to UTC) self.active = True # TimeTriggers are always active self.s = Settings('telldus.scheduler') self.timezone = self.s.get('tz', 'UTC')
def __init__(self): self.events = {} self.settings = Settings('telldus.event') self.schedulersettings = Settings('telldus.scheduler') self.live = TelldusLive(self.context) self.timezone = self.schedulersettings.get('tz', 'UTC') self.latitude = self.schedulersettings.get('latitude', '55.699592') self.longitude = self.schedulersettings.get('longitude', '13.187836') self.loadLocalEvents()
class TimeTrigger(Trigger): def __init__(self, manager, **kwargs): super(TimeTrigger,self).__init__(**kwargs) self.manager = manager self.minute = None self.hour = None self.setHour = None # this is the hour actually set (not recalculated to UTC) self.active = True # TimeTriggers are always active self.s = Settings('telldus.scheduler') self.timezone = self.s.get('tz', 'UTC') def close(self): self.manager.deleteTrigger(self) def parseParam(self, name, value): if name == 'minute': self.minute = int(value) elif name == 'hour': self.setHour = int(value) # recalculate hour to UTC if int(value) == -1: self.hour = int(value) else: local_timezone = timezone(self.timezone) currentDate = pytz.utc.localize(datetime.utcnow()) local_datetime = local_timezone.localize(datetime(currentDate.year, currentDate.month, currentDate.day, int(value))) utc_datetime = pytz.utc.normalize(local_datetime.astimezone(pytz.utc)) if datetime.now().hour > utc_datetime.hour: # retry it with new date (will have impact on daylight savings changes (but not sure it will actually help)) currentDate = currentDate + timedelta(days=1) local_datetime = local_timezone.localize(datetime(currentDate.year, currentDate.month, currentDate.day, int(value))) utc_datetime = pytz.utc.normalize(local_datetime.astimezone(pytz.utc)) self.hour = utc_datetime.hour if self.hour is not None and self.minute is not None: self.manager.addTrigger(self) def recalculate(self): if self.hour == -1: return False self.timezone = self.s.get('tz', 'UTC') currentHour = self.hour local_timezone = timezone(self.timezone) currentDate = pytz.utc.localize(datetime.utcnow()) local_datetime = local_timezone.localize(datetime(currentDate.year, currentDate.month, currentDate.day, self.setHour)) utc_datetime = pytz.utc.normalize(local_datetime.astimezone(pytz.utc)) if datetime.now().hour > utc_datetime.hour: # retry it with new date (will have impact on daylight savings changes (but not sure it will actually help)) currentDate = currentDate + timedelta(days=1) local_datetime = local_timezone.localize(datetime(currentDate.year, currentDate.month, currentDate.day, self.setHour)) utc_datetime = pytz.utc.normalize(local_datetime.astimezone(pytz.utc)) self.hour = utc_datetime.hour if currentHour == self.hour: #no change return False return True
def __init__(self, manager, eventId, minimumRepeatInterval, description): self.manager = manager self.eventId = eventId self.minimumRepeatInterval = minimumRepeatInterval self.description = description self.lastRun = None self.actions = {} self.conditions = {} self.triggers = {} self.evaluatingConditions = [] self.s = Settings('telldus.event')
def __tokenKey(self): if self.tokenKey is not None: return self.tokenKey password = Board.secret() settings = Settings('telldus.api') tokenKey = settings.get('tokenKey', '') backend = default_backend() blockSize = 16 # TODO: Get this programatically? if tokenKey == '': self.tokenKey = os.urandom(32) # Store it salt = os.urandom(16) kdf = PBKDF2HMAC( algorithm=hashes.SHA1(), length=32, salt=salt, iterations=1000, backend=backend ) key = kdf.derive(password) pwhash = ApiManager.pbkdf2crypt(password) settings['salt'] = base64.b64encode(salt) settings['pw'] = pwhash # Encrypt token key cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) encryptor = cipher.encryptor() buf = bytearray(len(self.tokenKey)+blockSize-1) lenEncrypted = encryptor.update_into(self.tokenKey, buf) settings['tokenKey'] = base64.b64encode(bytes(buf[:lenEncrypted]) + encryptor.finalize()) else: # Decode it salt = base64.b64decode(settings.get('salt', '')) pwhash = settings.get('pw', '') if ApiManager.pbkdf2crypt(password, pwhash) != pwhash: logging.warning('Could not decrypt token key, wrong password') return None kdf = PBKDF2HMAC( algorithm=hashes.SHA1(), length=32, salt=salt, iterations=1000, backend=backend ) key = kdf.derive(password) enc = base64.b64decode(tokenKey) cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) decryptor = cipher.decryptor() buf = bytearray(len(enc)+blockSize-1) lenDecrypted = decryptor.update_into(enc, buf) self.tokenKey = bytes(buf[:lenDecrypted]) + decryptor.finalize() return self.tokenKey
def __init__(self, event, id, delay, delayPolicy, delayExecTime=None, *__args, **__kwargs): super(Action, self).__init__() self.event = event self.id = id # pylint: disable=C0103 self.delay = delay self.delayPolicy = delayPolicy self.delayExecTime = None if delayExecTime: self.delayExecTime = float(delayExecTime) self.timeout = None self.triggerInfo = None self.settings = Settings('telldus.event') Application().registerShutdown(self.stop)
def __tokenKey(self): if self.tokenKey is not None: return self.tokenKey password = Board.secret() settings = Settings('telldus.api') tokenKey = settings.get('tokenKey', '') backend = default_backend() if tokenKey == '': self.tokenKey = os.urandom(32) # Store it salt = os.urandom(16) kdf = PBKDF2HMAC( algorithm=hashes.SHA1(), length=32, salt=salt, iterations=1000, backend=backend ) key = kdf.derive(password) pwhash = ApiManager.pbkdf2crypt(password) settings['salt'] = base64.b64encode(salt) settings['pw'] = pwhash # Encrypt token key cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) encryptor = cipher.encryptor() settings['tokenKey'] = base64.b64encode(bytes(encryptor.update(self.tokenKey))) else: # Decode it salt = base64.b64decode(settings.get('salt', '')) pwhash = settings.get('pw', '') if ApiManager.pbkdf2crypt(password, pwhash) != pwhash: logging.warning('Could not decrypt token key, wrong password') return None kdf = PBKDF2HMAC( algorithm=hashes.SHA1(), length=32, salt=salt, iterations=1000, backend=backend ) key = kdf.derive(password) enc = base64.b64decode(tokenKey) cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) decryptor = cipher.decryptor() self.tokenKey = bytes(decryptor.update(enc)) return self.tokenKey
def __init__(self, event, id, delay, delayPolicy, delayExecTime=None, *args, **kwargs): super(Action, self).__init__() self.event = event self.id = id self.delay = delay self.delayPolicy = delayPolicy self.delayExecTime = None if delayExecTime: self.delayExecTime = float(delayExecTime) self.timeout = None self.s = Settings('telldus.event') Application().registerShutdown(self.stop)
def _read_settings(self): settings = Settings(CONFIG) pys = self.pyShelly pys.set_cloud_settings(settings["cloud_server"], settings["cloud_auth_key"]) pys.update_status_interval = timedelta(seconds=30) pys.only_device_id = settings["only_device_id"] pys.mdns_enabled = False #settings.get('mdns', True) if not settings['logger']: LOGGER.setLevel(logging.WARNING)
def __init__(self): settings = Settings('tellduslive.config') uuid = settings['uuid'] self.xmpp = ClientXMPP('*****@*****.**' % uuid, '%s:%s' % (Board.getMacAddr(), Board.secret())) self.xmpp.add_event_handler("session_start", self.sessionStart) self.xmpp.add_event_handler("register", self.register) self.xmpp.register_plugin('xep_0030') # Service Discovery self.xmpp.register_plugin('xep_0077') # In-band registration self.shuttingDown = False # Connect is blocking. Do this in a separate thread threading.Thread(target=self.__start, name='XMPP connector').start()
class TimeCondition(Condition): def __init__(self, **kwargs): super(TimeCondition, self).__init__(**kwargs) self.fromMinute = None self.fromHour = None self.toMinute = None self.toHour = None self.settings = Settings('telldus.scheduler') self.timezone = self.settings.get('tz', 'UTC') def parseParam(self, name, value): if name == 'fromMinute': self.fromMinute = int(value) elif name == 'toMinute': self.toMinute = int(value) elif name == 'fromHour': self.fromHour = int(value) elif name == 'toHour': self.toHour = int(value) def validate(self, success, failure): currentDate = pytz.utc.localize(datetime.utcnow()) local_timezone = timezone(self.timezone) if self.fromMinute is None \ or self.toMinute is None \ or self.fromHour is None \ or self.toHour is None: # validate that all parameters have been loaded failure() return toTime = local_timezone.localize( datetime(currentDate.year, currentDate.month, currentDate.day, self.toHour, self.toMinute, 0)) fromTime = local_timezone.localize( datetime(currentDate.year, currentDate.month, currentDate.day, self.fromHour, self.fromMinute, 0)) if fromTime > toTime: if (currentDate >= fromTime or currentDate <= toTime): success() else: failure() else: # condition interval passes midnight if (currentDate >= fromTime and currentDate <= toTime): success() else: failure()
def __init__(self): logging.info("Telldus Live! loading") self.email = '' self.supportedMethods = 0 self.connected = False self.registered = False self.serverList = ServerList() Application().registerShutdown(self.stop) self.s = Settings('tellduslive.config') self.uuid = self.s['uuid'] self.conn = ServerConnection() self.pingTimer = 0 self.thread = threading.Thread(target=self.run) if self.conn.publicKey != '': # Only connect if the keys has been set. self.thread.start()
def __init__(self): self.last_sent_data = None self.stop_ping_loop = threading.Event() self.deviceManager = DeviceManager(self.context) Application().registerShutdown(self.shutdown) settings = Settings('tellduslive.config') self.uuid = settings['uuid'] self.logHandler = ShellyLogger(self.uuid) LOGGER.addHandler(self.logHandler) self.setupPing() LOGGER.info('Init Shelly ' + __version__) self._initPyShelly()
class WeekdayCondition(Condition): def __init__(self, **kwargs): super(WeekdayCondition,self).__init__(**kwargs) self.weekdays = None self.s = Settings('telldus.scheduler') self.timezone = self.s.get('tz', 'UTC') def parseParam(self, name, value): if name == 'weekdays': self.weekdays = value def validate(self, success, failure): currentDate = pytz.utc.localize(datetime.utcnow()) local_timezone = timezone(self.timezone) local_datetime = local_timezone.normalize(currentDate.astimezone(local_timezone)) currentWeekday = local_datetime.weekday() + 1 if str(currentWeekday) in self.weekdays: success() else: failure()
def __init__(self): self.rooms = {} self.settings = Settings('telldus.rooms') self.rooms = self.settings.get('rooms', {}) self.roomlistEmpty = self.settings.get('roomlistEmpty', False)
class RoomManager(Plugin): """The roommanager holds and manages all the rooms in the server""" implements(ISignalObserver) implements(ITelldusLiveObserver) public = True def __init__(self): self.rooms = {} self.settings = Settings('telldus.rooms') self.rooms = self.settings.get('rooms', {}) self.roomlistEmpty = self.settings.get('roomlistEmpty', False) def getResponsibleRooms(self, responsible=None): if not responsible: live = TelldusLive(self.context) responsible = live.uuid rooms = {} for roomUUID in self.rooms: room = self.rooms[roomUUID] if room['responsible'] == responsible: rooms[roomUUID] = room return rooms def liveRegistered(self, msg, refreshRequired): if refreshRequired: self.syncRoom() def reportRooms(self, rooms, removedRooms=None): report = {} if not rooms and not self.roomlistEmpty and not removedRooms: # only allow empty room reports if we know it has been # explicitly emptied return if rooms or self.roomlistEmpty: report['rooms'] = rooms if removedRooms: report['removedRooms'] = removedRooms msg = LiveMessage('RoomReport') msg.append(report) TelldusLive(self.context).send(msg) def roomChanged(self, room1, room2): for prop in room1: if not room1[prop] == room2[prop]: return True return False def setMode(self, roomId, mode, setAlways=1): """ Set a room to a new mode """ room = self.rooms.get(roomId, None) if not room: return setAlways = int(setAlways) if setAlways or room['mode'] != mode: if room['mode'] != mode: room['mode'] = mode self.settings['rooms'] = self.rooms live = TelldusLive(self.context) if live.registered and room.get('responsible', '') == live.uuid: # Notify live if we are the owner msg = LiveMessage('RoomModeSet') msg.append({'id': roomId, 'mode': mode}) live.send(msg) self.__modeChanged(roomId, mode, 'room', room.get('name', '')) def syncRoom(self): TelldusLive(self.context).send(LiveMessage("roomsync-request")) @signal('modeChanged') def __modeChanged(self, objectId, modeId, objectType, objectName): """ Called every time the mode changes for a room """ pass @TelldusLive.handler('room') def __handleRoom(self, msg): data = msg.argument(0).toNative() if 'name' in data: if isinstance(data['name'], int): data['name'] = str(data['name']) else: data['name'] = data['name'].decode('UTF-8') live = TelldusLive(self.context) if data['action'] == 'set': oldResponsible = '' if data['id'] in self.rooms: # existing room room = self.rooms[data['id']] oldResponsible = room['responsible'] validKeys = ['name', 'color', 'content', 'icon', 'responsible'] for key in validKeys: if key in data: room[key] = data.get(key, '') if 'mode' in data and room['mode'] != data.get('mode', ''): room['mode'] = data.get('mode', '') self.__modeChanged(data['id'], room['mode'], 'room', room['name']) self.rooms[data['id']] = room else: # new room self.rooms[data['id']] = { 'name': data.get('name', ''), 'parent': data.get('parent', ''), 'color': data.get('color', ''), 'content': data.get('content', ''), 'icon': data.get('icon', ''), 'responsible': data['responsible'], 'mode': data.get('mode', ''), } if live.registered and \ (data['responsible'] == live.uuid or oldResponsible == live.uuid): room = self.rooms[data['id']] msg = LiveMessage('RoomSet') msg.append({ # No need to call get() on room here since we know every value has at least a # default value above 'id': data['id'], 'name': room['name'], 'parent': room['parent'], 'color': room['color'], 'content': room['content'], 'icon': room['icon'], 'responsible': room['responsible'], 'mode': room['mode'], }) live.send(msg) self.settings['rooms'] = self.rooms return if data['action'] == 'remove': room = self.rooms.pop(data['id'], None) if room is None: return if live.registered and room['responsible'] == live.uuid: msg = LiveMessage('RoomRemoved') msg.append({'id': data['id']}) live.send(msg) if len(self.getResponsibleRooms()) == 0: self.settings['roomlistEmpty'] = True self.roomlistEmpty = True self.settings['rooms'] = self.rooms return if data['action'] == 'setMode': self.setMode(data.get('id', None), data.get('mode', ''), data.get('setAlways', 1)) return if data['action'] == 'sync': rooms = data['rooms'] responsibleRooms = self.getResponsibleRooms() if not rooms and responsibleRooms: # list from server was completely empty but we have rooms locally, # this might be an error in the fetching, or we have added rooms locally # when offline. In any case, don't sync this time, just post our rooms # for next time self.reportRooms(responsibleRooms) return changedRooms = {} newRooms = {} removedRooms = [] for roomUUID in rooms: room = rooms[roomUUID] if room['responsible'] == live.uuid: # we are responsible for this room if roomUUID not in self.rooms: # this room does not exist locally anymore removedRooms.append(roomUUID) continue localRoom = self.rooms[roomUUID] if self.roomChanged(room, localRoom): changedRooms[roomUUID] = localRoom else: newRooms[roomUUID] = room newRooms.update(responsibleRooms) self.rooms = newRooms self.reportRooms(changedRooms, removedRooms) self.settings['rooms'] = self.rooms
class 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']()
def handleRequest(self, plugin, path, __params, **__kwargs): if path == 'list': return WebResponseJson(self._getData()) if path == "config": if __params: settings = Settings(CONFIG) for param in __params: if param in ['cloud_server', 'cloud_auth_key']: settings[param] = __params[param] self._read_settings() return WebResponseJson(self._getConfig()) if path == 'devices': devices = list( map( lambda d: { 'id': d.id, 'name': d.friendly_name(), 'unit_id': d.unit_id, 'type': d.type, 'ip_addr': d.ip_addr, 'is_device': d.is_device, 'is_sensor': d.is_sensor, 'sub_name': d.sub_name, 'state_values': d.state_values, 'state': d.state, 'device_type': d.device_type, 'device_sub_type': d.device_sub_type, 'device_nr': d.device_nr, 'master_unit': d.master_unit, 'ext_sensor': d.ext_sensor, 'info_values': d.info_values, 'friendly_name': d.friendly_name() }, self.pyShelly.devices)) return WebResponseJson(devices) if path == 'blocks': blocks = list( map( lambda d: { 'id': d.id, 'unit_id': d.unit_id, 'type': d.type, 'ip_addr': d.ip_addr, 'info_values': d.info_values }, self.pyShelly.blocks.values())) return WebResponseJson(blocks) if path == 'dump': shellyDevices = self.deviceManager.retrieveDevices() devices = list( map( lambda d: { 'id': d.id(), 'localid': d.localId(), 'name': d.name(), 'state': d.state(), 'params': d.params(), 'stateValues': d.stateValues(), 'sensorValues': d.sensorValues(), 'isDevice': d.isDevice(), 'isSensor': d.isSensor(), 'methods': d.methods(), 'parameters': d.parameters(), 'metadata': d.metadata(), 'type': d.typeString() }, shellyDevices)) return WebResponseJson({'devices': devices}) if path in [ 'turnon', 'turnoff', 'up', 'down', 'stop', 'firmware_update' ]: LOGGER.info('Request ' + path) id = __params['id'] device = self.deviceManager.device(int(id)) if path == 'turnon': if hasattr(device.dev, 'brightness'): device.dev.turn_on(brightness=100) else: device.dev.turn_on() elif path == 'turnoff': device.dev.turn_off() elif path == 'up': device.dev.up() elif path == 'down': device.dev.down() elif path == 'stop': device.dev.stop() elif path == 'firmware_update': if device.dev.block: device.dev.block.update_firmware() return WebResponseJson({}) if path == "rgb": id = __params['id'] r = __params['r'] g = __params['g'] b = __params['b'] device = self.deviceManager.device(int(id)) device.dev.set_values(rgb=[r, g, b]) self.refreshClient() return WebResponseJson({}) if path == "rename": id = __params['id'] name = __params['name'] device = self.deviceManager.device(int(id)) device.local_name = name device.update_name() self.refreshClient() return WebResponseJson({}) if path == "clean": self.deviceManager.removeDevicesByType('Shelly') self._initPyShelly() self.refreshClient() return WebResponseJson({'msg': 'Clean done'}) if path == "discover": self.pyShelly.discover() return WebResponseJson({}) if path == "addMember": LOGGER.debug("Add membership") import socket import struct mreq = struct.pack("=4sl", socket.inet_aton("224.0.1.187"), socket.INADDR_ANY) self.pyShelly._socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) return WebResponseJson({}) if path == "dropMember": LOGGER.debug("Drop membership") import socket import struct mreq = struct.pack("=4sl", socket.inet_aton("224.0.1.187"), socket.INADDR_ANY) self.pyShelly._socket.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, mreq) return WebResponseJson({}) if path == "initSocket": self.pyShelly.init_socket() return WebResponseJson({'msg': 'init socket done'})
def _getConfig(self): settings = Settings(CONFIG) return { 'cloud_server': settings["cloud_server"], 'cloud_auth_key': settings["cloud_auth_key"] }
def _load_cache(self, name): settings = Settings('Shelly.cache') return json.loads(settings[name])
def _save_cache(self, name, data): settings = Settings('Shelly.cache') settings[name] = data
def getSettings(self): return Settings('telldus.scheduler')