Sean
Published © GPL3+

Cloud Height - Get Cloud Info Using Alexa and a METAR API

Cloud Height is an Amazon Alexa skill that can get cloud cover and altitude information using a METAR API and the postcode of the device.

IntermediateFull instructions provided2 hours242
Cloud Height - Get Cloud Info Using Alexa and a METAR API

Things used in this project

Software apps and online services

AWS Lambda
Amazon Web Services AWS Lambda
Used for hosting the skill and handling requests to/from the Alexa device. The METAR API is also called from Lambda.
Alexa Skills Kit
Amazon Alexa Alexa Skills Kit
The backbone of the project, the ASK allows us to easily use Alexa compatible devices as the interface for our project.
National Weather Service ADDS Dataserver
The ADDS Dataserver allows us to request METAR information using Latitude and Longitude inputs.

Story

Read more

Schematics

User-Skill Interaction Diagram

This diagram shows all the possible interactions between a User and the Skill.

Code

index.js

JavaScript
This file is the main component of the skill. When called by the Alexa Skills Kit, this Lambda function will make a request to the National Weather Service ADDS Dataserver using the Latitude and Longitude of the postcode specified in the Alexa App. For more info and usage instructions, read the Story section of the project.
/**
 * An Amazon Alexa skill that returns nearby cloud height.
 * It is based on the sample code for the Amazon Alexa Space Facts example
 * provided by Amazon, at
 *    https://github.com/alexa/skill-sample-nodejs-fact
 * with unnecessary features removed for simplicity. The original example
 * code has been largely removed leaving the Alexa framework and the
 * Cloud Height code built on top of it.
 * 
 * Copyright (c) 2017 Sean Sheedy, Tommy Sheedy under Amazon Software License
 **/

/*
Amazon Software License
1. Definitions
Licensor means any person or entity that distributes its Work.

Software means the original work of authorship made available under this 
License.

Work means the Software and any additions to or derivative works of the 
Software that are made available under this License.

The terms reproduce, reproduction, derivative works, and distribution 
have the meaning as provided under U.S. copyright law; provided, however, that 
for the purposes of this License, derivative works shall not include works that 
remain separable from, or merely link (or bind by name) to the interfaces of, 
the Work.

Works, including the Software, are made available under this License by 
including in or with the Work either (a) a copyright notice referencing the 
applicability of this License to the Work, or (b) a copy of this License.
2. License Grants
2.1 Copyright Grant. Subject to the terms and conditions of this License, each 
Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free, 
copyright license to reproduce, prepare derivative works of, publicly display, 
publicly perform, sublicense and distribute its Work and any resulting 
derivative works in any form.
2.2 Patent Grant. Subject to the terms and conditions of this License, each 
Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free 
patent license to make, have made, use, sell, offer for sale, import, and 
otherwise transfer its Work, in whole or in part. The foregoing license applies 
only to the patent claims licensable by Licensor that would be infringed by 
Licensors Work (or portion thereof) individually and excluding any 
combinations with any other materials or technology.
3. Limitations
3.1 Redistribution. You may reproduce or distribute the Work only if (a) you do 
so under this License, (b) you include a complete copy of this License with 
your distribution, and (c) you retain without modification any copyright, 
patent, trademark, or attribution notices that are present in the Work.
3.2 Derivative Works. You may specify that additional or different terms apply 
to the use, reproduction, and distribution of your derivative works of the Work 
(Your Terms) only if (a) Your Terms provide that the use limitation in 
Section 3.3 applies to your derivative works, and (b) you identify the specific 
derivative works that are subject to Your Terms. Notwithstanding Your Terms, 
this License (including the redistribution requirements in Section 3.1) will 
continue to apply to the Work itself.
3.3 Use Limitation. The Work and any derivative works thereof only may be used 
or intended for use with the web services, computing platforms or applications 
provided by Amazon.com, Inc. or its affiliates, including Amazon Web Services, 
Inc.
3.4 Patent Claims. If you bring or threaten to bring a patent claim against any 
Licensor (including any claim, cross-claim or counterclaim in a lawsuit) to 
enforce any patents that you allege are infringed by any Work, then your rights 
under this License from such Licensor (including the grants in Sections 2.1 and 
2.2) will terminate immediately.
3.5 Trademarks. This License does not grant any rights to use any Licensors or 
its affiliates names, logos, or trademarks, except as necessary to reproduce 
the notices described in this License.
3.6 Termination. If you violate any term of this License, then your rights 
under this License (including the grants in Sections 2.1 and 2.2) will 
terminate immediately.
4. Disclaimer of Warranty.
THE WORK IS PROVIDED AS IS WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 
EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF 
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT. 
YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER THIS LICENSE. SOME 
STATES CONSUMER LAWS DO NOT ALLOW EXCLUSION OF AN IMPLIED WARRANTY, SO THIS 
DISCLAIMER MAY NOT APPLY TO YOU.
5. Limitation of Liability.
EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL THEORY, 
WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE SHALL ANY 
LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, 
INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATED TO THIS LICENSE, 
THE USE OR INABILITY TO USE THE WORK (INCLUDING BUT NOT LIMITED TO LOSS OF 
GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS OR DATA, COMPUTER FAILURE OR 
MALFUNCTION, OR ANY OTHER COMM ERCIAL DAMAGES OR LOSSES), EVEN IF THE LICENSOR 
HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
Effective Date  April 18, 2008  2008 Amazon.com, Inc. or its affiliates. All 
rights reserved.
*/

