diff --git a/devicetypes/com-obycode/obything-music-player.src/obything-music-player.groovy b/devicetypes/com-obycode/obything-music-player.src/obything-music-player.groovy new file mode 100644 index 0000000..85e104a --- /dev/null +++ b/devicetypes/com-obycode/obything-music-player.src/obything-music-player.groovy @@ -0,0 +1,296 @@ +/** + * obything Music Player + * + * Copyright 2015 obycode + */ + +import groovy.json.JsonSlurper + +metadata { + definition (name: "obything Music Player", namespace: "com.obycode", author: "obycode") { + capability "Music Player" + capability "Refresh" + capability "Switch" + + // These strings are comma separated lists of names + attribute "playlists", "json_object" + attribute "speakers", "json_object" + attribute "playlistDescription", "string" + + // playPlaylist(String uri, speakers=null, volume=null, resume=false, restore=false) + command "playPlaylist", ["string", "string", "number", "number", "number"] + // playTrack(String uri, speakers=null, volume=null, resume=false, restore=false, playlist=null) + command "playTrack", ["string", "string", "number", "number", "number", "string"] + + command "update", ["string"] + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + tiles(scale: 2) { + multiAttributeTile(name:"richmusic", type:"lighting", width:6, height:4) { + tileAttribute("device.status", key: "PRIMARY_CONTROL") { + attributeState "paused", label: 'Paused', action:"music Player.play", icon:"http://obything.obycode.com/icons/obything-device.png", backgroundColor:"#D0D0D0" + attributeState "stopped", label: 'Stopped', action:"music Player.play", icon:"http://obything.obycode.com/icons/obything-device.png", backgroundColor:"#D0D0D0" + attributeState "playing", label:'Playing', action:"music Player.pause", icon:"http://obything.obycode.com/icons/obything-device.png", backgroundColor:"#4C4CFF" + } + tileAttribute("device.trackDescription", key: "SECONDARY_CONTROL") { + attributeState "default", label:'${currentValue}' + } + tileAttribute("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"music Player.setLevel", range:"(0..100)" + } + } + + standardTile("nextTrack", "device.status", width: 2, height: 2, decoration: "flat") { + state "next", label:'', action:"music Player.nextTrack", icon:"st.sonos.next-btn", backgroundColor:"#ffffff" + } + standardTile("playpause", "device.status", width: 2, height: 2, decoration: "flat") { + state "default", label:'', action:"music Player.play", icon:"st.sonos.play-btn", backgroundColor:"#ffffff" + state "playing", label:'', action:"music Player.pause", icon:"st.sonos.pause-btn", backgroundColor:"#ffffff" + state "paused", label:'', action:"music Player.play", icon:"st.sonos.play-btn", backgroundColor:"#ffffff" + } + standardTile("previousTrack", "device.status", width: 2, height: 2, decoration: "flat") { + state "previous", label:'', action:"music Player.previousTrack", icon:"st.sonos.previous-btn", backgroundColor:"#ffffff" + } + standardTile("refresh", "device.status", width: 2, height: 2, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh", backgroundColor:"#ffffff" + } + standardTile("mute", "device.mute", width: 2, height: 2, decoration: "flat") { + state "unmuted", label:"Mute", action:"music Player.mute", icon:"st.custom.sonos.unmuted", backgroundColor:"#ffffff" + state "muted", label:"Unmute", action:"music Player.unmute", icon:"st.custom.sonos.muted", backgroundColor:"#ffffff" + } + controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 4) { + state "level", label:"Volume", action:"music Player.setLevel", backgroundColor:"#ffffff" + } + valueTile("currentPlaylist", "device.playlistDescription", height:2, width:6, decoration: "flat") { + state "default", label:'${currentValue}', backgroundColor:"#ffffff" + } + + main "richmusic" + details(["richmusic", + "previousTrack","nextTrack","refresh", + "levelSliderControl","mute", + "currentPlaylist" + ]) + } + } +} + +// parse events into attributes +def parse(String description) { + // log.debug "parse called with $description" + def map = stringToMap(description) + if (map.headers && map.body) { //got device info response + if (map.body) { + def bodyString = new String(map.body.decodeBase64()) + def slurper = new JsonSlurper() + def result = slurper.parseText(bodyString) + parseMessage(result) + } + } +} + +def parseMessage(message) { + log.debug "message is $message" + if (message.containsKey("volume")) { + log.debug "setting volume to ${message.volume}" + sendEvent(name: "level", value: message.volume) + } + if (message.containsKey("mute")) { + log.debug "setting mute to ${message.mute}" + sendEvent(name: "mute", value: message.mute) + } + if (message.containsKey("status")) { + log.debug "setting status to ${message.status}" + sendEvent(name: "status", value: message.status) + } + if (message.containsKey("trackData")) { + def json = new groovy.json.JsonBuilder(message.trackData) + log.debug "setting trackData to ${json.toString()}" + sendEvent(name: "trackData", value: json.toString()) + } + if (message.containsKey("trackDescription")) { + log.debug "setting trackDescription info to ${message.trackDescription}" + sendEvent(name: "trackDescription", value: message.trackDescription) + } + if (message.containsKey("playlistData")) { + def json = new groovy.json.JsonBuilder(message.playlistData) + log.debug "setting playlistData to ${json.toString()}" + sendEvent(name: "playlistData", value: json.toString()) + } + if (message.containsKey("playlistDescription")) { + log.debug "setting playlistDescription info to ${message.playlistDescription}" + sendEvent(name: "playlistDescription", value: message.playlistDescription) + } + if (message.containsKey("playlists")) { + def json = new groovy.json.JsonBuilder(message.playlists) + log.debug "setting playlists to ${json.toString()}" + sendEvent(name: "playlists",value: json.toString()) + } + if (message.containsKey("speakers")) { + def json = new groovy.json.JsonBuilder(message.speakers) + log.debug "setting speakers to ${json.toString()}" + sendEvent(name: "speakers",value: json.toString()) + } +} + +// Called by service manager to send updates from device +def update(message) { + log.debug "update: $message" + parseMessage(message) +} + +def installed() { + // Refresh to get current state + refresh() +} + +// handle commands +def refresh() { + log.debug "Executing 'refresh'" + getInfo("command=refresh") +} + +def on() { + log.debug "Executing 'on' (play)" + sendCommand("command=play") +} + +def off() { + log.debug "Executing 'off' (pause)" + sendCommand("command=pause") +} + +def play() { + log.debug "Executing 'play'" + sendCommand("command=play") +} + +def pause() { + log.debug "Executing 'pause'" + sendCommand("command=pause") +} + +def stop() { + log.debug "Executing 'stop'" + sendCommand("command=stop") +} + +def nextTrack() { + log.debug "Executing 'nextTrack'" + sendCommand("command=next") +} + +def setLevel(value) { + log.debug "Executing 'setLevel' to $value" + sendCommand("command=volume&level=$value") +} + +// def playText(String msg) { +// log.debug "Executing 'playText'" +// sendCommand("say=$msg") +// } +// +def mute() { + log.debug "Executing 'mute'" + sendCommand("command=mute") +} + +def previousTrack() { + log.debug "Executing 'previousTrack'" + sendCommand("command=previous") +} + +def unmute() { + log.debug "Executing 'unmute'" + sendCommand("command=unmute") +} + +def playPlaylist(String uri, speakers=null, volume=null) { + log.trace "playPlaylist($uri, $speakers, $volume, $resume, $restore)" + def command = "command=playlist&name=${uri}" + if (speakers) { + command += "&speakers=${speakers}" + } + if (volume) { + command += "&volume=${volume}" + } + sendCommand(command) +} + +def playTrack(String uri, speakers=null, volume=null, resume=false, restore=false, playlist=null) { + log.trace "playTrack($uri, $speakers, $volume, $resume, $restore, $playlist)" + def command = "command=track&url=${uri}" + if (speakers) { + command += "&speakers=${speakers}" + } + if (volume) { + command += "&volume=${volume}" + } + if (resume) { + command += "&resume=" + } + else if (restore) { + command += "&restore=" + } + if (playlist) { + command += "&playlist=$playlist" + } + sendCommand(command) +} + +// def speak(text) { +// def url = textToSpeech(text) +// sendCommand("playTrack&track=${url.uri}&resume") +// } +// +// def beep() { +// sendCommand("beep") +// } + +// Private functions used internally +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +private getHostAddress() { + def parts = device.deviceNetworkId.split(":") + def ip = convertHexToIP(parts[0]) + def port = convertHexToInt(parts[1]) + return ip + ":" + port +} + +private sendCommand(command) { + def path = "/player&" + command + + def result = new physicalgraph.device.HubAction( + method: "POST", + path: path, + headers: [ + HOST: getHostAddress() + ], + ) + result +} + +private getInfo(command) { + def path = "/player&" + command + + def result = new physicalgraph.device.HubAction( + method: "GET", + path: path, + headers: [ + HOST: getHostAddress() + ], + ) + result +} diff --git a/smartapps/com-obycode/obything-connect.src/obything-connect.groovy b/smartapps/com-obycode/obything-connect.src/obything-connect.groovy new file mode 100644 index 0000000..b8ecb50 --- /dev/null +++ b/smartapps/com-obycode/obything-connect.src/obything-connect.groovy @@ -0,0 +1,211 @@ +/** + * ObyThing Music Connect + * + * Copyright 2015 obycode + * + */ +definition( + name: "obything Connect", + namespace: "com.obycode", + author: "obycode", + description: "Smart home, smart Mac. With obything.", + category: "Fun & Social", + iconUrl: "http://obything.obycode.com/icons/icon60.png", + iconX2Url: "http://obything.obycode.com/icons/icon120.png", + iconX3Url: "http://obything.obycode.com/icons/icon120.png", + oauth: [displayName: "obything", displayLink: "http://obything.obycode.com"]) + + +preferences { + section("Smart home, smart Mac. With obything.") { + app(name: "childApps", appName: "obything Notify with Sound", namespace: "com.obycode", title: "Notify with Sound", multiple: true) + app(name: "childApps", appName: "obything Music Control", namespace: "com.obycode", title: "Music Control", multiple: true) + app(name: "childApps", appName: "obything Trigger Playlist", namespace: "com.obycode", title: "Trigger Playlists", multiple: true) + app(name: "childApps", appName: "obything Weather Forecast", namespace: "com.obycode", title: "Weather Forecast", multiple: true) + } +} + +mappings { + path("/setup") { + action: [ + POST: "setup", + ] + } + path("/:uuid/:kind") { + action: [ + POST: "createChild", + PUT: "updateChild", + ] + } +} + +def installed() { + log.debug "Installed" +} + +def updated() { + log.debug "Updated" + + unsubscribe() +} + +// mapping handlers + +// /setup POST +def setup() { + def ip = request.JSON?.ip + if (ip == null) { + return httpError(400, "IP not specified") + } + def port = request.JSON?.port + if (port == null) { + return httpError(400, "Port not specified") + } + def name = request.JSON?.name + if (name == null) { + return httpError(400, "Name not specified") + } + def uuid = request.JSON?.uuid + if (uuid == null) { + return httpError(400, "UUID not specified") + } + + // If machines is not initialized yet, set it up + if (state.machines == null) { + state.machines = [:] + } + + // If this machine has already been initialized, just update it + if (state.machines[uuid]) { + state.machines[uuid]["ip"] = ip + state.machines[uuid]["port"] = port + state.machines[uuid]["name"] = name + log.debug "Updated machine" + + def dead = [] + // Update the child devices + def newHexIP = convertIPtoHex(ip) + state.machines[uuid]["children"].keySet().each { + def ids = state.machines[uuid]["children"][it] + def child = getChildDevice(ids.dni) + if (!child) { + dead.add(it) + } + else { + // Only change the IP; the label could've been manually changed and I'm + // not sure how to handle the port changing (its not allowed now anyway). + def oldHexPort = child.deviceNetworkId.split(':')[1] + def newDNI = "$newHexIP:$oldHexPort" + child.deviceNetworkId = newDNI + state.machines[uuid]["children"][it]["dni"] = newDNI + } + } + dead.each { + state.machines[uuid]["children"].remove(it) + } + } + // Otherwise, just create a new machine + else { + def machine = [ip:ip, port:port, name:name, children:[:]] + state.machines[uuid] = machine + log.debug "Added new machine" + } + + sendCommand(state.machines[uuid], "/ping") +} + +private removeChildDevices(delete) { + delete.each { + deleteChildDevice(it.deviceNetworkId) + } +} + +// /:uuid/:kind POST +def createChild() { + // Constants for the port offsets + final int iTunesService = 1 + final int pandoraService = 2 + final int spotifyService = 3 + + def machine = state.machines[params.uuid] + if (machine == null) { + return httpError(404, "Machine not found") + } + + def childName = machine["name"] + def portNum = machine["port"].toInteger() + switch (params.kind) { + case "itunes": + childName = childName + " iTunes" + portNum += iTunesService + break + case "pandora": + childName = childName + " Pandora" + portNum += pandoraService + break + case "spotify": + childName = childName + " Spotify" + portNum += spotifyService + break + default: + return httpError(400, "Unknown or unspecified device type") + } + + def hexIP = convertIPtoHex(machine["ip"]) + def hexPort = convertPortToHex(portNum.toString()) + def childId = "$hexIP:$hexPort" + + // If this child already exists, re-associate with it + def existing = getChildDevice(childId) + if (existing) { + log.debug "Found existing device: $existing" + state.machines[params.uuid]["children"][params.kind] = [id:existing.id, dni:childId] + } + // otherwise, create it + else { + def d = addChildDevice("com.obycode", "obything Music Player", childId, location.hubs[0].id, [name:"obything Music Player", label:childName, completedSetup:true]) + log.debug "Created child device: $d" + state.machines[params.uuid]["children"][params.kind] = [id:d.id, dni:d.deviceNetworkId] + } + + return [dni:childId] +} + +// /:uuid/:kind PUT +def updateChild() { + def machine = state.machines[params.uuid] + if (machine == null) { + return httpError(404, "Machine not found") + } + + def child = machine["children"][params.kind] + if (child == null) { + return httpError(404, "Device not found") + } + + def device = getChildDevice(child.dni) + if (device == null) { + return httpError(404, "Device not found") + } + + device.update(request.JSON) +} + +private String convertIPtoHex(ipAddress) { + String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02X', it.toInteger() ) }.join() + return hex + +} + +private String convertPortToHex(port) { + String hexport = port.toString().format( '%04X', port.toInteger() ) + return hexport +} + +private void sendCommand(machine, path, command = null) { + def fullPath = path + if (command) { + fullPath = fullPath + "?" + command + } + sendHubCommand(new physicalgraph.device.HubAction("GET " + fullPath + """ HTTP/1.1\r\nHOST: """ + machine["ip"] + ":" + machine["port"] + """\r\n\r\n""", physicalgraph.device.Protocol.LAN)) +} diff --git a/smartapps/com-obycode/obything-music-control.src/obything-music-control.groovy b/smartapps/com-obycode/obything-music-control.src/obything-music-control.groovy new file mode 100644 index 0000000..16c74ba --- /dev/null +++ b/smartapps/com-obycode/obything-music-control.src/obything-music-control.groovy @@ -0,0 +1,306 @@ +/** +* Music Control +* +* Author: obything +* +* Date: 2015-11-09 +*/ +definition( + name: "obything Music Control", + namespace: "com.obycode", + author: "obycode, based on Music Control by SmartThings", + description: "Play or pause your music when certain actions take place in your home.", + category: "Convenience", + iconUrl: "http://obything.obycode.com/icons/obything-device.png", + iconX2Url: "http://obything.obycode.com/icons/obything-device.png" +) + +preferences { + page(name: "mainPage", title: "Control your music when something happens", install: true, uninstall: true) + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage") { + def anythingSet = anythingSet() + if (anythingSet) { + section("When..."){ + ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section("Perform this action"){ + input "actionType", "enum", title: "Action?", required: true, defaultValue: "play", options: [ + "Play", + "Stop Playing", + "Toggle Play/Pause", + "Skip to Next Track", + "Play Previous Track" + ] + } + section { + input "obything", "capability.musicPlayer", title: "obything Music player", required: true + } + section("More options", hideable: true, hidden: true) { + input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + if (settings.modes) { + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } + } +} + +private anythingSet() { + for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","triggerModes","timeOfDay"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + log.trace "subscribeToEvents()" + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location, modeChangeHandler) + } + + if (timeOfDay) { + schedule(timeOfDay, scheduledTimeHandler) + } +} + +def eventHandler(evt) { + if (allOk) { + def lastTime = state[frequencyKey(evt)] + if (oncePerDayOk(lastTime)) { + if (frequency) { + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + else { + log.debug "Not taking action because $frequency minutes have not elapsed since last action" + } + } + else { + takeAction(evt) + } + } + else { + log.debug "Not taking action because it was already taken today" + } + } +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + takeAction(evt) +} + +private takeAction(evt) { + log.debug "takeAction($actionType)" + def options = [:] + if (volume) { + obything.setLevel(volume as Integer) + options.delay = 1000 + } + + switch (actionType) { + case "Play": + obything.play() + break + case "Stop Playing": + obything.pause() + break + case "Toggle Play/Pause": + def currentStatus = obything.currentValue("status") + log.debug "Current status is $currentStatus" + if (currentStatus == "playing") { + obything.pause() + } + else { + obything.play() + } + break + case "Skip to Next Track": + obything.nextTrack() + break + case "Play Previous Track": + obything.previousTrack() + break + default: + log.error "Action type '$actionType' not defined" + } + + if (frequency) { + state.lastActionTimeStamp = now() + } +} + +private frequencyKey(evt) { + //evt.deviceId ?: evt.value + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = true + if (oncePerDay) { + result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + } + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private timeIntervalLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} +// TODO - End Centralize diff --git a/smartapps/com-obycode/obything-notify-with-sound.src/obything-notify-with-sound.groovy b/smartapps/com-obycode/obything-notify-with-sound.src/obything-notify-with-sound.groovy new file mode 100644 index 0000000..9036bc6 --- /dev/null +++ b/smartapps/com-obycode/obything-notify-with-sound.src/obything-notify-with-sound.groovy @@ -0,0 +1,410 @@ +/** +* obything Notify with Sound +* +* Author: obycode +* Date: 2015-08-30 +*/ + +import groovy.json.JsonSlurper + +definition( + name: "obything Notify with Sound", + namespace: "com.obycode", + author: "obycode, based on 'Sonos Notify with Sound' by SmartThings", + description: "Play a sound or custom message through your Mac with obything when the mode changes or other events occur.", + category: "Convenience", + iconUrl: "http://obything.obycode.com/icons/obything-device.png", + iconX2Url: "http://obything.obycode.com/icons/obything-device.png", + parent: "com.obycode:obything Connect" +) + +preferences { + page(name: "mainPage", title: "Play a message over your speakers when something happens", install: true, uninstall: true) + page(name: "chooseTrack", title: "Select a playlist") + page(name: "chooseSpeakers", title: "Select speakers") + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage") { + def anythingSet = anythingSet() + if (anythingSet) { + section("Play message when"){ + ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + def hideable = anythingSet || app.installationState == "COMPLETE" + def sectionTitle = anythingSet ? "Select additional triggers" : "Play message when..." + + section(sectionTitle, hideable: hideable, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifUnset "triggerModes", "mode", title: "System Changes Mode", description: "Select mode(s)", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section{ + input "actionType", "enum", title: "Action?", required: true, defaultValue: "Custom Message", options: [ + "Custom Message", + "Custom URL", + "Bell 1", + "Bell 2", + "Dogs Barking", + "Fire Alarm", + "The mail has arrived", + "A door opened", + "There is motion", + "Smartthings detected a flood", + "Smartthings detected smoke", + "Someone is arriving", + "Piano", + "Lightsaber"] + input "message","text",title:"Play this message", required:false, multiple: false + input "url","text",title:"Play a sound at this URL", required:false, multiple: false + } + section { + input "obything", "capability.musicPlayer", title: "On this obything iTunes device", required: true + } + section { + href "chooseSpeakers", title: "With these speakers", description: speakers ? speakers : "Tap to set", state: speakers ? "complete" : "incomplete" + } + section("More options", hideable: true, hidden: true) { + input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true + href "chooseTrack", title: "Or play this music or radio station", description: playlist ? playlist : "Tap to set", state: playlist ? "complete" : "incomplete" + + input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + if (settings.modes) { + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)", required: false + } + } +} + +def chooseTrack() { + dynamicPage(name: "chooseTrack") { + section{ + input "playlist", "enum", title:"Play this playlist", required:true, multiple: false, options: playlistOptions() + } + } +} + +private playlistOptions() { + def playlistString = obything.currentValue("playlists") + log.debug "Playlists are $playlistString" + def jsonList = new JsonSlurper().parseText(playlistString) + jsonList.collect { + it.Name + } +} + +def chooseSpeakers() { + dynamicPage(name: "chooseSpeakers") { + section{ + input "speakers", "enum", title:"Play on these speakers", required:false, multiple: true, options: speakerOptions() + } + } +} + +private speakerOptions() { + def speakersString = obything.currentValue("speakers") + log.debug "Speakers are $speakersString" + def slurper = new JsonSlurper() + slurper.parseText(speakersString) +} + +private anythingSet() { + for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes","timeOfDay"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location, modeChangeHandler) + } + + if (timeOfDay) { + runDaily(timeOfDay, scheduledTimeHandler) + } + + loadText() +} + +def eventHandler(evt) { + log.trace "eventHandler($evt?.name: $evt?.value)" + if (allOk) { + log.trace "allOk" + def lastTime = state[frequencyKey(evt)] + if (oncePerDayOk(lastTime)) { + if (frequency) { + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + else { + log.debug "Not taking action because $frequency minutes have not elapsed since last action" + } + } + else { + takeAction(evt) + } + } + else { + log.debug "Not taking action because it was already taken today" + } + } +} +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + takeAction(evt) +} + +private takeAction(evt) { + log.trace "takeAction()" + + def speakerString + if (speakers) { + speakerString = "" + speakers.each { + speakerString += "\"$it\"," + } + // remove the last comma and encode + speakerString = encode(speakerString[0..-2]) + } + + if (playlist) { + obything.playTrack(state.sound.uri, speakerString, volume, resumePlaying, playlist) + } + else { + obything.playTrack(state.sound.uri, speakerString, volume, resumePlaying) + } + + if (frequency || oncePerDay) { + state[frequencyKey(evt)] = now() + } + log.trace "Exiting takeAction()" +} + +private frequencyKey(evt) { + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = true + if (oncePerDay) { + result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + } + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private getTimeLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} +// TODO - End Centralize + +private loadText() { + switch ( actionType) { + case "Bell 1": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"] + break; + case "Bell 2": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell2.mp3", duration: "10"] + break; + case "Dogs Barking": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/dogs.mp3", duration: "10"] + break; + case "Fire Alarm": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/alarm.mp3", duration: "17"] + break; + case "The mail has arrived": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/the+mail+has+arrived.mp3", duration: "1"] + break; + case "A door opened": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/a+door+opened.mp3", duration: "1"] + break; + case "There is motion": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/there+is+motion.mp3", duration: "1"] + break; + case "Smartthings detected a flood": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+a+flood.mp3", duration: "2"] + break; + case "Smartthings detected smoke": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+smoke.mp3", duration: "1"] + break; + case "Someone is arriving": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/someone+is+arriving.mp3", duration: "1"] + break; + case "Piano": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/piano2.mp3", duration: "10"] + break; + case "Lightsaber": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"] + break; + case "Custom Message": + if (message) { + log.debug "message is $message" + state.sound = textToSpeech(message) // instanceof List ? message[0] : message) // not sure why this is (sometimes) needed) + } + else { + state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App") + } + break; + case "Custom URL": + if (url) { + state.sound = [uri: url, duration: "0"] + } + else { + state.sound = textToSpeech("You selected the custom URL option but did not enter a URL in the $app.label Smart App") + } + break; + default: + log.debug "Invalid selection." + break; + } +} diff --git a/smartapps/com-obycode/obything-trigger-playlist.src/obything-trigger-playlist.groovy b/smartapps/com-obycode/obything-trigger-playlist.src/obything-trigger-playlist.groovy new file mode 100644 index 0000000..c214ac8 --- /dev/null +++ b/smartapps/com-obycode/obything-trigger-playlist.src/obything-trigger-playlist.groovy @@ -0,0 +1,313 @@ +/** +* obything Trigger Playlist +* +* Author: obycode +* Date: 2015-11-05 +*/ + +import groovy.json.JsonSlurper + +definition( + name: "obything Trigger Playlist", + namespace: "com.obycode", + author: "obycode, based on Sonos Mood Music by SmartThings", + description: "Plays a selected playlist on your Mac with obything", + category: "Convenience", + iconUrl: "http://obything.obycode.com/icons/obything-device.png", + iconX2Url: "http://obything.obycode.com/icons/obything-device.png" +) + +preferences { + page(name: "mainPage", title: "Choose the trigger(s)", nextPage: "chooseTrackAndSpeakers", uninstall: true) + page(name: "chooseTrackAndSpeakers", title: "Choose the playlist and speaker(s)", install: true, uninstall: true) + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage") { + def anythingSet = anythingSet() + if (anythingSet) { + section("Play music when..."){ + ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + + def hideable = anythingSet || app.installationState == "COMPLETE" + def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..." + + section(sectionTitle, hideable: hideable, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true + ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section { + input "obything", "capability.musicPlayer", title: "On this obything music player", required: true + } + section("More options", hideable: true, hidden: true) { + input "volume", "number", title: "Set the volume", description: "0-100%", required: false + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Sunday","Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + if (settings.modes) { + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + } +} + +def chooseTrackAndSpeakers() { + dynamicPage(name: "chooseTrackAndSpeakers") { + section { + input "playlist", "enum", title:"Play this playlist", required:true, multiple: false, options: playlistOptions() + input "speakers", "enum", title:"On these speakers", required:false, multiple: true, options: speakerOptions() + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)", required: false + } + } +} + +private playlistOptions() { + def playlistString = obything.currentValue("playlists") + log.debug "Playlists are $playlistString" + def jsonList = new JsonSlurper().parseText(playlistString) + log.debug("jsonList is $jsonList") + jsonList.collect { + it.name + } +} + +def chooseSpeakers() { + dynamicPage(name: "chooseSpeakers") { + section{ + input "speakers", "enum", title:"Play on these speakers", required:false, multiple: true, options: speakerOptions() + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)", required: false + } + } +} + +private speakerOptions() { + def speakersString = obything.currentValue("speakers") + log.debug "Speakers are $speakersString" + def slurper = new JsonSlurper() + slurper.parseText(speakersString) +} + +private anythingSet() { + for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes","timeOfDay"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + log.trace "subscribeToEvents()" + + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location, modeChangeHandler) + } + + if (timeOfDay) { + runDaily(timeOfDay, scheduledTimeHandler) + } +} + +def eventHandler(evt) { + log.debug "In eventHandler" + if (allOk) { + if (frequency) { + def lastTime = state[frequencyKey(evt)] + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + } + else { + takeAction(evt) + } + } +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + takeAction(evt) +} + +private takeAction(evt) { + log.info "Playing '$playlist'" + def speakerString + if (speakers) { + speakerString = "" + speakers.each { + speakerString += "\"$it\"," + } + // remove the last comma and encode + speakerString = encode(speakerString[0..-2]) + } + obything.playPlaylist(encode(playlist), speakerString, volume) + + if (frequency || oncePerDay) { + state[frequencyKey(evt)] = now() + } +} + +private frequencyKey(evt) { + "lastActionTimeStamp" +} + +private encode(text) { + return URLEncoder.encode(text).replaceAll("\\+", "%20") +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private timeIntervalLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} diff --git a/smartapps/com-obycode/obything-weather-forecast.src/obything-weather-forecast.groovy b/smartapps/com-obycode/obything-weather-forecast.src/obything-weather-forecast.groovy new file mode 100644 index 0000000..76038f0 --- /dev/null +++ b/smartapps/com-obycode/obything-weather-forecast.src/obything-weather-forecast.groovy @@ -0,0 +1,405 @@ +/** +* obything Weather Forecast +* +* Author: obycode +* Date: 2014-10-13 +*/ + +import groovy.json.JsonSlurper + +definition( + name: "obything Weather Forecast", + namespace: "com.obycode", + author: "obycode, based on Sonos Weather Forecast by SmartThings", + description: "Play a weather report through your Mac or AirPlay speakers when the mode changes or other events occur", + category: "Convenience", + iconUrl: "http://obything.obycode.com/icons/obything-device.png", + iconX2Url: "http://obything.obycode.com/icons/obything-device.png" +) + +preferences { + page(name: "mainPage", title: "Play the weather report on your AirPlay speakers", install: true, uninstall: true) + page(name: "chooseTrack", title: "Select a playlist") + page(name: "chooseSpeakers", title: "Select speakers") + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage") { + def anythingSet = anythingSet() + if (anythingSet) { + section("Play weather report when"){ + ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + def hideable = anythingSet || app.installationState == "COMPLETE" + def sectionTitle = anythingSet ? "Select additional triggers" : "Play weather report when..." + + section(sectionTitle, hideable: hideable, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section { + input("forecastOptions", "enum", defaultValue: "0", title: "Weather report options", description: "Select one or more", multiple: true, + options: [ + ["0": "Current Conditions"], + ["1": "Today's Forecast"], + ["2": "Tonight's Forecast"], + ["3": "Tomorrow's Forecast"], + ] + ) + } + section { + input "obything", "capability.musicPlayer", title: "On this obything iTunes device", required: true + } + section { + href "chooseSpeakers", title: "With these speakers", description: speakers ? speakers : "Tap to set", state: speakers ? "complete" : "incomplete" + } + section("More options", hideable: true, hidden: true) { + input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true + href "choosePlaylist", title: "Or play this playlist", description: playlist ? playlist : "Tap to set", state: playlist ? "complete" : "incomplete" + + input "zipCode", "text", title: "Zip Code", required: false + input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + if (settings.modes) { + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } + } +} + +def chooseTrack() { + dynamicPage(name: "chooseTrack") { + section{ + input "playlist", "enum", title:"Play this playlist", required:true, multiple: false, options: playlistOptions() + } + } +} + +def chooseSpeakers() { + dynamicPage(name: "chooseSpeakers") { + section{ + input "speakers", "enum", title:"Play on these speakers", required:false, multiple: true, options: speakerOptions() + } + } +} + +private speakerOptions() { + def speakersString = obything.currentValue("speakers") + log.debug "Speakers are $speakersString" + def slurper = new JsonSlurper() + slurper.parseText(speakersString) +} + +private anythingSet() { + for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location,modeChangeHandler) + } + + if (timeOfDay) { + runDaily(timeOfDay, scheduledTimeHandler) + } +} + +def eventHandler(evt) { + log.trace "eventHandler($evt?.name: $evt?.value)" + if (allOk) { + log.trace "allOk" + def lastTime = state[frequencyKey(evt)] + if (oncePerDayOk(lastTime)) { + if (frequency) { + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + else { + log.debug "Not taking action because $frequency minutes have not elapsed since last action" + } + } + else { + takeAction(evt) + } + } + else { + log.debug "Not taking action because it was already taken today" + } + } +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + takeAction(evt) +} + +private takeAction(evt) { + + loadText() + + def speakerString + if (speakers) { + speakerString = "" + speakers.each { + speakerString += "$it," + } + // remove the last comma and encode + speakerString = encode(speakerString[0..-2]) + } + + if (playlist) { + obything.playTrack(state.sound.uri, speakerString, volume, resumePlaying, playlist) + } + else { + obything.playTrack(state.sound.uri, speakerString, volume, resumePlaying) + } + + if (frequency || oncePerDay) { + state[frequencyKey(evt)] = now() + } +} + +private playlistOptions() { + def playlistString = obything.currentValue("playlists") + log.debug "Playlists are $playlistString" + def jsonList = new JsonSlurper().parseText(playlistString) + jsonList.collect { + it.Name + } +} + +private frequencyKey(evt) { + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = true + if (oncePerDay) { + result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + } + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private getTimeLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} +// TODO - End Centralize + +private loadText() { + if (location.timeZone || zipCode) { + def weather = getWeatherFeature("forecast", zipCode) + def current = getWeatherFeature("conditions", zipCode) + def isMetric = location.temperatureScale == "C" + def delim = "" + def sb = new StringBuilder() + list(forecastOptions).sort().each {opt -> + if (opt == "0") { + if (isMetric) { + sb << "The current temperature is ${Math.round(current.current_observation.temp_c)} degrees." + } + else { + sb << "The current temperature is ${Math.round(current.current_observation.temp_f)} degrees." + } + delim = " " + } + else if (opt == "1") { + sb << delim + sb << "Today's forecast is " + if (isMetric) { + sb << weather.forecast.txt_forecast.forecastday[0].fcttext_metric + } + else { + sb << weather.forecast.txt_forecast.forecastday[0].fcttext + } + } + else if (opt == "2") { + sb << delim + sb << "Tonight will be " + if (isMetric) { + sb << weather.forecast.txt_forecast.forecastday[1].fcttext_metric + } + else { + sb << weather.forecast.txt_forecast.forecastday[1].fcttext + } + } + else if (opt == "3") { + sb << delim + sb << "Tomorrow will be " + if (isMetric) { + sb << weather.forecast.txt_forecast.forecastday[2].fcttext_metric + } + else { + sb << weather.forecast.txt_forecast.forecastday[2].fcttext + } + } + } + + def msg = sb.toString() + msg = msg.replaceAll(/([0-9]+)C/,'$1 degrees') // TODO - remove after next release + log.debug "msg = ${msg}" + state.sound = textToSpeech(msg, true) + } + else { + state.sound = textToSpeech("Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.") + } +} + +private list(String s) { + [s] +} +private list(l) { + l +}