Bloomsky Integration

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
}
2 Likes

Thank you for porting this over. Glad to have my BloomSky back in action!

I'm trying to pull my image file into the dashboard and not having any luck? I'm not getting the right url?

How are you adding it to the dashboard. You must use the attribute tile template.

Here is mine with a radar map. I've added for fun :slight_smile:

07%20AM

1 Like

@cuboy29 Thanks for the info. i was trying to pull image in by using an I cap instead of a small i. Thanks so much for your help and transferring the app to Hubitat.

Hi.. long time ST user and new to Hubitat so please forgive me for newbie questions. I can't seem to get the Bloomsky port to work. I coped and pasted the app code into a new app in App Code. I then pasted the "Device Type" code into a new driver in "Drivers Code". I then created a new Virtual Device of type Bloomsky and finally went into the App and added my API key. I never see the Bloomsky device show up as an option in devices when setting up a dashboard. Am I missing something simple?

You don't have to manually create the virtual device. The app will create it for you.

Thanks... I went back and removed the virtual device, removed the app and loaded app again. I don't see the device showing up in the list. Is there something I need to do to trigger the app to create the device?

Disregard. I had a typo on the code copy/paste. I had modified my "namespace" in the code I copied over from ST and missed the section further down in the code where namepace needed to be changed as well. Appears to be working now. Thanks!

1 Like

Added air quality map for fun

Thanks for doing this, however how do you actually add the Bloomsky to Hubitat? Very confused after reading through this thread. Does it generate the device automatically or do you have to do something to get it to create the device. I added both the app and the device driver but now I'm stuck.

Any help would be much appreciated.

The connect app wil create the and install the device. If you have both installed, then load the connect app and enter your bloomsky api key and it should create it for you.

Thanks, that worked perfectly!

Guys,
Iā€™m looking for some info on Bloomsky
I love the idea of a cam to show the amont of cloud etc and am on the verge of buying one.
Unfortunately in the UK we pay the same as you guys in the US but they just change the currency symbol
So for me it will be Ā£300 - a bit pricey but worth it for the fun factor!

So my question...
Apart from the cam looking up, what do I get for my money?
What about other services?
Can I see radar maps or air quality maps for outside the US?

I have a pws already that gets me all the usual stuff (temp, wind, humidity etc) so what else can I get?

Andy

The radar map and air quality map are something that I added to the bloomsky driver. It's basically grab the URL for those from some website and set an attribute = to the URL. As far as the Bloomsky, I have the orginal unit which doesn't do much other than the nice pictures.

1 Like

@cuboy29, thanks for putting this together.

Is there a way this can be used to create a device for BloomSky that you follow?

I don't own one currently, but follow a couple and pull the data in using IFTTT. Looking to connect directly to HE without going through IFTTT.

I don't see why not. Go for it. If you have questions, post it here and this great community will help out.

If I'm not mistaken Andy Bloomsky can also detect rain but not the amount.

From their website..........

The updated 5 in 1 weather camera station accurately measures Temperature, Humidity, Barometric Pressure and Precipitation.

1 Like

Personally I can't justify $300 just for a camera. I have a Ambient WS that cost $150. I can buy a IP camera between $50 and $75.

A lot of us got bloomskys for free. They were giving them away for $20 shipping once upon a time. that is how I got mine.