const APP_ID = "YOUR_APP_ID_HERE";  // Replace this with your app id, this can be obtained from the Developer Console.

'use strict';

var Alexa = require('alexa-sdk');

const Https = require('https');

var resources = require('./resources.js')

/**
 * This is a small wrapper client for the Alexa Address API.
 * This code is taken from the official alexa github.
 * Source: https://github.com/alexa/skill-sample-node-device-address-api/blob/master/src/AlexaDeviceAddressClient.js
 * LICENSE GRANTED BY AMAZON FOR SAMPLE CODE IS LOCATED AT THE END OF THIS FILE
 */
class AlexaDeviceAddressClient {

	/**
	 * Retrieve an instance of the Address API client.
	 * @param apiEndpoint the endpoint of the Alexa APIs.
	 * @param deviceId the device ID being targeted.
	 * @param consentToken valid consent token.
	 */
	constructor(apiEndpoint, deviceId, consentToken) {
		//console.log("Creating AlexaAddressClient instance.");
		this.deviceId = deviceId;
		this.consentToken = consentToken;
		this.endpoint = apiEndpoint.replace(/^https?:\/\//i, "");
	}

	/**
	 * This will make a request to the Address API using the device ID and
	 * consent token provided when the Address Client was initialized.
	 * This will retrieve the full address of a device.
	 * @return {Promise} promise for the request in flight.
	 */
	getFullAddress() {
		const options = this.__getRequestOptions(`/v1/devices/${this.deviceId}/settings/address`);

		return new Promise((fulfill, reject) => {
			this.__handleDeviceAddressApiRequest(options, fulfill, reject);
		});
	}

	/**
	 * This will make a request to the Address API using the device ID and
	 * consent token provided when the Address Client was initialized.
	 * This will retrieve the country and postal code of a device.
	 * @return {Promise} promise for the request in flight.
	 */
	getCountryAndPostalCode() {
		const options = this.__getRequestOptions(
			`/v1/devices/${this.deviceId}/settings/address/countryAndPostalCode`);

		return new Promise((fulfill, reject) => {
			this.__handleDeviceAddressApiRequest(options, fulfill, reject);
		});
	}

	/**
	 * This is a helper method that makes requests to the Address API and handles the response
	 * in a generic manner. It will also resolve promise methods.
	 * @param requestOptions
	 * @param fulfill
	 * @param reject
	 * @private
	 */
	__handleDeviceAddressApiRequest(requestOptions, fulfill, reject) {
		Https.get(requestOptions, (response) => {
			
			//console.log(`Device Address API responded with a status code of : ${response.statusCode}`);

			response.on('data', (data) => {
				let responsePayloadObject = JSON.parse(data);

				const deviceAddressResponse = {
					statusCode: response.statusCode,
					address: responsePayloadObject
				};

				fulfill(deviceAddressResponse);
			});
		}).on('error', (e) => {
			console.error("ERROR: device address API failure, e = " + e);
			this.emit(':tell', 'We are having a temporary problem with back end services. Please try again later.');
			console.error(e);
			reject();
		});
	}

	/**
	 * Private helper method for retrieving request options.
	 * @param path the path that you want to hit against the API provided by the skill event.
	 * @return {{hostname: string, path: *, method: string, headers: {Authorization: string}}}
	 * @private
	 */
	__getRequestOptions(path) {
		return {
			hostname: this.endpoint,
			path: path,
			method: 'GET',
			'headers': {
				'Authorization': 'Bearer ' + this.consentToken
			}
		};
	}
}

/* END Amazon Sample Code */


/**
 * 
 * Below are the skill's Intent handlers.
 *
 **/
const handlers = {
	'LaunchRequest': function () {
		this.emit('GetCloudIntent');
	},
	'GetNewFactIntent': function () {
		this.emit('GetCloudIntent');
	},
	'GetCloudIntent': function () {

		console.log("ENTRY: GetCloudIntent");
 

		//
		// Get location consent token & other stuff
		//
		try {

			// security - must be provided correct application ID	
			try {            
				if (this.event.session.application.applicationId !== APP_ID) {
					console.error("ERROR: was presented bogus application ID = " + this.event.session.application.applicationId);
					reject();
					return;
				}
			} catch(err) {
				console.error("ERROR: exception while checking for application ID, err = " + err);
				reject();
				return;
			}
			
			var consentToken;
			
			try {

				// Access token
				consentToken = this.event.context.System.user.permissions.consentToken;
				
				// Test for consent not been granted
				if (!consentToken) {
					console.error("ERROR: !consentToken evaluated to true (consent not granted or other error)");
					this.emit(":tellWithPermissionCard",
							  "Please grant permission for Cloud Height to access your postal code in the skills section of your Alexa App.",
							  ["read::alexa:device:all:address:country_and_postal_code"]);
					return;
				} else {
					console.log("consentToken obtained");
				}
			}
			catch(err) {
				console.log("ERROR: exception; no consent token, err = " + err);
				this.emit(":tellWithPermissionCard",
						  "Please grant permission for Cloud Height to access your postal code in the skills section of your Alexa App.",
						  ["read::alexa:device:all:address:country_and_postal_code"]);
				return;
			}
			
			// Permission array for obtaining zip code
			const permissionArray = ['read::alexa:device:all:address:country_and_postal_code'];
			
			//
			// Get device ID
			//
			// Required for alexa device location API
			var deviceId;

			try {            
				deviceId = this.event.context.System.device.deviceId;
				// Test for bad deviceId
				if (!deviceId) {
					console.error("ERROR: !deviceId evaluated to true");
					this.emit(":tell", "Hmm. I'm having a problem with your request. Please try again later.");
					return;
				} else {
					console.log("deviceId obtained");
				}
			} catch(err) {
				console.error("ERROR: exception while checking for device ID, err = " + err);
				return;
			}
			

			//
			// Get API endpoint
			//
			// Required for alexa device location API
			var apiEndpoint;
			
			try {
				
				// Test for bad apiEndpoint
				apiEndpoint = this.event.context.System.apiEndpoint;
				if (!apiEndpoint) {
					console.error("ERROR: !apiEndpoint evaluated to true");
					this.emit(':tell', 'Hmm. I\'m having a problem with your request. Please try again later.');
					return;
				} else {
					console.log("apiEndpoint = " + apiEndpoint);
				}
			} catch(err) {
				console.error("ERROR: exception while checking for API endpoint, err = " + err);
				return;
			}



		
			// Prepare to obtain location
			const addressClient = new AlexaDeviceAddressClient(apiEndpoint, deviceId, consentToken);
			let deviceAddressRequest = addressClient.getCountryAndPostalCode();
	
			// Get location and handle response
			deviceAddressRequest.then((addressResponse) => {
				switch(addressResponse.statusCode) {
					case 200:
						const address = addressResponse.address;
						const zipCode = address['postalCode'];
						const countryCode = address['countryCode'];
						console.log("Address API response received");
						
						// Country code not provided
						if (!countryCode) {
							console.log("COUNTRY CODE NOT PROVIDED");
							this.emit(":tell", "Cloud Height only works in the fifty United States and Great Britain. Please ensure your address is set properly in the settings section of your Alexa App.");
							return;
						}
						
						// Zip code was not provided
						if (!zipCode) {
							console.log("ZIP CODE NOT PROVIDED");
							this.emit(":tell", "Cloud Height needs your postal code to find a nearby cloud reporting station. Please ensure your postal code is set in the settings section of your Alexa App.");
							return;
						}
						
						var zipLatLongTables = { "GB": resources.gb_post_codes, "US": resources.us_post_codes }

						//Returns error if there is no latlong table for the country code
						if (!zipLatLongTables[countryCode]) {
							console.log("COUNTRY NOT US, received "+countryCode+" zip: "+zipCode);
							this.emit(":tell", "Sorry, Cloud Height is not provided outside of the fifty United States and Great Britain.");
							return;
						}

						const latLong = zipLatLongTables[countryCode][zipCode.split(" ")[0]];
						
						// There was no lat/long entry for the supplied zip code.
						if (!latLong || !latLong[0] || !latLong[1]) {
							console.log("ZIP CODE NOT IN TABLE"); // , for zip code = " + zipCode);
							console.log("Zip code: "+zipCode)
							this.emit(":tell", "Hmm. Cloud Height can't find a cloud reporting station near the postal code that's saved in the settings section of your Alexa App.");
							return;
						}
						
						//Build request vars
						var radius_miles = 20;
						var hostname = "aviationweather.gov"
						var base_path = "/adds/dataserver_current/httpparam"
						var params = "?dataSource=metars&requestType=retrieve&format=csv"
						var radius_info = "&radialDistance=" + radius_miles + ";" + latLong[1] + "," + latLong[0]
						var search_constraints = "&mostRecentForEachStation=true&hoursBeforeNow=2"
						var path = base_path + params + radius_info + search_constraints
						
						Https.get({hostname:hostname, path:path, method:"GET"}, (response) => {
							
							// Response code logging. Enable for testing only.
							response.on('data', (data) => {
								
								// Response logging. Enable for testing only.
								console.log("Weather API response received"); // + data.toString('utf8'))
								
								// Parse the response.
								try {
									
									// split csv response into individual lines
									var lines = data.toString('utf8').split('\n');
									

									if (!lines || lines.length < 1) {
										console.error("ERROR: Problem with upstream provider.");
										this.emit(":tell", "Hmm. We're having trouble getting cloud information from our upstream provider. Please try again soon.");
										return;
									} else if (!lines[0].includes("No errors") || lines.length < 6) {
										console.error("ERROR: Error reported in response, line 1: " + lines[0]);
										if (lines.length > 1) {
											console.error("ERROR: line 2: " + lines[1]);
										}
										if (lines.length > 2) {
											console.error("ERROR: line 3: " + lines[2]);
										}
										if (lines.length > 3) {
											console.error("ERROR: line 4: " + lines[3]);
										}
										this.emit(":tell", "Hmm. We're having trouble getting cloud information from our upstream provider. Please try again soon.");
										return;
									}

									// look for valid data
									var i = 6      // the data starts on the seventh line; the first six are info/header
									var cloudLat;
									var cloudLon;
									var cloudType;
									var cloudHeight;
	
									var closestLat;
									var closestLon;
									var closestType;
									var closestHeight;
									var cardHeight;
									var cardType;
									
									var cardStationTime = "";
									var cardStationName = "";
									
									// We're only interested in data from the closest station.
									// Make initial value large enough so at least one station is closer.
									var closestStation = 1000;
									var cardStation;  // closestStation for the card
									
									var cardTitle;
									
									console.log("i = " + i.toString() + ", lines.length = " + lines.length.toString());
									
									// go through results until we find the first one with cloud type, lat and lon, and
									// cloud height (unless cloud type == CLR == clear skies below 12000)
									// if we don't find one, an exception will occur and we'll be able to tell the user.
									while (i < lines.length) {
										
										// get the current line
										var info = lines[i].split(',');
										
										// get lat, long
										cloudLat = info[3];
										cloudLon = info[4];
										
										// get first instance of cloud height and type
										cloudType = info[22];
										cloudHeight = info[23];
										
										// see if this station had valid cloud info. If so, and closer than last station, use its data.
										if (cloudLat && cloudLon && cloudType && (cloudHeight || (!cloudHeight && (cloudType == "CLR" || cloudType == "SKC")))) {
											
											// Use getDistance formula to get distance (see bottom of this file)
											// Round fractional distance up to the nearest mile.
											var distance = Math.ceil(findDistance(cloudLat, cloudLon, latLong[0], latLong[1]));
											
											if (distance < closestStation) {
												closestStation = distance;          // make this one the closer station
	
												// update closest values
												closestLat = cloudLat;
												closestLon = cloudLon;
												closestType = cloudType;
												closestHeight = cloudHeight;
												
												// get station name and time of measurement
												if (info[1].length > 0) {
													cardStationName = "Reporting station: " + info[1];
												}
												
												var datetime = info[2].split('T');
												var timeNoZ = datetime[1].split('Z');

												if (datetime.length == 2) {
													cardStationTime = "\nReport date/time (UTC): " + datetime[0] + " " + timeNoZ[0];
												}
												
												// get additional cloud layers
												var extraLayers = 0;   // reads 1 .. n if additional cloud layers found
												var layerType = [];
												var layerHeight = [];
												for (var j=0; j<3; j++) {
													layerType[j] = info[24 + (j*2)];
													layerHeight[j] = info[25 + (j*2)];
													// if no data, stop processing layers
													if (!layerType[j] || !layerHeight[j]) {
														break;
													} else {
														extraLayers++;
													}
												}
											}
										}

										// go to the next line
										i++
									}

									// No data returned?
									if (lines.length <= 6 || closestStation > 900) {
										console.log("NO STATION WITH WEATHER: no station found with weather");
										this.emit(":tell", "Unfortunately, we can't find a cloud measuring station with current data within 20 miles of your devices postal code.");
										return;    
									}
								}
								
								// We didn't find cloud info.
								catch(err) {
									console.error("EXCEPTION: error parsing returned weather info, error = " + err.toString());
									this.emit(":tell", "Unfortunately, we can't find a cloud measuring station with current data within 20 miles of your devices postal code. Please try again later.");
									return;    
								}
								
								// Use getDistance formula to get distance (see bottom of this file)
								// Round fractional distance up to the nearest mile.
								//var distance = Math.ceil(findDistance(cloudLat, cloudLon, latLong[0], latLong[1]));
								cardStation = closestStation;
								
								// Handle one mile differently than many miles
								if (closestStation <= 1) {
									closestStation = " less than a mile "
								} else {
									closestStation = closestStation + " miles "
								}
								
								var cloudSpeakAgl;
								
								var cardSpeak =  {"BKN":"Mostly cloudy at " + closestHeight + " feet AGL",
												   "CB":"Cumulonimbus clouds at " + closestHeight + " feet AGL",
												   "CLR":"Sky clear below 12000 feet (AGL)",
												   "FEW":"Few clouds at " + closestHeight + " feet AGL",
												   "OVC":"Overcast, ceiling " + closestHeight + " feet AGL",
												   "SCT":"Scattered clouds at " + closestHeight + " feet AGL",
												   "SKC":"Sky clear",
												   "TCU":"Towering cumulus at " + closestHeight + " feet AGL"}  
												   
								cloudSpeakAgl = "at <say-as interpret-as=\"cardinal\">" + closestHeight + "</say-as> feet above ground level";
								
								// Each cloud density has a different utterance. We set them here. These utterances are for when there is only one cloud layer.
								var cloudSpeak = {"BKN":"Broken clouds " + cloudSpeakAgl,
												   "CB":"Cumulonimbus clouds " + cloudSpeakAgl,
												   "CLR":"No clouds below twelve thousand feet above ground level",
												   "FEW":"Few clouds " + cloudSpeakAgl,
												   "OVC":"Overcast conditions " + cloudSpeakAgl,
												   "SCT":"Scattered clouds " + cloudSpeakAgl,
												   "SKC":"Clear skies",
												   "TCU":"Towering cumulus " + cloudSpeakAgl}
								 
								// If we do not recognize the cloud type returned by the API, just speak the cloud height.                   
								if (!cloudSpeak[closestType]) {
									console.error("ERROR: unexpected cloud type = " + closestType)
									this.emit(":tell", "We found a cloud measuring station " + closestStation + " away. But alas, we can't understand what it is telling us.");
									return;
								}
								
								var zipCodeText;
                                var zipSourceText;

								if (countryCode == "US") {
									zipCodeText = "zip code."
                                    zipSourceText = "US Census Bureau (2016)"
								} else {
									zipCodeText = "post code."
                                    zipSourceText = "National Statistics Postcode Lookup UK\nContains public sector information licensed under the Open Government Licence v3.0."
								}
								// Success! Prepare the cloud info for Alexa to read.
								var cloudFinal = " were reported within " + closestStation + " of your "+zipCodeText;
								var cardText;
								
								var cardTitleText = cardSpeak[closestType];
								
								var latitude = latLong[0];
								var longitude = latLong[1];

								var latDir = "N";
								if (latitude < 0) {
									latDir = "S";
									latitude = 0 - latitude;
								} 
								var longDir = "E";
								if (longitude < 0) {
									longDir = "W";
									longitude = 0 - longitude;
								} 
								var cardStationDistance = "\nDistance from " + zipCode + " (" + latitude + latDir + ", " + longitude + longDir + "): " + closestStation;

								var layerAGL;
								
								cardText = "";   // initialize in case there are no more layers
								
								var cloudText = "";

								if (extraLayers > 1) {   // plural
									cardText = "Also: ";
								} else if (extraLayers > 0) {   // singular
									cardText = "Also: ";
								}
								
								// Add speech for extra layers.
								for (var k=0; k<extraLayers; k++) {

									// terminate first sentence
									if (k + 4 < extraLayers) {
										cloudText += ", ";
									} else if (k + 3 < extraLayers) {
										cloudText += ", ";
									} else if (k + 2 < extraLayers) {
										cloudText += ", ";
									} else if (k + 1 < extraLayers) {
										cloudText += ", ";
									} else {
										cloudText += ", and ";
									}
									
									layerAGL = layerHeight[k];
									var layerSpeakAgl;
									
									cardSpeak = {"BKN":"mostly cloudy at " + layerAGL + " feet",
												   "CB":"cumulonimbus clouds at " + layerAGL + " feet",
												   "CLR":"sky clear below 12000 feet",
												   "FEW":"few clouds at " + layerAGL + " feet",
												   "OVC":"overcast, ceiling " + layerAGL + " feet",
												   "SCT":"scattered clouds at " + layerAGL + " feet",
												   "SKC":"sky clear",
												   "TCU":"towering cumulus, with a base at " + layerAGL + " feet"}  
												   
									if (layerAGL == "12000") {
										layerSpeakAgl = "at <say-as interpret-as=\"cardinal\">" + layerAGL + "</say-as> feet";
									} else {
										layerSpeakAgl = "at <say-as interpret-as=\"cardinal\">" + layerAGL + "</say-as> feet";
									}
									
									// Extra speech when there are multiple cloud layers.
									var layerSpeak = {"BKN":" a broken cloud layer "+ layerSpeakAgl,
													  "CB":" cumulonimbus clouds "+ layerSpeakAgl,
													  "CLR":" skies clear below twelve thousand feet",
													  "FEW":" few clouds "+ layerSpeakAgl,
													  "OVC":" an overcast layer "+ layerSpeakAgl,
													  "SCT":" scattered clouds "+ layerSpeakAgl,
													  "SKC":" the sky is clear",
													  "TCU":" towering cumulus with a base "+ layerSpeakAgl}
												   
									cardText += cardSpeak[layerType[k]];

									if (k + 3 < extraLayers) {
										cardText += "; ";
									} else if (k + 2 < extraLayers) {
										cardText += "; ";
									} else if (k + 1 < extraLayers && extraLayers > 1) {
										cardText += "; ";
									} else if (k + 1 < extraLayers) {
										cartText += "; ";  // was final "and" in a sentence
									}

									cloudText += layerSpeak[layerType[k]];
								}
								
								if (extraLayers > 0) {
									cardText += "\n";
								}
								

								// Speak the clouds
								cardText += cardStationName + cardStationTime + cardStationDistance + "\nData sources: aviationweather.gov, "+zipSourceText;

								console.log("SUCCESS: cloudText sent");

								var cloudOutput = cloudSpeak[closestType] + cloudText + cloudFinal;
								this.emit(":tellWithCard", cloudOutput, cardTitleText, cardText);
								return;
							});
							
						// What to do if the HTTP request for the METAR fails.
						}).on('error', (e) => {
							console.error("ERROR: aviation weather API error: " + e);
							this.emit(":tell", "Hmm. We're unable to get cloud information from our upstream provider. Please try again later.");
							reject();
						});
						break;
					case 204:
						console.error("ERROR: deviceAddressRequest response code 204");
						this.emit(":tell", "We use your postal code to find clouds near you. You can set it in the settings section of your Alexa App.");
						return;
						break;
					case 403:
						console.error("ERROR: deviceAddressRequest response code 403");
						this.emit(':tell', 'To find clouds near you, we need to know where you are. You can give us permission in the skills section of your Alexa App.');
						return;
						break;
					default:
						console.error("ERROR: unexpected deviceAddressRequest response code = " + addressResponse.statusCode.toString());
						this.emit(":tell", "Oh dear. The ones and zeros are not aligning right now. Please try again later.");
						return;
				}
			})
			
			deviceAddressRequest.catch((error) => {
				console.error("ERROR: Exception in deviceAddressRequest, e = " + error.toString())
				this.emit(":tell", "Oh dear. The zeros and ones are not aligning right now. Please try again later.");
				return;
			})
		}
		catch(err) {
			console.error("ERROR: Some unhandled exception: e = " + err);
			this.emit(':tell', 'Hmm. I\'m having trouble getting cloud information. Please try again later.');
			return;   
		}
	},
	'AMAZON.HelpIntent': function () {
		this.emit(':ask', 'You can say what is the cloud height, or, you can say exit... What can I help you with?','You can say what is the cloud height, or, you can say exit... What can I help you with?');
	},
	'AMAZON.CancelIntent': function () {
		this.emit(':tell', 'Goodbye!');
	},
	'AMAZON.StopIntent': function () {
		this.emit(':tell', 'Goodbye!');
	},
	'Unhandled': function() {
		this.emit('GetCloudIntent');
	}
};


//Configure handler
exports.handler = function (event, context, callback) {
	var alexa = Alexa.handler(event, context, callback);
	alexa.appId = APP_ID;
	alexa.registerHandlers(handlers);
	alexa.execute();
};


// END of Alexa Skill source licensed under Amazon Software License

// START of Javascript lat/long distance calculator

//==============================================================================
//
// Javascript lat/long distance calculator is by Andrew Hedges and can be
// found at http://andrew.hedges.name/experiments/haversine/
// Andrew provides the following license on his website. The code that
// follows powers the lat/long distance calculator on his website.
//
//==============================================================================
/*
Copyright  2002 Andrew Hedges

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in 
the Software without restriction, including without limitation the rights to 
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
of the Software, and to permit persons to whom the Software is furnished to do 
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
SOFTWARE.
*/

/*
This script is pretty basic, but if you use it, please let me know.  Thanks!
Andrew Hedges, andrew(at)hedges(dot)name
*/

var Rm = 3961; // mean radius of the earth (miles) at 39 degrees from the equator
var Rk = 6373; // mean radius of the earth (km) at 39 degrees from the equator
	
/* main function */
function findDistance(inlat1, inlon1, inlat2, inlon2) {
	var t1, n1, t2, n2, lat1, lon1, lat2, lon2, dlat, dlon, a, c, dm, dk, mi, km;
	
	// get values for lat1, lon1, lat2, and lon2
	t1 = inlat1;
	n1 = inlon1;
	t2 = inlat2;
	n2 = inlon2;
	
	// convert coordinates to radians
	lat1 = deg2rad(t1);
	lon1 = deg2rad(n1);
	lat2 = deg2rad(t2);
	lon2 = deg2rad(n2);
	
	// find the differences between the coordinates
	dlat = lat2 - lat1;
	dlon = lon2 - lon1;
	
	// here's the heavy lifting
	a  = Math.pow(Math.sin(dlat/2),2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(dlon/2),2);
	c  = 2 * Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); // great circle distance in radians
	dm = c * Rm; // great circle distance in miles
	dk = c * Rk; // great circle distance in km
	
