diff --git a/smartapps/smartthings/samsung-multiroom-url-speaker-control.src/samsung-multiroom-url-speaker-control.groovy b/smartapps/smartthings/samsung-multiroom-url-speaker-control.src/samsung-multiroom-url-speaker-control.groovy new file mode 100644 index 0000000..50a02d9 --- /dev/null +++ b/smartapps/smartthings/samsung-multiroom-url-speaker-control.src/samsung-multiroom-url-speaker-control.groovy @@ -0,0 +1,334 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Speaker Control + * + * Author: SmartThings + * + * Date: 2013-12-10 + * Modifications Author: Richard Bourne + * Date: 2017-18-02 + * Volume control currently does not work for R1 speakers. + */ +definition( + name: "Samsung Multiroom URL Speaker Control", + namespace: "smartthings", + author: "Richard Bourne", + description: "Play or pause your Samsung Speaker when certain actions take place in your home. Use the URL to define source of audio e.g. Radio/audio server.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png" +) + +preferences { + page(name: "mainPage", title: "Control your Speaker 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 URL", + "Play", + "Stop Playing", + "Toggle Play/Pause", + "Skip to Next Track", + "Play Previous Track" + + ] + } + section { + input "samsungspeaker", "capability.musicPlayer", title: "Speaker 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: ["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)" + } + section("Define a URL") { + input "URL", "text", title: "URL link:", defaultValue: "http://bbcmedia.ic.llnwd.net/stream/bbcmedia_radio1_mf_p", required: true + } + } +} + +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 (settings.volume) { + log.debug "volume altered ($options)" + samsungspeaker.setLevel(settings.volume as Number) + pause(500) + } + + switch (actionType) { + case "Play": + options ? samsungspeaker.on(options) : samsungspeaker.on() + break + case "Stop Playing": + options ? samsungspeaker.off(options) : samsungspeaker.off() + break + case "Toggle Play/Pause": + def currentStatus = samsungspeaker.currentValue("status") + if (currentStatus == "playing") { + options ? samsungspeaker.pause(options) : samsungspeaker.pause() + } + else { + options ? samsungspeaker.play(options) : samsungspeaker.play() + } + break + case "Skip to Next Track": + options ? samsungspeaker.nextTrack(options) : samsungspeaker.nextTrack() + break + case "Play Previous Track": + options ? samsungspeaker.previousTrack(options) : samsungspeaker.previousTrack() + break + case "Play URL": + samsungspeaker.playTrack(settings.URL as String) + break + case "5": + samsungspeaker.playTrack(settings.URL as String) + break + 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, location?.timeZone).time + def stop = timeToday(ending, location?.timeZone).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 +