Get local UV radiation -- prevent sunburn and skin cancer

,

Get local UV radiation information into openHAB

Update: The API used in this topic has been deprecated. I will update the example soon.

Introduction

UV Radiation leads to sun burn and is the largest cause of skin cancer.

OpenWeatherMap only provides UV information in their API for their expensive paid accounts. Wunderground provides UV field in the API, but I have not yet found a weather station that actually returns any data. Searching the web for another API lead me to the UV Index API that has been developed by Australian Alex Ershov for use with his UVIMate Android app

Pre-requisites

  • Working openHAB 2 installation (the solution should work with 1.x as well, but is not tested yet)
  • Legacy 1.x HTTP binding installed (for caching)
  • Facebook account (to register for the API)
  • Key for the UV Index API
  • Latitude and longitude for your home. Use the GPS Coordinates site if you don’t know them already.

General description

The UV Index API is called every 10 minutes for updates. The resulting JSON message is parsed by a rule to populate required items.

The sunburn times differ based on skin type, according to the international Fitzpatrick scale. The time is calculated in minutes, but is displayed in a nicely formatted text through a Javascript transformation.

Note: Due to the JavaScripts the sitemap section takes 5-10 seconds to load and show the section on a raspberry Pi 2B. If you don’t like this, you can always simply show the plain value (see the items file for an example at skin type VI).

Although you could use direct JSON transformations for individual items, I have decided to only get the raw JSON and parse that using a dedicated rule. This rule is needed for more complex conversion anyway, so now all logic is in one place. Big thanks to @rlkoshak for helping me out with parsing NULL values.

This solution can be used with or without the HTTP binding. With the binding, you can cache content and directly access the API data for multiple items without hammering/abusing the API. I have set up the binding to only allow calls every 10 minutes.

Instructions

Step 1: Setup HTTP binding

Add a new section to your http.cfg file in the configuration services folder.
Replace XXX with your personal API token.
Replace latitude and longitude with your home location.

http.cfg

# cache for 10 minutes (= 600000 milliseconds)
uvimateCache.url=https://uvimate.herokuapp.com/api/getUVI/51.9000/4.6000{x-access-token=XXX}
uvimateCache.updateInterval=600000

Step 2. Setup your items

Create uvi.items in the items configuration folder

uvi.items

Group Uvi "UV Index"

// get raw json every 10 minutes
// use http caching to prevent hitting the API too much :-)
String   Uvi_Raw     (Uvi) { http="<[uvimateCache:600000:REGEX((.*))]" }

// Other items are populated using a rule

// current index

Number   Uvi_Uvi         "Current UV Index [%.2f]"            <line>  (Uvi, Chart)
Number   Uvi_UviMax      "Expected max UV Index today [%.2f]" <line>  (Uvi)
DateTime Uvi_UviMaxTime  "Max UV Index Time [%1$tH:%1$tM]"    <clock> (Uvi)

// burn times

Number   Uvi_BurnTimeCeltic        "Sunburn (Type I)   [JS(duration.js):%s]" <woman_1> (Uvi)
Number   Uvi_BurnTimePale          "Sunburn (Type II)  [JS(duration.js):%s]" <woman_2> (Uvi)
Number   Uvi_BurnTimeCaucasian     "Sunburn (Type III) [JS(duration.js):%s]" <woman_3> (Uvi)
Number   Uvi_BurnTimeMediterranean "Sunburn (Type IV)  [JS(duration.js):%s]" <woman_4> (Uvi)
Number   Uvi_BurnTimeSouthAfrican  "Sunburn (Type V)   [JS(duration.js):%s]" <woman_5> (Uvi)
Number   Uvi_BurnTimeNegro         "Sunburn (Type VI)  [%d mins]" <woman_6> (Uvi)

// location information

String   Uvi_Location    "Location [%s]" <text> (Uvi)
DateTime Uvi_Observation "Observation [%1$tH:%1$tM]" <clock> (Uvi)
DateTime Uvi_LastUpdate "Last update [%1$tH:%1$tM]" <clock> (Uvi)

Step 3: Create javascript transformation

Create a file duration.js in your configuration transform folder

duration.js

// computes nicely formatted duration from given minutes

// change these values for your own language
var minute   = "minute"
var minutes  = "minutes"
var hour     = "hour"
var hours    = "hours"
var day      = "day"
var days     = "days"

(function(i){ 

	var d = Math.floor(i / (24 * 60));
	var h = Math.floor((i / 60) - (24 * d));
	var m = Math.round(i - 60 * (24 * d + h));
	var result = '';

	// days
	if (d > 0) { 
		result = result + d;
		if (d == 1) {
			result = d + ' ' + day;
		} else {
			result = d + ' ' + days;
		}
	}

	// hours
	if (h > 0) {
		if (result != '') {
			result = result + ', ';
		}
		result = result + h;
		if (h == 1) {
			result = result + ' ' + hour;
		} else {
			result = result + ' ' + hours;
		}
	}
	
	// minutes
	if (m > 0) {
		if (result != '') {
			result = result + ', ';
		}
		result = result + m;
		if (m == 1) {
			result = result + ' ' + minute;
		} else {
			result = result + ' ' + minutes;
		}
	}

	return result;
})(input)