	// round the results down to the nearest 1/1000
	mi = round(dm);
	km = round(dk);
	
	// We are only interested in miles.
	return mi;
}

// convert degrees to radians
function deg2rad(deg) {
	var rad = deg * Math.PI/180; // radians = degrees * pi/180
	return rad;
}

// round to the nearest 1/1000
function round(x) {
	return Math.round( x * 1000) / 1000;
}

resources.js

JavaScript
This file contains latitude and longitude coordinates for every US and UK postcode. These objects are required by the index.js file in order to determine the nearest METAR station using the postcode given by the Alexa Skills Kit.

Data Sources:
UK Post Codes: National Statistics Postcode Lookup UK
Contains public sector information licensed under the Open Government License v3.0.

US Zip Codes: US Census Bureau (2016)

Note: this file can be downloaded from https://s3.amazonaws.com/originalcloud/resources.js
The file had too many lines to upload to hackster without crashing the browser.
/*
This file has too many lines to upload to the hackster project. (I crashed my browser...) You can download the full file at:
https://s3.amazonaws.com/originalcloud/resources.js
*/

Intent Schema

JSON
This is the intent schema for the Alexa Skills Kit. It should be put into the Interaction Model tab of your Alexa Skills Kit project in the Amazon Developer Console.
{
  "intents": [
    {
      "intent": "AMAZON.CancelIntent"
    },
    {
      "intent": "AMAZON.HelpIntent"
    },
    {
      "intent": "AMAZON.StopIntent"
    },
    {
      "intent": "GetCloudIntent"
    }
  ]
}

