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 }) => (
))}
+
);
@@ -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',