def playStream(name, casts=CASTS, volume=None): ''' Play the given stream url. :param str name: see _STREAMS :param list(ChromeCast) casts: :return: boolean True if success; False if stream name is invalid. ''' if None != volume and (volume < 0 or volume > 100): raise ValueError('volume must be between 0 and 100') url = getStreamUrl(name) if None != url: for cast in casts: if None != volume: scope.events.sendCommand(cast.getVolumeName(), str(volume)) if url == cast.getStreamUrl(): resume([cast]) else: Audio.playStream(cast.getSinkName(), url) cast.setStream(name, url) return True else: PE.logInfo("Missing stream URL for '{0}'".format(name)) return False
def armAndSendAlert(): # Don't know why list comprehension version like below # doesn't work, Jython just hang on this statement: # if not any(z.isOccupied(elapsedTime) for z in zoneManager.getZones()): # Below is a work around. occupied = False activeDevice = None for z in zoneManager.getZones(): if z.isExternal(): continue # motion sensor switches off after around 3', need to # that into account. motionDelayInSec = 3 * 60 delayTimeInSec = self.maxElapsedTimeInSeconds + motionDelayInSec (occupied, activeDevice) = z.isOccupied([], delayTimeInSec) if occupied: break if occupied: PE.logInfo( 'Auto-arm cancelled (activities detected @ {}).'. format(activeDevice)) else: securityPartitions[0].armAway(events) msg = 'The house has been automatically armed-away (front door closed and no activity)' alert = Alert.createWarningAlert(msg) AlertManager.processAlert(alert, zoneManager)
def alertIfRainInShortermForecast(event): forecasts = EnvCanada.retrieveHourlyForecast('Ottawa', 12) rainPeriods = [f for f in forecasts if \ 'High' == f.getPrecipationProbability() or \ 'Medium' == f.getPrecipationProbability()] if len(rainPeriods) > 0: if len(rainPeriods) == 1: subject = u"Possible precipation at {}".format( rainPeriods[0].getUserFriendlyForecastTime()) else: subject = u"Possible precipation from {} to {}".format( rainPeriods[0].getUserFriendlyForecastTime(), rainPeriods[-1].getUserFriendlyForecastTime()) body = u'Forecasts:\n' body += u"{:5} {:7} {:25} {:6} {:6}\n".format('Hour: ', 'Celsius', 'Condition', 'Prob.', 'Wind') for f in forecasts: body += unicode(f) + '\n' alert = Alert.createInfoAlert(subject, body) result = AlertManager.processAlert(alert, zm) if not result: PE.logInfo('Failed to send rain alert')
def startAutoReporWatchDog(self, timerIntervalInSeconds=10 * 60, inactiveIntervalInSeconds=10 * 60): ''' Starts a timer that run every timerIntervalInSeconds. When the timer is triggered, it will scan auto-report devices (Devices::isAutoReport), and if any of them hasn't been triggered in the last inactiveIntervalInSeconds, it will reset the item value. This method is safe to call multiple times (a new timer will be started and any old timer is cancelled). :param int timerIntervalInSeconds: the timer duration :param int inactiveIntervalInSeconds: the inactive duration after which the device's value will be reset. :rtype: None ''' def resetFailedAutoReportDevices(): devices = [] for z in self.getZones(): [devices.append(d) for d in z.getDevices() \ if d.isAutoReport() and \ not d.wasRecentlyActivated(inactiveIntervalInSeconds)] if len(devices) > 0: itemNames = [] for d in devices: itemNames.append(d.getItemName()) d.resetValueStates() PE.logWarning( "AutoReport Watchdog: {} failed auto-report devices: {}". format(len(devices), itemNames)) else: PE.logDebug( "AutoReport Watchdog: no failed auto-report devices") # restart the timer self.autoReportWatchDogTimer = Timer(timerIntervalInSeconds, resetFailedAutoReportDevices) self.autoReportWatchDogTimer.start() if None != self.autoReportWatchDogTimer \ and self.autoReportWatchDogTimer.isAlive(): self.autoReportWatchDogTimer.cancel() self.autoReportWatchDogTimer = None self.autoReportWatchDogTimer = Timer(timerIntervalInSeconds, resetFailedAutoReportDevices) self.autoReportWatchDogTimer.start() PE.logInfo("Started auto-report watchdog timer.")
def playAnnouncementAndMusicInTheMorning(event): global morningMusicStartCount global inMorningSession if isInMorningTimeRange() and \ morningMusicStartCount < MAX_MORNING_MUSIC_START_COUNT: if not inMorningSession: PE.logInfo('{} Playing morning annoucement.'.format(LOG_PREFIX)) inMorningSession = True msg = getMorningAnnouncement() casts = cast_manager.getFirstFloorCasts() cast_manager.playMessage(msg, casts) cast_manager.playStream("WWFM Classical", casts, 35) morningMusicStartCount += 1
def onAction(self, eventInfo): zone = eventInfo.getZone() zoneManager = eventInfo.getZoneManager() securityPartitions = zoneManager.getDevicesByType(AlarmPartition) if len(securityPartitions) == 0: return False if not securityPartitions[0].isArmedAway(): return False # Get an audio sink from the first floor. audioSink = None zones = [ z for z in zoneManager.getZones() if z.getLevel() == Level.FIRST_FLOOR ] for z in zones: sinks = z.getDevicesByType(ChromeCastAudioSink) if len(sinks) > 0: audioSink = sinks[0] break if None == audioSink: return False activities = zoneManager.getDevicesByType(ActivityTimes) if len(activities) > 0: if activities[0].isSleepTime(): return False audioSink.playStream(self.musicUrl, self.musicVolume) if None != self.timer: self.timer.cancel() durationInSeconds = self.playDurationInSeconds if None == durationInSeconds: durationInSeconds = random.randint(3 * 60, 10 * 60) self.timer = Timer(durationInSeconds, lambda: audioSink.pause()) self.timer.start() PE.logInfo( "Simulate daytime presence by playing music for {} seconds".format( durationInSeconds)) return True
def _checkAndSendAlert(zoneManager, checkFunction, deviceTypeString, thresholdInSeconds): inactiveDevices = checkFunction(zm, thresholdInSeconds) if len(inactiveDevices) > 0: subject = "{} inactive {} devices".format( len(inactiveDevices), deviceTypeString) body = "The following devices haven't triggered "\ "in the last {} hours\r\n - ".format( thresholdInSeconds / 3600) body += "\r\n - ".join(inactiveDevices) alert = Alert.createInfoAlert(subject, body) if not AlertManager.processAdminAlert(alert): PE.logInfo('Failed to send inactive {} device alert'.format(deviceTypeString)) else: PE.logInfo("No inactive {} devices detected.".format(deviceTypeString))
def processAlert(alert, zoneManager=None): ''' Processes the provided alert. If the alert's level is WARNING or CRITICAL, the TTS subject will be played on the ChromeCasts. :param Alert alert: the alert to be processed :param ImmutableZoneManager zoneManager: used to retrieve the ActivityTimes :return: True if alert was processed; False otherwise. :raise: ValueError if alert is None ''' if None == alert: raise ValueError('Invalid alert.') PE.logInfo(u"Processing alert\n{}".format(alert.toString())) if AlertManager._isThrottled(alert): return False if not alert.isAudioAlertOnly(): AlertManager._emailAlert(alert, AlertManager._getOwnerEmailAddresses()) # Play an audio message if the alert is warning or critical. # Determine the volume based on the current zone activity. volume = 0 if alert.isCriticalLevel(): volume = 60 elif alert.isWarningLevel(): if None == zoneManager: volume = 60 else: activity = zoneManager.getDevicesByType(ActivityTimes)[0] if activity.isSleepTime(): volume = 0 elif activity.isQuietTime(): volume = 40 else: volume = 60 if volume > 0: casts = cast_manager.getAllCasts() cast_manager.playMessage(alert.getSubject(), casts, volume) return True
def onAction(self, eventInfo): zone = eventInfo.getZone() zoneManager = eventInfo.getZoneManager() currentEpoch = time.time() doorOpenPeriodInSeconds = 10 for door in zone.getDevicesByType(Door): if door.wasRecentlyActivated(doorOpenPeriodInSeconds): PE.logInfo( "A door was just open for zone {}; ignore motion event.". format(zone.getName())) return cameras = zone.getDevicesByType(Camera) if len(cameras) == 0: PE.logInfo("No camera found for zone {}".format(zone.getName())) return camera = cameras[0] if not camera.hasMotionEvent(): PE.logInfo( "Camera doesn't indicate motion event; likely a false positive PIR event." ) return time.sleep(10) # wait for a bit to retrieve more images offsetSeconds = 5 maxNumberOfSeconds = 15 attachmentUrls = camera.getSnapshotUrls(currentEpoch, maxNumberOfSeconds, offsetSeconds) if len(attachmentUrls) > 0: timeStruct = time.localtime() hour = timeStruct[3] msg = 'Activity detected at the {} area.'.format( zone.getName(), len(attachmentUrls)) if SM.isArmedAway(zm) or hour <= 6: alert = Alert.createWarningAlert(msg, None, attachmentUrls) else: alert = Alert.createAudioWarningAlert(msg) AlertManager.processAlert(alert) return True else: PE.logInfo("No images from {} camera.".format(zone.getName())) return False
def processAdminAlert(alert): ''' Processes the provided alert by sending an email to the administrator. :param Alert alert: the alert to be processed :return: True if alert was processed; False otherwise. :raise: ValueError if alert is None ''' if None == alert: raise ValueError('Invalid alert.') PE.logInfo(u"Processing admin alert\n{}".format(alert.toString())) if AlertManager._isThrottled(alert): return False AlertManager._emailAlert(alert, AlertManager._getAdminEmailAddresses()) return True
def retrieveSnapshots(itemPrefix, snapshotCount): ''' Retrieve the supplied number of snapshots. :param str itemPrefix: the camera item prefix; the items Image and\ UpdateImage are created from the prefix. :param int snapshotCount: the # of snapshot images to retrieve :return: list of snapshot URLs :rtype: list(str) ''' attachmentUrls = [] imageItemName = itemPrefix + '_Image' updateItemName = itemPrefix + '_UpdateImage' PE.logInfo('Retrieving {} snapshots'.format(snapshotCount)) previousRawBytes = [] for idx in range(snapshotCount): # Flip the state of the update channel to force retrieval of new image if scope.items[updateItemName] == scope.OnOffType.ON: scope.events.sendCommand(updateItemName, "OFF") else: scope.events.sendCommand(updateItemName, "ON") time.sleep(_WAIT_TIME_AFTER_FORCE_IMAGE_UPDATE_IN_SECONDS) imageState = scope.items[imageItemName] if scope.UnDefType.UNDEF != imageState and scope.UnDefType.NULL != imageState: rawBytes = imageState.getBytes() if rawBytes != previousRawBytes: fileName = '{}/{}-{}.jpg'.format(_SNAPSHOT_PATH, itemPrefix, idx) file = io.open(fileName, 'wb') file.write(rawBytes) file.close() attachmentUrls.append('file://' + fileName) previousRawBytes = rawBytes return attachmentUrls
def playMorningAnnouncement(event): msg = getMorningAnnouncement() PE.logInfo(u"{} Saying: {}".format(LOG_PREFIX, msg)) cast_manager.playMessage(msg) events.sendCommand(event.itemName, 'OFF')
def pauseMorningMusic(event): global inMorningSession if isInMorningTimeRange() and inMorningSession: PE.logInfo('{} Pausing morning music.'.format(LOG_PREFIX)) cast_manager.pause() inMorningSession = False
def initializeZoneManager(): ''' Creates a new instance of ZoneManager and populate the zones. :rtype: ZoneManager ''' zm = ZoneManager() zones = ZoneParser().parse(items, itemRegistry, zm._createImmutableInstance()) # actions externalZoneActions = [ AlertOnEntraceActivity(), SimulateDaytimePresence("http://hestia2.cdnstream.com:80/1277_192"), AlertOnExternalDoorLeftOpen(), ArmAfterFrontDoorClosed(12 * 60), # arm after 12' ] fanActions = [PlayMusicDuringShower("http://hestia2.cdnstream.com:80/1277_192")] switchActions = [TurnOnSwitch(), TurnOffAdjacentZones()] # add virtual devices and actions for z in zones: if z.getName() == 'Virtual': timeMap = { 'wakeup': '6 - 9', 'lunch': '12:00 - 13:30', 'quiet' : '14:00 - 16:00, 20:00 - 22:59', 'dinner': '17:50 - 20:00', 'sleep': '23:00 - 7:00' } z = z.addDevice(ActivityTimes(timeMap)) if len(z.getDevicesByType(Switch)) > 0: for a in switchActions: z = z.addAction(a) if z.isExternal(): for a in externalZoneActions: z = z.addAction(a) if len(z.getDevicesByType(AlarmPartition)) > 0: z = z.addAction(TurnOffDevicesOnAlarmModeChange()) if len(z.getDevicesByType(HumiditySensor)) > 0 and not z.isExternal(): z = z.addAction(AlertOnHumidityOutOfRange()) if len(z.getDevicesByType(GasSensor)) > 0: z = z.addAction(AlertOnHighGasLevel()) # add the play music action if zone has a fan switch. fans = z.getDevicesByType(Fan) if len(fans) > 0: for a in fanActions: z = z.addAction(a) zm.addZone(z) PE.logInfo("Configured ZoneManager with {} zones.".format(len(zones))) zones = zm.getZones() output = "{} zones".format(len(zones)) for z in zones: output += '\n' + str(z) PE.logInfo(output) zm.startAutoReporWatchDog() return zm
def onAction(self, eventInfo): events = eventInfo.getEventDispatcher() zone = eventInfo.getZone() zoneManager = eventInfo.getZoneManager() isProcessed = False canTurnOffAdjacentZones = True lightOnTime = zone.isLightOnTime() zoneIlluminance = zone.getIlluminanceLevel() for switch in zone.getDevicesByType(Switch): if switch.isOn(): switch.turnOn(events) # renew the timer if a switch is already on isProcessed = True canTurnOffAdjacentZones = False continue if not switch.canBeTriggeredByMotionSensor(): # A special case: if a switch is configured not to be # triggered by a motion sensor, it means there is already # another switch sharing that motion sensor. In this case, we # don't want to turn off the other switch. canTurnOffAdjacentZones = False if DEBUG: PE.logInfo("{}: rejected - can't be triggerred by motion sensor".format( switch.getItemName())) continue # Break if switch was just turned off. if None != switch.getLastOffTimestampInSeconds(): if (time.time() - switch.getLastOffTimestampInSeconds()) <= \ TurnOnSwitch.DELAY_AFTER_LAST_OFF_TIME_IN_SECONDS: if DEBUG: PE.logInfo("{}: rejected - switch was just turned off".format( switch.getItemName())) continue # Break if the switch of a neighbor sharing the motion sensor was # just turned off. openSpaceZones = [zoneManager.getZoneById(n.getZoneId()) \ for n in zone.getNeighbors() if n.isOpenSpace()] sharedMotionSensorZones = [z for z in openSpaceZones if zone.shareSensorWith(z, MotionSensor)] theirSwitches = reduce(lambda a, b : a + b, [z.getDevicesByType(Switch) for z in sharedMotionSensorZones], []) if any(time.time() - s.getLastOffTimestampInSeconds() <= \ TurnOnSwitch.DELAY_AFTER_LAST_OFF_TIME_IN_SECONDS \ for s in theirSwitches): if DEBUG: PE.logInfo("{}: rejected - can't be triggerred by motion sensor".format( switch.getItemName())) continue if isinstance(switch, Light): if lightOnTime or switch.isLowIlluminance(zoneIlluminance): isProcessed = True if isProcessed and None != zoneManager: masterZones = [zoneManager.getZoneById(n.getZoneId()) \ for n in zone.getNeighbors() \ if NeighborType.OPEN_SPACE_MASTER == n.getType()] if any(z.isLightOn() for z in masterZones): isProcessed = False # This scenario indicates that there is already # activity in the master zone, and thus such activity # must not prematurely turns off the light in the # adjacent zone. canTurnOffAdjacentZones = False if DEBUG: PE.logInfo("{}: rejected - a master zone's light is on".format( switch.getItemName())) if isProcessed: switch.turnOn(events) else: switch.turnOn(events) isProcessed = True # Now shut off the light in any shared space zones if canTurnOffAdjacentZones: if DEBUG: PE.logInfo("{}: turning off adjancent zone's light".format( switch.getItemName())) offEventInfo = EventInfo(ZoneEvent.SWITCH_TURNED_ON, eventInfo.getItem(), eventInfo.getZone(), eventInfo.getZoneManager(), eventInfo.getEventDispatcher()) TurnOffAdjacentZones().onAction(offEventInfo) return isProcessed