Utterances

JSON
These are the utterances, which are possible phrases a user can say to initiate the GetCloudIntent intent in the Alexa Skills Kit. It should be put in the Utterances Section of the Interaction Model Tab of your Alexa Skills Kit Project in the Amazon Developer Console.
GetCloudIntent whats the deal with the clouds right now
GetCloudIntent to get me cloud info
GetCloudIntent cloud info
GetCloudIntent get me cloud info
GetCloudIntent how high are the clouds
GetCloudIntent clouds
GetCloudIntent whats the cloud coverage
GetCloudIntent whats up with the clouds
GetCloudIntent how high are the clouds today
GetCloudIntent how high the clouds are
GetCloudIntent for cloud info
GetCloudIntent cloud height
GetCloudIntent for cloud height
GetCloudIntent cloud cover
GetCloudIntent for cloud cover
GetCloudIntent for the cloud cover
GetCloudIntent for the cloud height
GetCloudIntent for the cloud info
GetCloudIntent where are the clouds
GetCloudIntent how far away are the clouds
GetCloudIntent how far are the clouds
GetCloudIntent how far are clouds
GetCloudIntent how thick are the clouds
GetCloudIntent how thick are clouds
GetCloudIntent what the cloud height is
GetCloudIntent what's the cloud height
GetCloudIntent what's the altitude of the clouds
GetCloudIntent what's the cloud altitude
GetCloudIntent what altitude are the clouds
GetCloudIntent cloud altitude
GetCloudIntent altitude
GetCloudIntent height

Credits

Sean

Sean

0 projects • 1 follower
I like coding and road trips and building stuff.

Comments