diff --git a/front/package.json b/front/package.json index 44047dda70..e7089efe0c 100644 --- a/front/package.json +++ b/front/package.json @@ -38,7 +38,6 @@ "immutability-helper": "^3.0.0", "leaflet": "^1.4.0", "linkstate": "^1.1.1", - "moment": "^2.24.0", "preact": "^8.2.6", "preact-cli-plugin-fast-async": "^1.0.1", "preact-compat": "^3.18.4", diff --git a/front/src/actions/dashboard/boxes/weather.js b/front/src/actions/dashboard/boxes/weather.js index ae53e79746..50266d3019 100644 --- a/front/src/actions/dashboard/boxes/weather.js +++ b/front/src/actions/dashboard/boxes/weather.js @@ -6,17 +6,52 @@ import get from 'get-value'; const BOX_KEY = 'Weather'; -function createActions(store) { +const translateWeatherToFeIcon = weather => { + if (weather === 'snow') { + return 'fe-cloud-snow'; + } + if (weather === 'rain') { + return 'fe-cloud-rain'; + } + if (weather === 'clear') { + return 'fe-sun'; + } + if (weather === 'cloud') { + return 'fe-cloud'; + } + if (weather === 'fog') { + return 'fe-cloud'; + } + if (weather === 'sleet') { + return 'fe-cloud-drizzle'; + } + if (weather === 'wind') { + return 'fe-wind'; + } + if (weather === 'night') { + return 'fe-moon'; + } + return 'fe-question'; +}; + +const createActions = store => { const boxActions = createBoxActions(store); const actions = { async getWeather(state, box, x, y) { boxActions.updateBoxStatus(state, BOX_KEY, x, y, RequestStatus.Getting); try { - const weather = await state.httpClient.get(`/api/v1/house/${box.house}/weather`); + const weather = await state.httpClient.get(`/api/v1/house/${box.house}/weather?mode=${box.mode}`); weather.datetime_beautiful = dayjs(weather.datetime) .locale(state.user.language) - .format('D MMM'); + .format('dddd DD MMMM'); + weather.temperature = weather.temperature.toFixed(2); + weather.weather = translateWeatherToFeIcon(weather.weather); + if (box.mode === 'advanced') { + weather.hours.map(day => { + day.weather = translateWeatherToFeIcon(day.weather); + }); + } boxActions.mergeBoxData(state, BOX_KEY, x, y, { weather }); @@ -36,6 +71,6 @@ function createActions(store) { } }; return Object.assign({}, actions); -} +}; export default createActions; diff --git a/front/src/components/boxs/weather/EditWeatherBox.jsx b/front/src/components/boxs/weather/EditWeatherBox.jsx index cceb13f358..ac46092990 100644 --- a/front/src/components/boxs/weather/EditWeatherBox.jsx +++ b/front/src/components/boxs/weather/EditWeatherBox.jsx @@ -3,6 +3,7 @@ import { connect } from 'unistore/preact'; import { Text } from 'preact-i18n'; import actions from '../../../actions/dashboard/edit-boxes/editWeather'; import BaseEditBox from '../baseEditBox'; +import { GetWeatherModes } from '../../../utils/consts'; const EditWeatherBox = ({ children, ...props }) => ( @@ -22,6 +23,30 @@ const EditWeatherBox = ({ children, ...props }) => ( ))} +
+
+ +
+
+ {GetWeatherModes.map(mode => { + const label = 'dashboard.boxes.weather.displayModes.' + mode; + return ( + + ); + })} +
+
); @@ -35,12 +60,19 @@ class EditWeatherBoxComponent extends Component { house: e.target.value }); }; + + updateBoxMode = e => { + this.props.updateBoxConfig(this.props.x, this.props.y, { + mode: e.target.value + }); + }; + componentDidMount() { this.props.getHouses(); } render(props, {}) { - return ; + return ; } } diff --git a/front/src/components/boxs/weather/WeatherBox.jsx b/front/src/components/boxs/weather/WeatherBox.jsx index 86c80690d1..4425eed4ae 100644 --- a/front/src/components/boxs/weather/WeatherBox.jsx +++ b/front/src/components/boxs/weather/WeatherBox.jsx @@ -2,6 +2,7 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; import { Text } from 'preact-i18n'; import { Link } from 'preact-router/match'; +import dayjs from 'dayjs'; import actions from '../../../actions/dashboard/boxes/weather'; import { RequestStatus, @@ -18,7 +19,7 @@ const padding = { paddingBottom: '10px' }; -const BOX_REFRESH_INTERVAL_MS = 30 * 60 * 1000; +const BOX_REFRESH_INTERVAL_MS = 10 * 60 * 1000; const WeatherBox = ({ children, ...props }) => (
@@ -97,19 +98,20 @@ const WeatherBox = ({ children, ...props }) => ( )} {props.weather && (
+
+ {props.datetimeBeautiful} - {props.houseName} +
-
-
- {props.datetimeBeautiful} -
+
@@ -124,37 +126,72 @@ const WeatherBox = ({ children, ...props }) => (
- {props.weather === 'rain' && ( + +
+
+ {props.display === 'advanced' && ( +
+ - )} - {props.weather === 'sun' && ( - - )} - {props.weather === 'cloud' && ( + > + % + + + - )} + {props.wind} + + {props.units === 'si' ? 'km/h' : 'm/h'} + +
-
+ )} + {props.display === 'advanced' && ( +
+ {props.alert_display} +
+ {props.hours_display} +
+
+ )}
)}
@@ -176,11 +213,67 @@ class WeatherBoxComponent extends Component { const boxData = get(props, `${DASHBOARD_BOX_DATA_KEY}Weather.${props.x}_${props.y}`); const boxStatus = get(props, `${DASHBOARD_BOX_STATUS_KEY}Weather.${props.x}_${props.y}`); const weatherObject = get(boxData, 'weather'); - const weather = get(weatherObject, 'weather'); - const temperature = Math.round(get(weatherObject, 'temperature')); - const units = get(weatherObject, 'units'); + + const display = this.props.box.mode || 'basic'; const datetimeBeautiful = get(weatherObject, 'datetime_beautiful'); + const units = get(weatherObject, 'units'); + const temperature = get(weatherObject, 'temperature'); const houseName = get(weatherObject, 'house.name'); + let weather = get(weatherObject, 'weather'); + + let humidity, wind, alert_display, hours_display; + + if (display === 'advanced') { + humidity = get(weatherObject, 'humidity'); + wind = get(weatherObject, 'wind_speed'); + const alert = get(weatherObject, 'alert'); + if (units === 'si') { + wind = wind * 3.6; + wind = wind.toFixed(2); + } + if (typeof alert != 'undefined' && alert !== null) { + let color = '#FFD6D4'; + if (alert.severity === 'warning') { + color = '#FF8D87'; + } + alert_display = ( +
+

{alert.title}

+

{alert.description}

+
+ ); + } + const hours = get(weatherObject, 'hours'); + if (typeof hours !== 'undefined') { + let i = 0; + hours_display = hours.map(hour => { + const borderStyle = {}; + if (i === 3) { + borderStyle.borderRight = '0.01em dotted grey'; + borderStyle.marginRight = '0'; + } + if (i === 4) { + borderStyle.borderLeft = '0.01em dotted grey'; + borderStyle.marginLeft = '0'; + } + i += 1; + return ( +
+

+ {dayjs(hour.datetime).format('HH')}h +

+

+ +

+

{hour.temperature}°

+
+ ); + }); + } + } return ( ); } diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index f2adebd617..7f037cf2f7 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -133,7 +133,12 @@ "serviceNotConfigured": "The DarkSky service is not configured. Please go to the 'Integrations' tab, and configure the DarkSky service.", "requestToThirdPartyFailed": "The request to DarkSKy API failed. Is your Gladys instance connected to the internet? Please go to the DarkSky configuration panel to troubleshoot this problem.", "clickHere": "Click here to access the DarkSky configuration panel.", - "unknownError": "We are unable to get the weather for this house. Did you define a house for this box?" + "unknownError": "We are unable to get the weather for this house. Did you define a house for this box?", + "editModeLabel": "Select the mode you want to display.", + "displayModes": { + "basic": "Basic : temperature", + "advanced": "Advanced : temperature, humidity, wind speed, previsions for the next 8 hours and forecast alerts" + } }, "devicesInRoom": { "editRoomLabel": "Select the room you want to display here." diff --git a/front/src/routes/integration/all/darksky/DarkSky.jsx b/front/src/routes/integration/all/darksky/DarkSky.jsx index 2ea02b09c4..b57facdbd1 100644 --- a/front/src/routes/integration/all/darksky/DarkSky.jsx +++ b/front/src/routes/integration/all/darksky/DarkSky.jsx @@ -46,7 +46,7 @@ const DarkSkyPage = ({ children, ...props }) => ( class={cx('btn', 'btn-success', { 'btn-loading': props.loading })} - onClick={props.saveApiKey} + onClick={props.saveConfig} type="button" > diff --git a/front/src/routes/integration/all/darksky/actions.js b/front/src/routes/integration/all/darksky/actions.js index 3517f1cd71..05fbae843d 100644 --- a/front/src/routes/integration/all/darksky/actions.js +++ b/front/src/routes/integration/all/darksky/actions.js @@ -6,39 +6,39 @@ const actions = store => ({ darkSkyApiKey: e.target.value }); }, - async getApiKey(state) { + async getConfig(state) { store.setState({ - darkskyGetApiKeyStatus: RequestStatus.Getting + darkskyConfigStatus: RequestStatus.Getting }); try { - const variable = await state.httpClient.get('/api/v1/service/darksky/variable/DARKSKY_API_KEY'); + const apiKey = await state.httpClient.get('/api/v1/service/darksky/variable/DARKSKY_API_KEY'); store.setState({ - darkSkyApiKey: variable.value, - darkskyGetApiKeyStatus: RequestStatus.Success + darkSkyApiKey: apiKey.value, + darkskyGetConfigStatus: RequestStatus.Success }); } catch (e) { store.setState({ - darkskyGetApiKeyStatus: RequestStatus.Error + darkskyGetConfigStatus: RequestStatus.Error }); } }, - async saveApiKey(state) { + async saveConfig(state) { store.setState({ - darkskySaveApiKeyStatus: RequestStatus.Getting + darkskySaveConfigStatus: RequestStatus.Getting }); try { - // save api key + // save config await state.httpClient.post('/api/v1/service/darksky/variable/DARKSKY_API_KEY', { - value: state.darkSkyApiKey + value: state.darkSkyApiKey.trim() }); // start service await state.httpClient.post('/api/v1/service/darksky/start'); store.setState({ - darkskySaveApiKeyStatus: RequestStatus.Success + darkskySaveConfigStatus: RequestStatus.Success }); } catch (e) { store.setState({ - darkskySaveApiKeyStatus: RequestStatus.Error + darkskySaveConfigStatus: RequestStatus.Error }); } } diff --git a/front/src/routes/integration/all/darksky/index.js b/front/src/routes/integration/all/darksky/index.js index 2345e12e84..9cd4c23f68 100644 --- a/front/src/routes/integration/all/darksky/index.js +++ b/front/src/routes/integration/all/darksky/index.js @@ -5,19 +5,19 @@ import DarkSkyPage from './DarkSky'; import { RequestStatus } from '../../../../utils/consts'; @connect( - 'user,darkSkyApiKey,darkskySaveApiKeyStatus,darkskyGetApiKeyStatus', + 'user,darkSkyApiKey,darkskySaveConfigStatus,darkskyGetConfigStatus', actions ) -class TelegramIntegration extends Component { +class DarkSkyIntegration extends Component { componentWillMount() { - this.props.getApiKey(); + this.props.getConfig(); } render(props, {}) { const loading = - props.darkskySaveApiKeyStatus === RequestStatus.Getting || props.darkskyGetApiKeyStatus === RequestStatus.Getting; + props.darkskySaveConfigStatus === RequestStatus.Getting || props.darkskyGetConfigStatus === RequestStatus.Getting; return ; } } -export default TelegramIntegration; +export default DarkSkyIntegration; diff --git a/front/src/utils/consts.js b/front/src/utils/consts.js index 43a4a394e5..61af398ec5 100644 --- a/front/src/utils/consts.js +++ b/front/src/utils/consts.js @@ -71,6 +71,8 @@ export const GetWeatherStatus = { RequestToThirdPartyFailed: 'RequestToThirdPartyFailed' }; +export const GetWeatherModes = ['basic', 'advanced']; + export const DASHBOARD_BOX_STATUS_KEY = 'DashboardBoxStatus'; export const DASHBOARD_BOX_DATA_KEY = 'DashboardBoxData'; diff --git a/server/api/controllers/weather.controller.js b/server/api/controllers/weather.controller.js index b545f9a1c0..955962548d 100644 --- a/server/api/controllers/weather.controller.js +++ b/server/api/controllers/weather.controller.js @@ -7,6 +7,7 @@ module.exports = function WeatherController(gladys) { * @api {get} /api/v1/user/:user_selector/weather get weather user * @apiName getWeatherUser * @apiGroup Weather + * @apiParam {string} [mode] Mode of the result * @apiSuccessExample {json} Success-Example * { * "temperature": 27.28, @@ -24,6 +25,8 @@ module.exports = function WeatherController(gladys) { latitude: lastLocation.latitude, longitude: lastLocation.longitude, language: req.user.language, + mode: req.query.mode, + datetime: req.query.datetime, }; const weatherResult = await gladys.weather.get(options); res.json(weatherResult); @@ -33,6 +36,7 @@ module.exports = function WeatherController(gladys) { * @api {get} /api/v1/house/:house_selector/weather get weather house * @apiName getWeatherHouse * @apiGroup Weather + * @apiParam {string} [mode] Mode of the result * @apiSuccessExample {json} Success-Example * { * "temperature": 27.28, @@ -53,6 +57,8 @@ module.exports = function WeatherController(gladys) { latitude: house.latitude, longitude: house.longitude, language: req.user.language, + mode: req.query.mode, + datetime: req.query.datetime, }; const weatherResult = await gladys.weather.get(options); weatherResult.house = house; diff --git a/server/config/brain/weather/answers.en.json b/server/config/brain/weather/answers.en.json index 6645796edc..610fa76ad5 100644 --- a/server/config/brain/weather/answers.en.json +++ b/server/config/brain/weather/answers.en.json @@ -7,10 +7,6 @@ "label": "weather.get.success.rain", "answers": ["It's raining and temperature outside is {{ temperature }} {{ units }}."] }, - { - "label": "weather.get.success.clear", - "answers": ["It's a clear day today. Temperature outside is {{ temperature }} {{ units }}."] - }, { "label": "weather.get.success.cloud", "answers": ["It's cloudy outside and temperature is {{ temperature }} {{ units }}."] @@ -27,10 +23,32 @@ "label": "weather.get.success.wind", "answers": ["Windy day today. Temperature outside is {{ temperature }} {{ units }}."] }, + { + "label": "weather.get.success.night", + "answers": ["Clear night today. Temperature outside is {{ temperature }} {{ units }}."] + }, + { + "label": "weather.get.success.clear", + "answers": ["Clear day today. Temperature outside is {{ temperature }} {{ units }}."] + }, { "label": "weather.get.success.unknown", "answers": ["Temperature outside is {{ temperature }} {{ units }}."] }, + + { + "label": "weather.getPrevisions.success.daily", + "answers": [ + "{{previsionDate}}, it will be {{summary}} outside. The temperature should be beetween {{temperatureMin}}{{units}} and {{temperatureMax}}{{units}}" + ] + }, + { + "label": "weather.getPrevisions.success", + "answers": [ + "{{previsionDate}} at {{previsionTime}}, it will be {{summary}} outside. The temperature should be around {{temperature}}{{units}}" + ] + }, + { "label": "weather.get.fail", "answers": ["There was an error while getting the weather."] diff --git a/server/config/brain/weather/questions.en.json b/server/config/brain/weather/questions.en.json index b6821d0180..5018f698da 100644 --- a/server/config/brain/weather/questions.en.json +++ b/server/config/brain/weather/questions.en.json @@ -1,6 +1,16 @@ [ { "label": "weather.get", - "questions": ["What's the weather like?"] + "questions": ["What's the weather like?", "What's the weather?", "What's the current weather?"] + }, + { + "label": "weather.getPrevisions", + "questions": [ + "What will be the weather in %hours% hours?", + "What will be the weather in %days% days?", + "What will be the weather at %hours%?", + "What will be the weather %day%?", + "What will be the weather tomorrow?" + ] } ] diff --git a/server/lib/weather/index.js b/server/lib/weather/index.js index d05623bda3..c9ceeb2c75 100644 --- a/server/lib/weather/index.js +++ b/server/lib/weather/index.js @@ -1,4 +1,4 @@ -const { get } = require('./weather.get'); +const { get, getPrevisions } = require('./weather.get'); const { command } = require('./weather.command'); const { INTENTS } = require('../../utils/constants'); @@ -8,9 +8,11 @@ const Weather = function Weather(service, event, messageManager, house) { this.messageManager = messageManager; this.house = house; this.event.on(INTENTS.WEATHER.GET, this.command.bind(this)); + this.event.on(INTENTS.WEATHER.GET_PREVISIONS, this.command.bind(this)); }; Weather.prototype.get = get; +Weather.prototype.getPrevisions = getPrevisions; Weather.prototype.command = command; module.exports = Weather; diff --git a/server/lib/weather/weather.command.js b/server/lib/weather/weather.command.js index c20932ae43..482f441f21 100644 --- a/server/lib/weather/weather.command.js +++ b/server/lib/weather/weather.command.js @@ -1,6 +1,44 @@ +const moment = require('moment'); const logger = require('../../utils/logger'); const { ServiceNotConfiguredError, NoValuesFoundError } = require('../../utils/coreErrors'); +const DAYS_IN_WEEK = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; + +const parseTime = (time) => { + let hour = 0; + let minute = 0; + if (time.search('AM') !== -1) { + hour = Number(time.substring(0, time.length - 2)); + } else if (time.search('PM') !== -1) { + hour = Number(time.substring(0, time.length - 2)) + 12; + } else if (time.search(':') !== -1) { + [hour, minute] = time.split(':'); + } + return { hour, minute }; +}; + +const parseWeek = (day, contextDate) => { + let i = 1; + let dayActual = 0; + let dayTarget = 0; + DAYS_IN_WEEK.forEach((dayInWeek) => { + if (day.format('dddd').toLowerCase() === dayInWeek) { + dayActual = i; + } + if (contextDate === dayInWeek) { + dayTarget = i; + } + i += 1; + }); + if (dayActual === dayTarget) { + day.add(7, 'days'); + } else if (dayTarget > dayActual) { + day.add(dayTarget - dayActual, 'days'); + } else { + day.add(7 - dayActual + dayTarget, 'days'); + } +}; + /** * @description Get the weather in a text request. * @param {Object} message - The message sent by the user. @@ -17,6 +55,14 @@ async function command(message, classification, context) { if (!house || !house.latitude || !house.longitude) { throw new NoValuesFoundError(); } + const day = moment().subtract( + moment() + .toDate() + .getTimezoneOffset(), + 'minutes', + ); + let hour = 0; + let minute = 0; switch (classification.intent) { case 'weather.get': weather = await this.get(house); @@ -24,6 +70,89 @@ async function command(message, classification, context) { context.units = weather.units === 'si' ? '°C' : '°F'; await this.messageManager.replyByIntent(message, `weather.get.success.${weather.weather}`, context); break; + case 'weather.getPrevisions': + if (context.time) { + house.target = 'hourly'; + const { hour: Thour, minute: Tminute } = parseTime(context.time); + hour = Thour; + minute = Tminute; + if (hour < day.hour()) { + day.add(1, 'days'); + } + } else if (context.date) { + house.target = 'daily'; + const date = context.date.split(' '); + if (date[0] === 'in') { + if (date[2] === 'days') { + day.add(date[1], 'days'); + } + } else { + if (DAYS_IN_WEEK.includes(context.date)) { + parseWeek(day, context.date); + } + if (context.date.toLowerCase() === 'tomorrow') { + day.add(1, 'days'); + } + } + } else if (context.datetime) { + const datetime = context.datetime.split(' '); + if (datetime[0] === 'in') { + const dayToCompare = day.clone(); + if (datetime[2] === 'minutes') { + house.target = 'hourly'; + [, minute] = datetime; + if (day.date() !== dayToCompare.add(datetime[1], 'minutes').date()) { + day.add(datetime[1], 'minutes'); + hour = day.hour(); + minute = day.minute(); + } + } + if (datetime[2] === 'hours') { + house.target = 'hourly'; + [, hour] = datetime; + if (day.date() !== dayToCompare.add(datetime[1], 'hours').date()) { + day.add(datetime[1], 'hours'); + hour = day.hour(); + minute = day.minute(); + } else { + hour = dayToCompare.hour(); + minute = dayToCompare.minute(); + } + } + } else if (datetime[1] === 'at') { + house.target = 'hourly'; + parseWeek(day, datetime[0]); + const { hour: Thour, minute: Tminute } = parseTime(datetime[2]); + hour = Thour; + minute = Tminute; + } + } + day.startOf('day'); + day.set('hours', hour); + day.set('minutes', minute); + house.datetime = day + .clone() + .add( + moment() + .toDate() + .getTimezoneOffset(), + 'minutes', + ) + .unix(); + weather = await this.get(house); + context.previsionDate = day.format('dddd DD'); + context.previsionTime = `${day.format('HH')}h${day.format('mm')}`; + context.summary = weather.summary; + context.units = weather.units === 'si' ? '°C' : '°F'; + if (weather.temperatureMin && weather.temperatureMax) { + context.temperatureMin = weather.temperatureMin; + context.temperatureMax = weather.temperatureMax; + await this.messageManager.replyByIntent(message, `weather.getPrevisions.success.daily`, context); + } else { + context.temperature = weather.temperature; + await this.messageManager.replyByIntent(message, `weather.getPrevisions.success`, context); + } + break; default: throw new Error('Not found'); } diff --git a/server/lib/weather/weather.get.js b/server/lib/weather/weather.get.js index a433b47f48..1cda646d51 100644 --- a/server/lib/weather/weather.get.js +++ b/server/lib/weather/weather.get.js @@ -5,16 +5,21 @@ const { ServiceNotConfiguredError } = require('../../utils/coreErrors'); * @param {Object} options - Options parameters. * @param {number} options.latitude - The latitude to get the weather from. * @param {number} options.longitude - The longitude to get the weather from. - * @param {number} options.offset - Get weather in the future, offset is in hour. + * @param {number} [options.offset] - Get weather in the future, offset is in hour. + * @param {number} [options.datetime] - Get weather at a specified time in the future, time is a timestamp in seconds. + * @param {string} [options.mode] - Get display mode to return [basic, advanced]. + * @param {string} [options.target] - Get time target for result [currently, hourly, daily]. * @param {string} [options.language] - The language of the report. - * @param {string} [options.units] - Units of the weather [auto, si, us]. + * @param {string} [options.units] - Unit of the weather [auto, si, us]. * @example - * gladys.weather.get({ + * gladys.services.darksky.weather.get({ * latitude: 112, * longitude: -2, * offset: 0, + * datetime: 1562703427, * language: 'fr', - * units: 'si' + * units: 'si', + * mode: 'basic' * }); */ function get(options) { diff --git a/server/models/dashboard.js b/server/models/dashboard.js index c98e81d0bf..02a64138c0 100644 --- a/server/models/dashboard.js +++ b/server/models/dashboard.js @@ -12,6 +12,7 @@ const boxesSchema = Joi.array().items( room: Joi.string(), camera: Joi.string(), name: Joi.string(), + mode: Joi.string(), }), ), ); diff --git a/server/package-lock.json b/server/package-lock.json index 76ecf23d18..d2ae2d9af4 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -256,11 +256,23 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, "@types/geojson": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", "integrity": "sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w==" }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, "@types/node": { "version": "10.12.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.21.tgz", @@ -2060,6 +2072,11 @@ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" }, + "dayjs": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.15.tgz", + "integrity": "sha512-HYHCI1nohG52B45vCQg8Re3hNDZbMroWPkhz50yaX7Lu0ATyjGsTdoYZBpjED9ar6chqTx2dmSmM8A51mojnAg==" + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", diff --git a/server/package.json b/server/package.json index 992fcbdbd2..ccdc4e5419 100644 --- a/server/package.json +++ b/server/package.json @@ -41,6 +41,8 @@ ] }, "devDependencies": { + "@types/chai": "^4.1.7", + "@types/mocha": "^5.2.7", "apidoc": "^0.17.7", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", @@ -71,6 +73,7 @@ "bluebird": "^3.5.3", "compression": "^1.7.4", "cross-env": "^5.2.0", + "dayjs": "^1.8.15", "dockerode": "^2.5.8", "express": "^4.16.4", "express-rate-limit": "^4.0.3", @@ -80,6 +83,7 @@ "handlebars": "^4.1.0", "joi": "^14.3.1", "jsonwebtoken": "^8.4.0", + "moment": "^2.24.0", "node-nlp": "^3.0.3", "node-webcrypto-ossl": "^1.0.48", "path-to-regexp": "^3.0.0", diff --git a/server/services/darksky/index.js b/server/services/darksky/index.js index 4d8324911c..9dcfde1849 100644 --- a/server/services/darksky/index.js +++ b/server/services/darksky/index.js @@ -8,6 +8,7 @@ const DARKSKY_API_KEY = 'DARKSKY_API_KEY'; module.exports = function DarkSkyService(gladys, serviceId) { const { default: axios } = require('axios'); + const moment = require('moment'); let darkSkyApiKey; /** @@ -39,7 +40,10 @@ module.exports = function DarkSkyService(gladys, serviceId) { * @param {Object} options - Options parameters. * @param {number} options.latitude - The latitude to get the weather from. * @param {number} options.longitude - The longitude to get the weather from. - * @param {number} options.offset - Get weather in the future, offset is in hour. + * @param {number} [options.offset] - Get weather in the future, offset is in hour. + * @param {number} [options.datetime] - Get weather at a specified time in the future, time is a timestamp in seconds. + * @param {string} [options.mode] - Get display mode to return [basic, advanced]. + * @param {string} [options.target] - Get time target for result [currently, hourly, daily]. * @param {string} [options.language] - The language of the report. * @param {string} [options.units] - Unit of the weather [auto, si, us]. * @example @@ -47,8 +51,10 @@ module.exports = function DarkSkyService(gladys, serviceId) { * latitude: 112, * longitude: -2, * offset: 0, + * datetime: 1562703427, * language: 'fr', - * units: 'si' + * units: 'si', + * mode: 'basic' * }); */ async function get(options) { @@ -56,14 +62,24 @@ module.exports = function DarkSkyService(gladys, serviceId) { language: 'en', units: 'si', offset: 0, + target: 'currently', }; - const optionsMerged = Object.assign({}, DEFAULT, options); - const { latitude, longitude, language, units } = optionsMerged; if (!darkSkyApiKey) { throw new ServiceNotConfiguredError('Dark Sky API Key not found'); } - const url = `https://api.darksky.net/forecast/${darkSkyApiKey}/${latitude},${longitude}?language=${language}&units=${units}`; + const optionsMerged = Object.assign({}, DEFAULT, options); + const { latitude, longitude, language, units, offset, datetime } = optionsMerged; + let timestamp = ''; + if (offset !== 0) { + timestamp = `, ${moment() + .add(offset, 'hours') + .unix()}`; + } + if (datetime) { + timestamp = `,${datetime}`; + } + const url = `https://api.darksky.net/forecast/${darkSkyApiKey}/${latitude},${longitude}${timestamp}?language=${language}&units=${units}`; try { const { data } = await axios.get(url); const weatherFormatted = formatResults(optionsMerged, data); diff --git a/server/services/darksky/lib/formatResults.js b/server/services/darksky/lib/formatResults.js index ffa9da9653..d342259187 100644 --- a/server/services/darksky/lib/formatResults.js +++ b/server/services/darksky/lib/formatResults.js @@ -1,3 +1,41 @@ +/** + * @description Transform DarkSky Icon to weather icon and summary. + * @param {string} weather - The weather icon. + * @param {Date} [datetime] - Date in timestamp format. + * @param {Date} [sunrise] - Sunrise datetime in timestamp format. + * @param {Date} [sunset] - Sunset datetime in timestamp format. + * @returns {Object} Return an icon and a summary. + * @example + * const {icon, summary} = translateIconToWeather(darkskyIcon); + */ +const translateIconToWeather = (weather, datetime = null, sunrise = null, sunset = null) => { + if (weather.search('cloud') !== -1) { + return { icon: 'cloud', summary: 'cloudy' }; + } + if (weather.search('night') !== -1 && (datetime && sunrise && sunset && (datetime < sunrise || datetime > sunset))) { + return { icon: 'night', summary: 'clear night' }; + } + if (weather.search('rain') !== -1) { + return { icon: 'rain', summary: 'raining' }; + } + if (weather.search('clear') !== -1) { + return { icon: 'clear', summary: 'clear sky' }; + } + if (weather.search('snow') !== -1) { + return { icon: 'snow', summary: 'snowing' }; + } + if (weather.search('fog') !== -1) { + return { icon: 'fog', summary: 'floggy' }; + } + if (weather.search('sleet') !== -1) { + return { icon: 'sleet', summary: 'sleety' }; + } + if (weather.search('wind') !== -1) { + return { icon: 'wind', summary: 'windy' }; + } + return { icon: 'unknown', summary: 'unknown' }; +}; + /** * @description Transform DarkSky JSON to Gladys data. * @param {Object} options - The weather call options. @@ -6,61 +44,74 @@ * @example * const formatted = formatResults(options, result); */ -function formatResults(options, result) { +const formatResults = (options, result) => { const dataToReturn = {}; - let dataPoint = null; - - // if options.offset == 0, it means it's now - if (options.offset === 0) { - dataPoint = result.currently; - - // if options.offset < 24, we take in the hour response - } else if (options.offset < 24) { - if (result.hourly.data.length > options.offset) { - dataPoint = result.hourly.data[options.offset]; - } else { - dataPoint = result.currently; + let dataPoint = result.currently; + if (options.target === 'currently') { + dataToReturn.temperature = Number(dataPoint.temperature.toFixed(2)); + } else if (options.target === 'hourly') { + dataToReturn.temperature = Math.round(dataPoint.temperature); + } else if (options.target === 'daily') { + [dataPoint] = result.daily.data; + if (dataPoint.temperatureMin) { + dataToReturn.temperatureMin = Math.round(dataPoint.temperatureMin); } - - // else if options.offset > 24, we take in the daily response - } else { - // we transform options.offset in day count - options.offset = Math.round(options.offset / 24); - - if (result.daily.data.length > options.offset) { - dataPoint = result.daily.data[options.offset]; - } else { - dataPoint = result.daily.data[result.daily.data.length - 1]; + if (dataPoint.temperatureMax) { + dataToReturn.temperatureMax = Math.round(dataPoint.temperatureMax); } } - dataToReturn.temperature = dataPoint.temperature; - dataToReturn.humidity = dataPoint.humidity; - dataToReturn.pressure = dataPoint.pressure; dataToReturn.datetime = new Date(dataPoint.time * 1000); dataToReturn.units = options.units; - dataToReturn.wind_speed = dataPoint.windSpeed; + dataToReturn.time_sunrise = new Date(result.daily.data[0].sunriseTime * 1000); + dataToReturn.time_sunset = new Date(result.daily.data[0].sunsetTime * 1000); - if (dataPoint.icon.search('snow') !== -1) { - dataToReturn.weather = 'snow'; - } else if (dataPoint.icon.search('rain') !== -1) { - dataToReturn.weather = 'rain'; - } else if (dataPoint.icon.search('clear') !== -1) { - dataToReturn.weather = 'clear'; - } else if (dataPoint.icon.search('cloud') !== -1) { - dataToReturn.weather = 'cloud'; - } else if (dataPoint.icon.search('fog') !== -1) { - dataToReturn.weather = 'fog'; - } else if (dataPoint.icon.search('sleet') !== -1) { - dataToReturn.weather = 'sleet'; - } else if (dataPoint.icon.search('wind') !== -1) { - dataToReturn.weather = 'wind'; - } else { - dataToReturn.weather = 'unknown'; - } + const { icon, summary } = translateIconToWeather( + dataPoint.icon, + dataToReturn.datetime, + dataToReturn.time_sunrise, + dataToReturn.time_sunset, + ); + dataToReturn.weather = icon; + dataToReturn.summary = summary; + if (options.mode && options.mode === 'advanced') { + dataToReturn.humidity = Math.round(dataPoint.humidity * 100); + dataToReturn.wind_speed = Number(dataPoint.windSpeed.toFixed(2)); + dataToReturn.alert = null; + + if (result.alerts) { + dataToReturn.alert = { + title: result.alerts[0].title, + description: result.alerts[0].description, + severity: result.alerts[0].severity, + }; + } + + if (result.hourly) { + const dataHours = result.hourly.data; + dataToReturn.hours = []; + for (let i = 1; i < 13; i += 1) { + if (i < 5 || i % 2 === 0) { + dataToReturn.hours.push({ + datetime: new Date(dataHours[i].time * 1000), + summary: dataHours[i].summary, + weather: translateIconToWeather( + dataHours[i].icon, + new Date(dataHours[i].time * 1000), + dataToReturn.time_sunrise, + dataToReturn.time_sunset, + ).icon, + temperature: Math.round(dataHours[i].temperature), + precipitation_type: dataHours[i].precipType, + precipitation_probability: Math.round(dataHours[i].precipProbability * 100), + }); + } + } + } + } return dataToReturn; -} +}; module.exports = { formatResults, diff --git a/server/services/darksky/package-lock.json b/server/services/darksky/package-lock.json index c833a3856a..328eaeddf4 100644 --- a/server/services/darksky/package-lock.json +++ b/server/services/darksky/package-lock.json @@ -33,6 +33,11 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", diff --git a/server/services/darksky/package.json b/server/services/darksky/package.json index e0f331b8ef..e599c4358a 100644 --- a/server/services/darksky/package.json +++ b/server/services/darksky/package.json @@ -13,6 +13,7 @@ ], "scripts": {}, "dependencies": { - "axios": "^0.18.0" + "axios": "^0.18.0", + "moment": "^2.24.0" } } diff --git a/server/test/services/darksky/darksky.test.js b/server/test/services/darksky/darksky.test.js new file mode 100644 index 0000000000..de9c7e562f --- /dev/null +++ b/server/test/services/darksky/darksky.test.js @@ -0,0 +1,262 @@ +const { expect, assert } = require('chai'); +const proxyquire = require('proxyquire').noCallThru(); +const weatherExample = require('./weather.json'); + +const workingAxios = { + axios: { + default: { + get: () => ({ data: weatherExample }), + }, + }, +}; + +const brokenAxios = { + axios: { + default: { + get: () => Promise.reject(new Error('broken')), + }, + }, +}; + +const gladys = { + variable: { + getValue: () => Promise.resolve('DARK_SKY_API_KEY'), + }, +}; + +describe('DarkSkyService', () => { + it('should start service', async () => { + const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); + const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); + await darkSkyService.start(); + }); + it('should stop service', async () => { + const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); + const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); + await darkSkyService.stop(); + }); + it('should return weather formatted without any mode specified', async () => { + const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); + const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); + await darkSkyService.start(); + const weather = await darkSkyService.weather.get({ + latitude: 12, + longitude: 10, + }); + expect(weather).to.deep.equal({ + datetime: new Date('2019-03-28T07:50:18.000Z'), + temperature: 54.87, + time_sunrise: new Date('2019-03-28T14:02:00.000Z'), + time_sunset: new Date('2019-03-29T02:29:43.000Z'), + units: 'si', + summary: 'cloudy', + weather: 'cloud', + }); + }); + it('should return weather formatted in basic mode', async () => { + const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); + const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); + await darkSkyService.start(); + const weather = await darkSkyService.weather.get({ + latitude: 12, + longitude: 10, + mode: 'basic', + }); + expect(weather).to.deep.equal({ + datetime: new Date('2019-03-28T07:50:18.000Z'), + temperature: 54.87, + time_sunrise: new Date('2019-03-28T14:02:00.000Z'), + time_sunset: new Date('2019-03-29T02:29:43.000Z'), + units: 'si', + summary: 'cloudy', + weather: 'cloud', + }); + }); + it('should return weather formatted in basic mode for a specific datetime', async () => { + const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); + const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); + await darkSkyService.start(); + const weather = await darkSkyService.weather.get({ + latitude: 12, + longitude: 10, + mode: 'basic', + datetime: '1562861076', + }); + expect(weather).to.deep.equal({ + datetime: new Date('2019-03-28T07:50:18.000Z'), + temperature: 54.87, + time_sunrise: new Date('2019-03-28T14:02:00.000Z'), + time_sunset: new Date('2019-03-29T02:29:43.000Z'), + units: 'si', + summary: 'cloudy', + weather: 'cloud', + }); + }); + it('should return weather formatted in basic mode with an offset', async () => { + const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); + const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); + await darkSkyService.start(); + const weather = await darkSkyService.weather.get({ + latitude: 12, + longitude: 10, + mode: 'basic', + offset: 2, + }); + expect(weather).to.deep.equal({ + datetime: new Date('2019-03-28T07:50:18.000Z'), + temperature: 54.87, + time_sunrise: new Date('2019-03-28T14:02:00.000Z'), + time_sunset: new Date('2019-03-29T02:29:43.000Z'), + units: 'si', + summary: 'cloudy', + weather: 'cloud', + }); + }); + + it('should return weather formatted for daily target', async () => { + const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); + const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); + await darkSkyService.start(); + const weather = await darkSkyService.weather.get({ + latitude: 12, + longitude: 10, + mode: 'basic', + datetime: '1562861076', + target: 'daily', + }); + expect(weather).to.deep.equal({ + datetime: new Date('2019-03-28T07:00:00.000Z'), + temperatureMin: 52, + temperatureMax: 58, + time_sunrise: new Date('2019-03-28T14:02:00.000Z'), + time_sunset: new Date('2019-03-29T02:29:43.000Z'), + units: 'si', + summary: 'cloudy', + weather: 'cloud', + }); + }); + it('should return weather formatted for hourly target', async () => { + const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); + const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); + await darkSkyService.start(); + const weather = await darkSkyService.weather.get({ + latitude: 12, + longitude: 10, + mode: 'basic', + datetime: '1562861076', + target: 'hourly', + }); + expect(weather).to.deep.equal({ + datetime: new Date('2019-03-28T07:50:18.000Z'), + temperature: 55, + time_sunrise: new Date('2019-03-28T14:02:00.000Z'), + time_sunset: new Date('2019-03-29T02:29:43.000Z'), + units: 'si', + summary: 'cloudy', + weather: 'cloud', + }); + }); + it('should return weather formatted in advanced mode', async () => { + const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); + const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); + await darkSkyService.start(); + const weather = await darkSkyService.weather.get({ + latitude: 12, + longitude: 10, + mode: 'advanced', + }); + expect(weather).to.deep.equal({ + datetime: new Date('2019-03-28T07:50:18.000Z'), + hours: [ + { + temperature: 55, + datetime: new Date('2019-03-28T08:00:00.000Z'), + precipitation_probability: 11, + precipitation_type: 'rain', + summary: 'Partly Cloudy', + weather: 'rain', + }, + { + temperature: 54, + datetime: new Date('2019-03-28T09:00:00.000Z'), + precipitation_probability: 7, + precipitation_type: 'rain', + summary: 'Partly Cloudy', + weather: 'clear', + }, + { + temperature: 53, + datetime: new Date('2019-03-28T10:00:00.000Z'), + precipitation_probability: 9, + precipitation_type: 'rain', + summary: 'Partly Cloudy', + weather: 'snow', + }, + { + temperature: 52, + datetime: new Date('2019-03-28T11:00:00.000Z'), + precipitation_probability: 25, + precipitation_type: 'rain', + summary: 'Possible Light Rain', + weather: 'wind', + }, + { + temperature: 52, + datetime: new Date('2019-03-28T13:00:00.000Z'), + precipitation_probability: 19, + precipitation_type: 'rain', + summary: 'Mostly Cloudy', + weather: 'sleet', + }, + { + temperature: 52, + datetime: new Date('2019-03-28T15:00:00.000Z'), + precipitation_probability: 19, + precipitation_type: 'rain', + summary: 'Mostly Cloudy', + weather: 'clear', + }, + { + temperature: 54, + datetime: new Date('2019-03-28T17:00:00.000Z'), + precipitation_probability: 27, + precipitation_type: 'rain', + summary: 'Mostly Cloudy', + weather: 'unknown', + }, + { + temperature: 56, + datetime: new Date('2019-03-28T19:00:00.000Z'), + precipitation_probability: 21, + precipitation_type: 'rain', + summary: 'Mostly Cloudy', + weather: 'fog', + }, + ], + alert: { + description: + '...FLOOD WATCH REMAINS IN EFFECT THROUGH LATE MONDAY NIGHT...\nTHE FLOOD WATCH CONTINUES FOR\n* A PORTION OF NORTHWEST WASHINGTON...INCLUDING THE FOLLOWING\nCOUNTY...MASON.\n* THROUGH LATE FRIDAY NIGHT\n* A STRONG WARM FRONT WILL BRING HEAVY RAIN TO THE OLYMPICS\nTONIGHT THROUGH THURSDAY NIGHT. THE HEAVY RAIN WILL PUSH THE\nSKOKOMISH RIVER ABOVE FLOOD STAGE TODAY...AND MAJOR FLOODING IS\nPOSSIBLE.\n* A FLOOD WARNING IS IN EFFECT FOR THE SKOKOMISH RIVER. THE FLOOD\nWATCH REMAINS IN EFFECT FOR MASON COUNTY FOR THE POSSIBILITY OF\nAREAL FLOODING ASSOCIATED WITH A MAJOR FLOOD.\n', + severity: 'warning', + title: 'Flood Watch for Mason, WA', + }, + temperature: 54.87, + humidity: 76, + time_sunrise: new Date('2019-03-28T14:02:00.000Z'), + time_sunset: new Date('2019-03-29T02:29:43.000Z'), + units: 'si', + wind_speed: 5.25, + summary: 'cloudy', + weather: 'cloud', + }); + }); + it('should return error, unable to contact third party provider', async () => { + const DarkSkyService = proxyquire('../../../services/darksky/index', brokenAxios); + const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); + await darkSkyService.start(); + const promise = darkSkyService.weather.get({ + latitude: 12, + longitude: 10, + }); + return assert.isRejected(promise, 'REQUEST_TO_THIRD_PARTY_FAILED'); + }); +}); diff --git a/server/test/services/darksky/darsky.test.js b/server/test/services/darksky/darsky.test.js deleted file mode 100644 index b94ba7ff25..0000000000 --- a/server/test/services/darksky/darsky.test.js +++ /dev/null @@ -1,66 +0,0 @@ -const { expect, assert } = require('chai'); -const proxyquire = require('proxyquire').noCallThru(); -const weatherExample = require('./weather.json'); - -const workingAxios = { - axios: { - default: { - get: () => ({ data: weatherExample }), - }, - }, -}; - -const brokenAxios = { - axios: { - default: { - get: () => Promise.reject(new Error('broken')), - }, - }, -}; - -const gladys = { - variable: { - getValue: () => Promise.resolve('DARK_SKY_API_KEY'), - }, -}; - -describe('DarkSkyService', () => { - it('should start service', async () => { - const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); - const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); - await darkSkyService.start(); - }); - it('should stop service', async () => { - const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); - const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); - await darkSkyService.stop(); - }); - it('should return weather formatted', async () => { - const DarkSkyService = proxyquire('../../../services/darksky/index', workingAxios); - const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); - await darkSkyService.start(); - const weather = await darkSkyService.weather.get({ - latitude: 12, - longitude: 10, - }); - expect(weather).to.deep.equal({ - temperature: 54.87, - humidity: 0.76, - pressure: 1019.4, - datetime: new Date('2019-03-28T07:50:18.000Z'), - units: 'si', - wind_speed: 5.25, - weather: 'cloud', - }); - }); - it('should return error, unable to contact third party provider', async () => { - const DarkSkyService = proxyquire('../../../services/darksky/index', brokenAxios); - const darkSkyService = DarkSkyService(gladys, '35deac79-f295-4adf-8512-f2f48e1ea0f8'); - await darkSkyService.start(); - const promise = darkSkyService.weather.get({ - latitude: 12, - longitude: 10, - }); - return assert.isRejected(promise, 'REQUEST_TO_THIRD_PARTY_FAILED'); - }); -}); diff --git a/server/test/services/darksky/weather.json b/server/test/services/darksky/weather.json index 07d806c3ac..589fd3e991 100644 --- a/server/test/services/darksky/weather.json +++ b/server/test/services/darksky/weather.json @@ -447,7 +447,7 @@ { "time": 1553756400, "summary": "Partly Cloudy", - "icon": "partly-cloudy-night", + "icon": "clear-night", "precipIntensity": 0, "precipProbability": 0, "temperature": 55.4, @@ -466,7 +466,7 @@ { "time": 1553760000, "summary": "Partly Cloudy", - "icon": "partly-cloudy-night", + "icon": "rain", "precipIntensity": 0.0024, "precipProbability": 0.11, "precipType": "rain", @@ -486,7 +486,7 @@ { "time": 1553763600, "summary": "Partly Cloudy", - "icon": "partly-cloudy-night", + "icon": "clear-day", "precipIntensity": 0.0016, "precipProbability": 0.07, "precipType": "rain", @@ -506,7 +506,7 @@ { "time": 1553767200, "summary": "Partly Cloudy", - "icon": "partly-cloudy-night", + "icon": "snow", "precipIntensity": 0.0013, "precipProbability": 0.09, "precipType": "rain", @@ -526,7 +526,7 @@ { "time": 1553770800, "summary": "Possible Light Rain", - "icon": "rain", + "icon": "wind", "precipIntensity": 0.0355, "precipProbability": 0.25, "precipType": "rain", @@ -546,7 +546,7 @@ { "time": 1553774400, "summary": "Mostly Cloudy", - "icon": "partly-cloudy-night", + "icon": "fog", "precipIntensity": 0.0059, "precipProbability": 0.23, "precipType": "rain", @@ -566,7 +566,7 @@ { "time": 1553778000, "summary": "Mostly Cloudy", - "icon": "partly-cloudy-night", + "icon": "sleet", "precipIntensity": 0.004, "precipProbability": 0.19, "precipType": "rain", @@ -586,7 +586,7 @@ { "time": 1553781600, "summary": "Overcast", - "icon": "cloudy", + "icon": "sleet", "precipIntensity": 0.004, "precipProbability": 0.18, "precipType": "rain", @@ -606,7 +606,7 @@ { "time": 1553785200, "summary": "Mostly Cloudy", - "icon": "partly-cloudy-day", + "icon": "clear-night", "precipIntensity": 0.0047, "precipProbability": 0.19, "precipType": "rain", @@ -626,7 +626,7 @@ { "time": 1553788800, "summary": "Mostly Cloudy", - "icon": "partly-cloudy-day", + "icon": "wind", "precipIntensity": 0.0045, "precipProbability": 0.19, "precipType": "rain", @@ -646,7 +646,7 @@ { "time": 1553792400, "summary": "Mostly Cloudy", - "icon": "partly-cloudy-day", + "icon": "test", "precipIntensity": 0.016, "precipProbability": 0.27, "precipType": "rain", @@ -686,7 +686,7 @@ { "time": 1553799600, "summary": "Mostly Cloudy", - "icon": "partly-cloudy-day", + "icon": "fog", "precipIntensity": 0.0061, "precipProbability": 0.21, "precipType": "rain", @@ -1746,6 +1746,24 @@ } ] }, + "alerts": [ + { + "title": "Flood Watch for Mason, WA", + "time": 1509993360, + "expires": 1510036680, + "severity": "warning", + "description": "...FLOOD WATCH REMAINS IN EFFECT THROUGH LATE MONDAY NIGHT...\nTHE FLOOD WATCH CONTINUES FOR\n* A PORTION OF NORTHWEST WASHINGTON...INCLUDING THE FOLLOWING\nCOUNTY...MASON.\n* THROUGH LATE FRIDAY NIGHT\n* A STRONG WARM FRONT WILL BRING HEAVY RAIN TO THE OLYMPICS\nTONIGHT THROUGH THURSDAY NIGHT. THE HEAVY RAIN WILL PUSH THE\nSKOKOMISH RIVER ABOVE FLOOD STAGE TODAY...AND MAJOR FLOODING IS\nPOSSIBLE.\n* A FLOOD WARNING IS IN EFFECT FOR THE SKOKOMISH RIVER. THE FLOOD\nWATCH REMAINS IN EFFECT FOR MASON COUNTY FOR THE POSSIBILITY OF\nAREAL FLOODING ASSOCIATED WITH A MAJOR FLOOD.\n", + "uri": "http://alerts.weather.gov/cap/wwacapget.php?x=WA1255E4DB8494.FloodWatch.1255E4DCE35CWA.SEWFFASEW.38e78ec64613478bb70fc6ed9c87f6e6" + }, + { + "title": "(2) Flood Watch for Mason, WA", + "time": 1509993360, + "expires": 1510036680, + "severity": "watch", + "description": "(2)...FLOOD WATCH REMAINS IN EFFECT THROUGH LATE MONDAY NIGHT...\nTHE FLOOD WATCH CONTINUES FOR\n* A PORTION OF NORTHWEST WASHINGTON...INCLUDING THE FOLLOWING\nCOUNTY...MASON.\n* THROUGH LATE FRIDAY NIGHT\n* A STRONG WARM FRONT WILL BRING HEAVY RAIN TO THE OLYMPICS\nTONIGHT THROUGH THURSDAY NIGHT. THE HEAVY RAIN WILL PUSH THE\nSKOKOMISH RIVER ABOVE FLOOD STAGE TODAY...AND MAJOR FLOODING IS\nPOSSIBLE.\n* A FLOOD WARNING IS IN EFFECT FOR THE SKOKOMISH RIVER. THE FLOOD\nWATCH REMAINS IN EFFECT FOR MASON COUNTY FOR THE POSSIBILITY OF\nAREAL FLOODING ASSOCIATED WITH A MAJOR FLOOD.\n", + "uri": "http://alerts.weather.gov/cap/wwacapget.php?x=WA1255E4DB8494.FloodWatch.1255E4DCE35CWA.SEWFFASEW.38e78ec64613478bb70fc6ed9c87f6e6" + } + ], "flags": { "sources": ["nearest-precip", "nwspa", "cmc", "gfs", "hrrr", "icon", "isd", "madis", "nam", "sref", "darksky"], "nearest-station": 1.839, diff --git a/server/utils/constants.js b/server/utils/constants.js index c12aa67439..ee268fb309 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -193,6 +193,7 @@ const INTENTS = { }, WEATHER: { GET: 'intent.weather.get', + GET_PREVISIONS: 'intent.weather.getPrevisions', }, CAMERA: { GET_IMAGE_ROOM: 'intent.camera.get-image-room',