Bloomsky code for Hubitat.
Connect App:
/**
* BloomSky (Connect)
*
* Modified to work with Hubitat by cuboy29
*
* 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.
*
* Instructions:
* - Copy this app into your IDE using the "From Code" Option in the IDE and publish it
* - If you have a bloomsky device installed currently, remove it and remove the DTH code from your account.
* - If this is your first time installing a bloomsky device you can skip this step completely.
* - Copy the new DTH code into your account and publish it (https://github.com/tslagle13/SmartThingsPersonal/blob/master/devicetypes/tslagle13/bloomsky.src/bloomsky.groovy)
* - Install the "BloomSky (Connect)" app through the SmartThings Mobile App
**/
import groovy.json.*
definition(
name: "BloomSky (Connect)",
namespace: "tslagle13",
author: "Tim Slagle",
description: "Used to spin up device types for BloomSky weather stations. ",
category: "My Apps",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
singleInstance: true)
preferences {
page(name: "setupPage")
}
def getVersion() {
return "1.0"
}
def setupPage() {
dynamicPage(name: "setupPage", install: true, uninstall: true) {
section("BloomSky API Settings") {
input "apiKey", "password", title: "API Key", Required: true
}
section("Options:", hideable: true, hidden: false) {
input(name: "refreshTime", title: "Refresh Time (Minutes: 1 - 60)", type: "number", range: "01..60", defaultValue: 10, required: true)
input(name: "detailDebug", type:"bool", title: "Enable Debug logs", defaultValue: false, submitOnChange: true)
paragraph "Give a name to this SmartApp (Optional)"
input
label(title: "Assign a name", required: false)
}
section ("Version " + "${getVersion()}") { }
}
}
def installed() {
log.info "--- Installed with settings: ${settings}"
initialize()
}
def updated() {
log.info "--- Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
log.info "--- BloomSky (Connect) - Version: ${getVersion()}"
getBloomskyIds()
// Schedule Refresh
int refreshMin = settings.refreshTime ? settings.refreshTime : 10
String refreshSchedule = "0 0/" + refreshMin.toString() + " * 1/1 * ? *"
schedule(refreshSchedule, "refreshBloomsky")
}
// use API key to find device IDs
def getBloomskyIds(evt) {
if (settings.detailDebug) log.debug "Getting BloomSky Devices..."
def pollParams = [
uri: "https://api.bloomsky.com",
path: "/api/skydata/",
requestContentType: "application/json",
requestContentType: "application/x-www-form-urlencoded",
headers: ["Authorization": apiKey]
]
try {
httpGet(pollParams) { resp ->
//log.debug resp.getData().collectNested{it.DeviceID}
state.deviceCollection = resp.getData().collect{it.DeviceID} //get all device IDs on bloomsky account
}
} catch (Exception e) {
log.debug "Error: $e"
}
createBloomskyDevices()
}
def createBloomskyDevices() {
if (settings.detailDebug) log.debug "Creating BloomSky Devices..."
if (state.deviceCollection) {
state.deviceCollection.each{
def childDevices = getChildDevices()
//if child device doens't exist, create it
if (!(childDevices.name).toString(/*convert to string for testing*/).contains("${it}")) {
addChildDevice("tslagle13", "Bloomsky", it, null, [label:"Bloomsky " + it, name:"${it}"]) //create child device with name and label so name remains protected
log.info "Created Child Device - Bloomsky ${it}"
}
//if child device does exist log that it does
else if ((childDevices.name).toString(/*convert to string for testing*/).contains("${it}")) {
log.info "Child device - Bloomsky ${it} already exists"
}
}
removeOldDevices(getChildDevices().name - state.deviceCollection) //find child devices that exist in ST that do not exist in the bloomsky account.
}
refreshBloomsky()
}
def removeOldDevices(devices) {
if (settings.detailDebug) log.debug "Removing Old BloomSky Devices..."
devices.each {
def device = getChildDevice("${it}") //find device ID with DNI of non bloomsky device
deleteChildDevice(device.deviceNetworkId)
log.debug "Removed Child Device '${device.name}' because it no longer exists in your bloomsky account."
}
}
//provide API Key to child devices in more secure manner
private getAPIKey() {
def key = apiKey as String
return key
}
//refresh for child devices
def refreshBloomsky(evt) {
log.info "--- Refresh Devices"
state.lastTime = now()
def devices = getChildDevices()
devices.each{
if (settings.detailDebug) log.debug "Calling Refresh on BloomSky device: ${it.id}"
it.callAPI()
}
}
Device Type:
/**
* Device: Bloomsky
*
* This DTH requires the BloomSky (Connect) app (https://github.com/tslagle13/SmartThingsPersonal/blob/master/smartapps/tslagle13/bloomsky-connect.src/bloomsky-connect.groovy)
*
* Copyright 2016 Tim Slagle
*
* Modified to work with Hubitat by cuboy29
*
* 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.
*
*/
def getVersion() { return "2.1.9"}
metadata {
definition (name: "Bloomsky", namespace: "tslagle13", author: "Tim Slagle") {
capability "Battery"
capability "Illuminance Measurement"
capability "Refresh"
capability "Polling"
capability "Relative Humidity Measurement"
capability "Sensor"
capability "Temperature Measurement"
capability "Ultraviolet Index"
capability "Water Sensor"
attribute "pressure", "number"
attribute "pressureUnit", "string"
attribute "lastUpdated", "string"
attribute "deviceMode", "string"
// STORM data
attribute "rainRate", "number"
attribute "rainDaily", "number"
attribute "rain24h", "number"
attribute "windSpeed", "number"
attribute "windDirection", "string"
attribute "windGust", "number"
attribute "imageURL", "string"
}
preferences {
input "pressureMbar", "boolean", title: "Pressure in Mbar? (Default = inHg)", displayDuringSetup:false, defaultValue:false
input "enableStorm", "boolean", title: "Enable STORM data?", displayDuringSetup:false, defaultValue:false
input "windspeedKph", "boolean", title: "Wind Speed in Kph? (Default = Mph)", displayDuringSetup:false, defaultValue:false
input "rainMm", "boolean", title: "Rain in mm? (Default = inches)", displayDuringSetup:false, defaultValue:false
input "detailDebug", "boolean", title: "Enable Debug logging?", displayDuringSetup:false, defaultValue:true
}
}
def getTempColors() {
def colorMap = []
colorMap = [
[value: 0, color: "#a39393"],
[value: 4, color: "#c56fc5"],
[value: 5, color: "#800f87"],
[value: 20, color: "#0e47e3"],
[value: 35, color: "#1e9cbb"],
[value: 50, color: "#90d2a7"],
[value: 65, color: "#f1d801"],
[value: 80, color: "#ffa500"],
[value: 95, color: "#d04e00"],
[value: 110, color: "#bc2323"]
]
return colorMap
}
def getTempColors2() {
def colorMap = []
if (state?.tempMetric) {
colorMap = [
// Celsius Color Range
[value: -23, color: "#a39393"],
[value: -15, color: "#c56fc5"],
[value: -7, color: "#800f87"],
[value: 1, color: "#0e47e3"],
[value: 2, color: "#1e9cbb"],
[value: 10, color: "#90d2a7"],
[value: 18, color: "#f1d801"],
[value: 27, color: "#ffa500"],
[value: 35, color: "#d04e00"],
[value: 43, color: "#bc2323"]
]
} else {
colorMap = [
// Fahrenheit Color Range
[value: 32, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 92, color: "#d04e00"],
[value: 98, color: "#bc2323"]
]
}
return colorMap
}
def getPressureColors() {
def colorMap = []
if ("true" == pressureMbar) {
colorMap = [
// mbar Color Range
[value: 0, color: "#1e9cbb"],
[value: 1009, color: "#289500"],
[value: 1022, color: "#ffa500"]
]
} else {
colorMap = [
// inHg Color Range
[value: 0, color: "#1e9cbb"],
[value: 29.80, color: "#289500"],
[value: 30.20, color: "#ffa500"]
]
}
return colorMap
}
def installed() {
log.info "--- BloomSky - Version: ${getVersion()}"
log.info "--- Device Created"
state.debug = ("true" == detailDebug)
refresh()
}
def updated() {
log.info "--- BloomSky - Version: ${getVersion()}"
log.info "--- Device Config Updated"
state.debug = ("true" == detailDebug)
// Check if STORM is enabled
if (!("true" == enableStorm)) {
sendEvent(name:"rainRate", value: 0.0, displayed:false)
sendEvent(name:"rainDaily", value: 0.0, displayed:false)
sendEvent(name:"rain24h", value: 0.0, displayed:false)
sendEvent(name:"windSpeed", value: 0.0, displayed:false)
sendEvent(name:"windDirection", value: "N", displayed:false)
sendEvent(name:"windGust", value: 0.0, displayed:false)
}
refresh()
}
def poll() {
state.debug = ("true" == detailDebug)
log.info("--- Device Poll")
callAPI()
}
def refresh() {
state.debug = ("true" == detailDebug)
callAPI()
}
//call Bloomsky API and update device
private def callAPI() {
log.info "--- Refreshing Bloomsky Device: ${device.label}"
def data = [:]
try {
tempUnitEvent(getTemperatureScale())
httpGet([
//uri: "https://thirdpartyapi.appspot.com",
uri: "https://api.bloomsky.com",
path: "/api/skydata/",
requestContentType: "application/json",
headers: ["Authorization": parent.getAPIKey()]
]) { resp ->
// Get the Device Info of the Correct Bloom Sky
def individualBloomSky = []
// If you don't have Device ID specific get the first bloom sky only
if(device.deviceNetworkId) {
individualBloomSky = resp.getData().findAll{ it.DeviceID == device.deviceNetworkId }
if (state.debug) log.debug "Found BloomSky ID: ${device.deviceNetworkId}"
}
else {
individualBloomSky = resp.data[0]
if (state.debug) log.debug "Using BloomSky ID: ${individualBloomSky.DeviceID}"
}
// Initialize data fields for both (Sky & Storm)
def uvindex = 0
// Bloomsky (SKY1/SKY2) data
data << individualBloomSky.Data
if (data) {
if (state.debug) log.debug "--- Getting Bloomsky (SKY1/SKY2) data"
//itterate through hashmap pairs to update device. Used case statement because it was twice as fast.
data.each {oldkey, datum->
def key = oldkey.toLowerCase() //bloomsky returns camel cased keys. Put to lowercase so dth can update correctly
if (state.debug) log.debug "${key}:${datum}"
switch(key) {
case "voltage": //update "battery"
sendEvent(name:"battery", value: getBattery(datum), unit: "%")
break;
case "ts": //update last update from bloomsky
def date = (datum * 1000L)
def df = new java.text.SimpleDateFormat("MMM dd hh:mm a")
df.setTimeZone(location.timeZone)
def lastUpdated = df.format(date)
sendEvent(name: "lastUpdated", value: lastUpdated)
break;
case "rain": //check if it is raining or not
if (datum == true) {
sendEvent(name: "water", value: "wet")
} else {
sendEvent(name: "water", value: "dry")
}
break;
case "luminance": //update illuminance
sendEvent(name: "illuminance", value: datum, unit: "Lux")
break;
case "uvindex": //bloomsky does UV index! how cool is that!?
uvindex = datum.toInteger()
break;
case "pressure":
def presValue = datum.toDouble().trunc(2)
def presUnit = "inHg"
if (("true" == pressureMbar)) {
//presValue = (presValue * 33.8639).round(0).toInteger()
presValue = (presValue * 33.8639).trunc(1)
presUnit = "mbar"
} else {
presValue = presValue.trunc(2)
}
sendEvent(name:"${key}", value: presValue, unit: presUnit)
sendEvent(name:"pressureUnit", value: presUnit, displayed:false)
break;
case "humidity":
sendEvent(name: "${key}", value: datum, unit: "%")
break;
case "temperature": //temperature needs to be converted to celcius in some cases so we break it out on its own
def temp = getTemperature(datum).toDouble().trunc(1)
sendEvent(name:"${key}", value: temp, unit: tempScale())
break;
case "night": // Mode = Day or Night
def mode = "Day"
if (datum == true) {
mode = "Night"
}
sendEvent(name:"deviceMode", value: mode)
break;
case "devicetype": // Device Type: SKY1 or SKY2
sendEvent(name:"deviceType", value: datum)
break;
case "imageurl": //lastly update the image from bloomsky to the DTH
def imageURL = "${key}:${datum}"
if (imageURL.contains("http")){
imageURL = imageURL.getAt(9..imageURL.length()-1)
log.debug "YOUR URL " + imageURL
test = "<img src='" + imageURL + "' style='width:400px'/>"
log.debug test
sendEvent(name: "imageURL", value: test)
}
break;
default:
if (state.debug) log.debug "${key} is not being used"
break;
}
}
}
data.clear()
// Bloomsky (STORM) data
data << individualBloomSky.Storm
def rainRate = 0.0
def rainDaily = 0.0
def rain24h = 0.0
def windSpeed = 0.0
def windDirection = "N"
def windGust = 0.0
def msgStorm = "No Wind data"
def msgStormRain = "No Rain data"
def rainUnit = "in"
if (("true" == rainMm)) {
rainUnit = "mm"
}
def windSpeedUnit = "mph"
if (("true" == windspeedKph)) {
windSpeedUnit = "kph"
}
if (data && ("true" == enableStorm)) {
if (state.debug) log.debug "--- Getting Bloomsky (STORM) data"
data.each {oldkey, datum->
def key = oldkey.toLowerCase() //bloomsky returns camel cased keys. Put to lowercase so dth can update correctly
if (state.debug) log.debug "${key}:${datum}"
switch(key) {
case "uvindex":
uvindex = datum.toInteger()
break;
case "raindaily":
if (datum.toDouble() < 9000) {
rainDaily = datum.toDouble()
if (("true" == rainMm)) {
rainDaily = rainDaily.trunc(1)
} else {
rainDaily = (rainDaily * 0.039370).round(1).trunc(1)
}
}
sendEvent(name:"rainDaily", value: rainDaily, unit: rainUnit)
break;
case "24hrain":
if (datum.toDouble() < 9000) {
rain24h = datum.toDouble()
if (("true" == rainMm)) {
rain24h = rain24h.trunc(1)
} else {
rain24h = (rain24h * 0.039370).round(1).trunc(1)
}
}
sendEvent(name:"rain24h", value: rain24h, unit: rainUnit)
break;
case "rainrate":
if (datum.toDouble() < 9000) {
rainRate = datum.toDouble()
if (("true" == rainMm)) {
rainRate = rainRate.trunc(1)
} else {
rainRate = (rainRate * 0.039370).round(1).trunc(1)
}
}
sendEvent(name:"rainRate", value: rainRate, unit: rainUnit+"/h")
break;
case "sustainedwindspeed":
if (datum.toDouble() < 9000) {
windSpeed = datum.toDouble()
if (("true" == windspeedKph)) {
windSpeed = (windSpeed * 1.609344).round(1).trunc(1)
} else {
windSpeed = windSpeed.trunc(1)
}
}
sendEvent(name:"windSpeed", value: windSpeed, unit: windSpeedUnit)
break;
case "winddirection":
if (datum instanceof String) {
windDirection = datum.toString()
}
sendEvent(name:"windDirection", value: windDirection)
break;
case "windgust":
if (datum.toDouble() < 9000) {
windGust = datum.toDouble()
if (("true" == windspeedKph)) {
windGust = (windGust * 1.609344).round(1).trunc(1)
} else {
windGust = windGust.trunc(1)
}
}
sendEvent(name:"windGust", value: windGust, unit: windSpeedUnit)
break;
default:
if (state.debug) log.debug "${key} is not being used"
break;
}
}
//--- Create STORM data message
msgStorm = "Wind: " + windSpeed.toString() + " " + windSpeedUnit + " / " + windDirection + " - Gusts: " + windGust.toString() + " " + windSpeedUnit
msgStormRain = "Rain Rate: " + rainRate.toString() + " " + rainUnit + "/h - Daily: " + rainDaily.toString() + " " + rainUnit + " - Last 24h: " + rain24h.toString() + " " + rainUnit
} else {
if (state.debug) log.debug "--- STORM data Disabled"
}
// Send Events for both Sky & Storm
sendEvent(name: "ultravioletIndex", value: uvindex)
sendEvent(name: "msgStorm", value: msgStorm, displayed:false)
sendEvent(name: "msgStormRain", value: msgStormRain, displayed:false)
}
}
//log exception gracefully
catch (Exception e) {
log.debug "Error - callAPI(): $e"
}
data.clear()
}
//convert temp to celcius if needed
def getTemperature(value) {
def cmdScale = getTemperatureScale()
return convertTemperatureIfNeeded(value.toFloat(), "F", 4)
}
//create unique name for picture storage
private getPictureName() {
def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '')
"image" + "_$pictureUuid" + ".jpg"
}
def getBattery(v) {
def result = 0
def miliVolts = v
// Minimum: 2460 - Maximum: 2620
def minVolts = 2460
def maxVolts = 2620
if (miliVolts >= minVolts) {
if (miliVolts > maxVolts) {
result = 100
} else {
def pct = (miliVolts - minVolts) / (maxVolts - minVolts)
result = Math.min(100, (int) pct * 100)
}
}
return result
}
def tempUnitEvent(unit) {
state.tempUnit = unit
state.tempMetric = (unit == "C")
if (state.debug) log.debug "Temp Unit: ${state?.tempUnit} - Metric: ${state?.tempMetric}"
}
def tempScale() {
def tempScaleUnit = state?.tempUnit
return tempScaleUnit
}