Step 4: Create rule to parse JSON

Create uvi.rules file in your configuration rules folder.

uvi.rules

// logger title
val logger= "rules.uvi"

/*
To debug, set logging in karaf console:
log:set DEBUG org.eclipse.smarthome.model.script.rules.uvi
*/

/*
The HTTP binding is used to obtain raw JSON from the Uvimate API.
The individual items however are populated using this rule.
Some items could be easily obtained through a simple JSONPATH translation in the items file.
Other items need specific translation, for example a conversion from zulu to local time
or handling a 'null' value for a number.
*/
rule UvimateUpdated
when
	Item Uvi_Raw received update
then
	logDebug(logger, "Uvimate received update")

	// asap
	Uvi_LastUpdate.postUpdate(new DateTimeType)
	
	// get values
	val raw         = Uvi_Raw.state.toString
	val observation = new DateTimeType(transform("JSONPATH", "$.result.date", raw))
	val location    = transform("JSONPATH", "$.result.location", raw)
	val uvi         = transform("JSONPATH", "$.result.uviData.uvi", raw)
	val uviMax      = transform("JSONPATH", "$.result.uviData.uviMax", raw)
	val uviMaxTime  = new DateTimeType(transform("JSONPATH", "$.result.uviData.uviMaxTime", raw))
	val celtic      = transform("JSONPATH", "$.result.burnTime.celtic", raw)
	val pale        = transform("JSONPATH", "$.result.burnTime.pale", raw)
	val caucasian   = transform("JSONPATH", "$.result.burnTime.caucasian", raw)
	val mediterranean = transform("JSONPATH", "$.result.burnTime.mediterranean", raw)
	val southAfrican = transform("JSONPATH", "$.result.burnTime.southAfrican", raw)
	val negro       = transform("JSONPATH", "$.result.burnTime.negro", raw)

	// debug
	logDebug(logger, "raw           : " + raw)
	logDebug(logger, "observation   : " + observation)
	logDebug(logger, "location      : " + location)

	logDebug(logger, "uvi           : " + uvi)
	logDebug(logger, "uviMax        : " + uviMax)
	logDebug(logger, "uviMaxTime    : " + uviMaxTime)

	logDebug(logger, "celtic        : " + celtic)
	logDebug(logger, "pale          : " + pale)
	logDebug(logger, "caucasian     : " + caucasian)
	logDebug(logger, "mediterranean : " + mediterranean)
	logDebug(logger, "southAfrican  : " + southAfrican)
	logDebug(logger, "negro         : " + negro)

	// post updates

	Uvi_Observation.postUpdate(observation)
	Uvi_Location.postUpdate(location)
	Uvi_Uvi.postUpdate(uvi)	
	Uvi_UviMax.postUpdate(uviMax)	
	Uvi_UviMaxTime.postUpdate(uviMaxTime)

	// oneliners with null will fail

	if (celtic==null) Uvi_BurnTimeCeltic.postUpdate(NULL)    
		else Uvi_BurnTimeCeltic.postUpdate(celtic)    

	if (pale==null) Uvi_BurnTimePale.postUpdate(NULL)       
		else Uvi_BurnTimePale.postUpdate(pale)       

	if (caucasian==null) Uvi_BurnTimeCaucasian.postUpdate(NULL)
		else Uvi_BurnTimeCaucasian.postUpdate(caucasian)

	if (mediterranean==null) Uvi_BurnTimeMediterranean.postUpdate(NULL)
		else Uvi_BurnTimeMediterranean.postUpdate(mediterranean)

	if (southAfrican==null) Uvi_BurnTimeSouthAfrican.postUpdate(NULL)
		else Uvi_BurnTimeSouthAfrican.postUpdate(southAfrican)

	if (negro==null) Uvi_BurnTimeNegro.postUpdate(NULL)
		else Uvi_BurnTimeNegro.postUpdate(negro)
	
end

Step 5: Create sitemap

Create file uvi.sitemap in your configuration sitemaps folder. Valuecolors are given in hex values, since the iOS app does not (yet) supported color names.

uvi.sitemap

sitemap uvi label="UV Index" {

	Frame item=Uvi {
		Text item=Uvi_Uvi valuecolor=[
				Uvi_Uvi=="NULL"="#D3D3D3", // lightgray
				<3="#006400", // darkgreen
				<6="#FFD700", // gold
				<8="#FF8C00", // darkorange
				<11="#DC143C", // crimson (red)
				>=11="#4B0082" ] // indigo
		Text item=Uvi_UviMax
		Text item=Uvi_UviMaxTime
		Text item=Uvi_BurnTimeCeltic       
		Text item=Uvi_BurnTimePale         
		Text item=Uvi_BurnTimeCaucasian    
		Text item=Uvi_BurnTimeMediterranean
		Text item=Uvi_BurnTimeSouthAfrican 
		Text item=Uvi_BurnTimeNegro        
		Text item=Uvi_Location
		Text item=Uvi_Observation
		Text item=Uvi_LastUpdate
	}
}

Update 2017-02-15: Removed forecasts. Added skin type icons for burn time items. Added value colors for index in sitemap.

10 Likes

Robert, that’s a great piece of work! Congratulations!

I’m gonna test this soon.
Shame that one needs a facebook account to get the API key, but fortunately enough you can limit number of personal information shared with the app.

Seems that their server is down now, because it won’t let me register an account :wink:

Application error
An error occurred in the application and your page could not be served. If you are the application owner, check your logs for details.
1 Like

Perhaps subscribing won’t work but the API is still up. Last observation was a few minutes ago.

Let me know if this is not solved in some time; I have the email address of the developer.

Loading the JavaScript transformations takes 5-10 seconds to load (part of) the sitemap on my Raspberry Pi model 2B. I have reverted to showing minutes. Added a note in the OP.

@rtvb I still get Application Error unfortunately.
Tried on two machines, with and without VPN, with and without uBlock in my browser.

Application error
An error occurred in the application and your page could not be served. If you are the application owner, check your logs for details.

Then when I refresh the page, i see the message from Facebook Auth:

{
error: "FacebookTokenError: This authorization code has been used.
 at Strategy.parseErrorResponse (/app/node_modules/passport-facebook/lib/strategy.js:196:12) 
 at Strategy.OAuth2Strategy._createOAuthError (/app/node_modules/passport-oauth2/lib/strategy.js:367:16) 
 at /app/node_modules/passport-oauth2/lib/strategy.js:166:45
 at /app/node_modules/oauth/lib/oauth2.js:177:18 at passBackControl (/app/node_modules/oauth/lib/oauth2.js:123:9)
 at IncomingMessage.<anonymous> (/app/node_modules/oauth/lib/oauth2.js:143:7)
 at emitNone (events.js:91:20)
 at IncomingMessage.emit (events.js:185:7)
 at endReadableNT (_stream_readable.js:974:12)
 at _combinedTickCallback (internal/process/next_tick.js:74:11)
 at process._tickCallback (internal/process/next_tick.js:98:9)"
}

That is strange. I will PM you the email adres of the author.

Is the service still alive?
Since a few days I receive in my logs:

2017-02-13 13:50:21.503 [ERROR] [.script.engine.ScriptExecutionThread] - Rule ‘UvimateUpdated’: The argument ‘state’ must not be null or empty.

I don’t have any issues so far. The API was last updated/called max 10 minutes ago.

Thats the airquality binding, not the uvi http request

:blush:

Mine is not working either. I will look into this.

I logged in to the developer page

That page returns results, so the API itself seems to be working. I can see however that the forecast JSON is empty. Will look deeper into it tomorrow.

Yep. The API has changed a little. The array of forecasts is now empty. I don’t know if this is permanent or if it is a bug. For now if you comment out the postUpdate for each of the forecast items, the rule will run without issues.

I will contact the API developer and ask what’s wrong. After that I will update the tutorial if needed.

Thanks @rtvb

The author replied to my email:

Hi Robert! First of all thank yoy for the amazing article how to integrate the API into your system. The forecast array was cleaned up from values intentionally cause of perfomance issues. I have a plan to transfer that data to a distinct method when I will have some time, hpefully in the near future (I am primary focused on iOS version of the UVIMate app now). On the other hand I will pobably implement a daily forecast instead of long term one. So for now that response parameter has to be avoided for consumption.

Cheers.
Alex.

So for now the forecast is deprecated. When I have time the coming days I will update the tutorial.

1 Like

I have updated the tutorial:

  • Removed forecasts. They are no longer supported in the API.
  • Added skin type icons for burn time items. This gives a visual clue.
  • Added value colors for index in sitemap. The color matches the international standard and hints you about the level of risk.

TODO: Notification when value updates to above or below a specific level.

2 Likes

Hi @rtvb ,

I’m not sure if you or anyone else is getting the same problem, but when I install/run this, My Control tab in PaperUI stops working. I’ve verified this to be the cause through removal and reinstallation.

Any ideas what could be causing this?

I’ve checked my logs but I get a flood of information regarding transformations.

Game over with this. From the website

Hi guys! Unfortunatelly since 1.05.2017 the public access to the UV Index API has been closed. We are currently working on billing platform for the API that will be available further this year. Please contact me if you want to continue the API usage: aershov24@gmail.com. Cheers. Alex.

:cry: :cry: :cry: :cry: :cry: :cry: :cry: :cry:

We’ll try to find another platform that provides an API with UVI radiotion information.

If any of you found one I’m happy to update my example solution.

1 Like

Is offering an API to the free subscritpion. the API also provides air quality data

And here I was wondering why my UV-index field was not filled :slight_smile:
Great work. Will try this soon with a working service.
OpenWeathermap is not the answer I am afraid:

Slight update, suddenly Weatherunderground started to send UVI data. Great