diff --git a/front/cypress/integration/routes/dashboard/DashboardHumidityBox.js b/front/cypress/integration/routes/dashboard/DashboardHumidityBox.js new file mode 100644 index 0000000000..b0e5e35ae5 --- /dev/null +++ b/front/cypress/integration/routes/dashboard/DashboardHumidityBox.js @@ -0,0 +1,148 @@ +describe('Dashboard Humidity Box', () => { + beforeEach(() => { + cy.login(); + + // Create dashboard + const serverUrl = Cypress.env('serverUrl'); + const { rooms } = Cypress.env('house'); + const roomSelector = rooms[0].selector; + cy.request({ + method: 'POST', + url: `${serverUrl}/api/v1/dashboard`, + body: { + name: 'Test', + type: 'test', + selector: 'test', + boxes: [ + [], + [ + { + type: 'humidity-in-room', + room: roomSelector + } + ], + [] + ] + } + }); + + cy.request({ + method: 'GET', + url: `${serverUrl}/api/v1/room/${roomSelector}` + }).then(res => { + // Create humidity device in room + const device1 = { + name: 'First device', + external_id: 'first-device', + selector: 'first-device', + room_id: res.body.id, + features: [ + { + name: 'Temp sensor', + category: 'humidity-sensor', + type: 'decimal', + external_id: 'first-humidity', + selector: 'first-humidity', + read_only: true, + keep_history: true, + has_feedback: false, + min: -50, + max: 100 + } + ] + }; + cy.createDevice(device1, 'example'); + }); + + // Create another humidity device without room + const otherRoomDevice = { + name: 'Second device', + external_id: 'second-device', + selector: 'second-device', + features: [ + { + name: 'Temp sensor', + category: 'humidity-sensor', + type: 'decimal', + external_id: 'second-humidity', + selector: 'second-humidity', + read_only: true, + keep_history: true, + has_feedback: false, + min: -50, + max: 100 + } + ] + }; + cy.createDevice(otherRoomDevice, 'example'); + + cy.intercept({ + method: 'GET', + url: `${serverUrl}/api/v1/dashboard/test` + }).as('loadDashboard'); + + cy.intercept({ + method: 'GET', + url: `${serverUrl}/api/v1/room/${roomSelector}?expand=humidity,devices` + }).as('loadBox'); + + cy.visit('/dashboard/test'); + + cy.wait('@loadDashboard'); + cy.wait('@loadBox'); + }); + afterEach(() => { + // Delete all devices + cy.deleteDevices('example'); + // Delete dashboard + const serverUrl = Cypress.env('serverUrl'); + cy.request({ + method: 'DELETE', + url: `${serverUrl}/api/v1/dashboard/test` + }); + }); + it('Should have no value box', () => { + cy.contains('p', 'dashboard.boxes.humidityInRoom.noHumidityRecorded').should('exist'); + + const { rooms } = Cypress.env('house'); + const roomName = rooms[0].name; + cy.contains('small', roomName).should('exist'); + }); + + it('Should update humidity on update device value', () => { + const serverUrl = Cypress.env('serverUrl'); + const { rooms } = Cypress.env('house'); + const roomSelector = rooms[0].selector; + + cy.intercept({ + method: 'GET', + url: `${serverUrl}/api/v1/room/${roomSelector}?expand=humidity,devices` + }).as('reloadBox'); + + cy.request({ + method: 'POST', + url: `${serverUrl}/api/v1/device/first-device/humidity-sensor/decimal/value`, + body: { value: 24 } + }); + + cy.sendWebSocket({ type: 'device.new-state', payload: { device_feature_selector: 'first-humidity' } }); + + cy.wait('@reloadBox'); + + cy.contains('h4', '24%').should('exist'); + }); + + it('Should not update humidity on update device value', () => { + const serverUrl = Cypress.env('serverUrl'); + + cy.request({ + method: 'POST', + url: `${serverUrl}/api/v1/device/first-device/humidity-sensor/decimal/value`, + body: { value: 24 } + }); + + cy.sendWebSocket({ type: 'device.new-state', payload: { device_feature_selector: 'second-humidity' } }); + + cy.contains('p', 'dashboard.boxes.humidityInRoom.noHumidityRecorded').should('exist'); + }); +}); diff --git a/front/cypress/integration/routes/dashboard/DashboardTemperatureBox.js b/front/cypress/integration/routes/dashboard/DashboardTemperatureBox.js new file mode 100644 index 0000000000..6a69820307 --- /dev/null +++ b/front/cypress/integration/routes/dashboard/DashboardTemperatureBox.js @@ -0,0 +1,150 @@ +describe('Dashboard Temperature Box', () => { + beforeEach(() => { + cy.login(); + + // Create dashboard + const serverUrl = Cypress.env('serverUrl'); + const { rooms } = Cypress.env('house'); + const roomSelector = rooms[0].selector; + cy.request({ + method: 'POST', + url: `${serverUrl}/api/v1/dashboard`, + body: { + name: 'Test', + type: 'test', + selector: 'test', + boxes: [ + [], + [ + { + type: 'temperature-in-room', + room: roomSelector + } + ], + [] + ] + } + }); + + cy.request({ + method: 'GET', + url: `${serverUrl}/api/v1/room/${roomSelector}` + }).then(res => { + // Create temperature device in room + const device1 = { + name: 'First device', + external_id: 'first-device', + selector: 'first-device', + room_id: res.body.id, + features: [ + { + name: 'Temp sensor', + category: 'temperature-sensor', + type: 'decimal', + external_id: 'first-temperature', + selector: 'first-temperature', + unit: 'celsius', + read_only: true, + keep_history: true, + has_feedback: false, + min: -50, + max: 100 + } + ] + }; + cy.createDevice(device1, 'example'); + }); + + // Create another temperature device without room + const otherRoomDevice = { + name: 'Second device', + external_id: 'second-device', + selector: 'second-device', + features: [ + { + name: 'Temp sensor', + category: 'temperature-sensor', + type: 'decimal', + external_id: 'second-temperature', + selector: 'second-temperature', + unit: 'celsius', + read_only: true, + keep_history: true, + has_feedback: false, + min: -50, + max: 100 + } + ] + }; + cy.createDevice(otherRoomDevice, 'example'); + + cy.intercept({ + method: 'GET', + url: `${serverUrl}/api/v1/dashboard/test` + }).as('loadDashboard'); + + cy.intercept({ + method: 'GET', + url: `${serverUrl}/api/v1/room/${roomSelector}?expand=temperature,devices` + }).as('loadBox'); + + cy.visit('/dashboard/test'); + + cy.wait('@loadDashboard'); + cy.wait('@loadBox'); + }); + afterEach(() => { + // Delete all devices + cy.deleteDevices('example'); + // Delete dashboard + const serverUrl = Cypress.env('serverUrl'); + cy.request({ + method: 'DELETE', + url: `${serverUrl}/api/v1/dashboard/test` + }); + }); + it('Should have no value box', () => { + cy.contains('p', 'dashboard.boxes.temperatureInRoom.noTemperatureRecorded').should('exist'); + + const { rooms } = Cypress.env('house'); + const roomName = rooms[0].name; + cy.contains('small', roomName).should('exist'); + }); + + it('Should update temperature on update device value', () => { + const serverUrl = Cypress.env('serverUrl'); + const { rooms } = Cypress.env('house'); + const roomSelector = rooms[0].selector; + + cy.intercept({ + method: 'GET', + url: `${serverUrl}/api/v1/room/${roomSelector}?expand=temperature,devices` + }).as('reloadBox'); + + cy.request({ + method: 'POST', + url: `${serverUrl}/api/v1/device/first-device/temperature-sensor/decimal/value`, + body: { value: 24 } + }); + + cy.sendWebSocket({ type: 'device.new-state', payload: { device_feature_selector: 'first-temperature' } }); + + cy.wait('@reloadBox'); + + cy.contains('h4', '24°C').should('exist'); + }); + + it('Should not update temperature on update device value', () => { + const serverUrl = Cypress.env('serverUrl'); + + cy.request({ + method: 'POST', + url: `${serverUrl}/api/v1/device/first-device/temperature-sensor/decimal/value`, + body: { value: 24 } + }); + + cy.sendWebSocket({ type: 'device.new-state', payload: { device_feature_selector: 'second-temperature' } }); + + cy.contains('p', 'dashboard.boxes.temperatureInRoom.noTemperatureRecorded').should('exist'); + }); +}); diff --git a/front/src/actions/dashboard/boxes/humidityInRoom.js b/front/src/actions/dashboard/boxes/humidityInRoom.js index 4ec78953d8..395a21bbda 100644 --- a/front/src/actions/dashboard/boxes/humidityInRoom.js +++ b/front/src/actions/dashboard/boxes/humidityInRoom.js @@ -1,3 +1,5 @@ +import get from 'get-value'; + import { RequestStatus } from '../../../utils/consts'; import createBoxActions from '../boxActions'; @@ -10,7 +12,7 @@ function createActions(store) { async getHumidityInRoom(state, box, x, y) { boxActions.updateBoxStatus(state, BOX_KEY, x, y, RequestStatus.Getting); try { - const room = await state.httpClient.get(`/api/v1/room/${box.room}?expand=humidity`); + const room = await state.httpClient.get(`/api/v1/room/${box.room}?expand=humidity,devices`); boxActions.mergeBoxData(state, BOX_KEY, x, y, { room }); @@ -18,6 +20,22 @@ function createActions(store) { } catch (e) { boxActions.updateBoxStatus(state, BOX_KEY, x, y, RequestStatus.Error); } + }, + async deviceFeatureWebsocketEvent(state, box, x, y, payload) { + const data = boxActions.getBoxData(state, BOX_KEY, x, y); + const devices = get(data, 'room.devices', { default: [] }); + + // Search if feature is in room + const featureIndex = devices.findIndex(device => { + const featureIndex = device.features.findIndex(feature => feature.selector === payload.device_feature_selector); + return featureIndex !== -1; + }); + + // If feature is in room + if (featureIndex !== -1) { + // Refresh box value + this.getHumidityInRoom(box, x, y); + } } }; return Object.assign({}, actions); diff --git a/front/src/actions/dashboard/boxes/temperatureInRoom.js b/front/src/actions/dashboard/boxes/temperatureInRoom.js index ec1ba92e10..587d52332b 100644 --- a/front/src/actions/dashboard/boxes/temperatureInRoom.js +++ b/front/src/actions/dashboard/boxes/temperatureInRoom.js @@ -1,3 +1,5 @@ +import get from 'get-value'; + import { RequestStatus } from '../../../utils/consts'; import createBoxActions from '../boxActions'; @@ -10,7 +12,7 @@ function createActions(store) { async getTemperatureInRoom(state, box, x, y) { boxActions.updateBoxStatus(state, BOX_KEY, x, y, RequestStatus.Getting); try { - const room = await state.httpClient.get(`/api/v1/room/${box.room}?expand=temperature`); + const room = await state.httpClient.get(`/api/v1/room/${box.room}?expand=temperature,devices`); boxActions.mergeBoxData(state, BOX_KEY, x, y, { room }); @@ -18,6 +20,22 @@ function createActions(store) { } catch (e) { boxActions.updateBoxStatus(state, BOX_KEY, x, y, RequestStatus.Error); } + }, + async deviceFeatureWebsocketEvent(state, box, x, y, payload) { + const data = boxActions.getBoxData(state, BOX_KEY, x, y); + const devices = get(data, 'room.devices', { default: [] }); + + // Search if feature is in room + const featureIndex = devices.findIndex(device => { + const featureIndex = device.features.findIndex(feature => feature.selector === payload.device_feature_selector); + return featureIndex !== -1; + }); + + // If feature is in room + if (featureIndex !== -1) { + // Refresh box value + this.getTemperatureInRoom(box, x, y); + } } }; return Object.assign({}, actions); diff --git a/front/src/components/boxs/room-humidity/RoomHumidity.jsx b/front/src/components/boxs/room-humidity/RoomHumidity.jsx index 884b6b96ad..660fb13026 100644 --- a/front/src/components/boxs/room-humidity/RoomHumidity.jsx +++ b/front/src/components/boxs/room-humidity/RoomHumidity.jsx @@ -1,9 +1,11 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; import { Text } from 'preact-i18n'; +import get from 'get-value'; + import actions from '../../../actions/dashboard/boxes/humidityInRoom'; import { DASHBOARD_BOX_STATUS_KEY, DASHBOARD_BOX_DATA_KEY } from '../../../utils/consts'; -import get from 'get-value'; +import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../server/utils/constants'; const isNotNullOrUndefined = value => value !== undefined && value !== null; @@ -47,8 +49,13 @@ class RoomHumidityBoxComponent extends Component { this.props.getHumidityInRoom(this.props.box, this.props.x, this.props.y); }; + updateRoomHumidity = payload => { + this.props.deviceFeatureWebsocketEvent(this.props.box, this.props.x, this.props.y, payload); + }; + componentDidMount() { this.refreshData(); + this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.DEVICE.NEW_STATE, this.updateRoomHumidity); } componentDidUpdate(previousProps) { @@ -58,6 +65,10 @@ class RoomHumidityBoxComponent extends Component { } } + componentWillUnmount() { + this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.DEVICE.NEW_STATE, this.updateRoomHumidity); + } + render(props, {}) { const boxData = get(props, `${DASHBOARD_BOX_DATA_KEY}HumidityInRoom.${props.x}_${props.y}`); const boxStatus = get(props, `${DASHBOARD_BOX_STATUS_KEY}HumidityInRoom.${props.x}_${props.y}`); @@ -69,6 +80,6 @@ class RoomHumidityBoxComponent extends Component { } export default connect( - 'DashboardBoxDataHumidityInRoom,DashboardBoxStatusHumidityInRoom', + 'session,DashboardBoxDataHumidityInRoom,DashboardBoxStatusHumidityInRoom', actions )(RoomHumidityBoxComponent); diff --git a/front/src/components/boxs/room-temperature/RoomTemperature.jsx b/front/src/components/boxs/room-temperature/RoomTemperature.jsx index 5c376a3623..4ce184c09b 100644 --- a/front/src/components/boxs/room-temperature/RoomTemperature.jsx +++ b/front/src/components/boxs/room-temperature/RoomTemperature.jsx @@ -1,9 +1,11 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; import { Text } from 'preact-i18n'; +import get from 'get-value'; + import actions from '../../../actions/dashboard/boxes/temperatureInRoom'; import { DASHBOARD_BOX_STATUS_KEY, DASHBOARD_BOX_DATA_KEY } from '../../../utils/consts'; -import get from 'get-value'; +import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../server/utils/constants'; const isNotNullOrUndefined = value => value !== undefined && value !== null; @@ -14,13 +16,13 @@ const RoomTemperatureBox = ({ children, ...props }) => (
- {isNotNullOrUndefined(props.temperature) && ( + {props.valued && (

- {props.unit === 'celsius' ? 'C' : 'F'} +

)} - {!isNotNullOrUndefined(props.temperature) && ( + {!props.valued && (

@@ -36,8 +38,13 @@ class RoomTemperatureBoxComponent extends Component { this.props.getTemperatureInRoom(this.props.box, this.props.x, this.props.y); }; + updateRoomTemperature = payload => { + this.props.deviceFeatureWebsocketEvent(this.props.box, this.props.x, this.props.y, payload); + }; + componentDidMount() { this.refreshData(); + this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.DEVICE.NEW_STATE, this.updateRoomTemperature); } componentDidUpdate(previousProps) { @@ -47,19 +54,32 @@ class RoomTemperatureBoxComponent extends Component { } } + componentWillUnmount() { + this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.DEVICE.NEW_STATE, this.updateRoomTemperature); + } + render(props, {}) { const boxData = get(props, `${DASHBOARD_BOX_DATA_KEY}TemperatureInRoom.${props.x}_${props.y}`); const boxStatus = get(props, `${DASHBOARD_BOX_STATUS_KEY}TemperatureInRoom.${props.x}_${props.y}`); const temperature = get(boxData, 'room.temperature.temperature'); const unit = get(boxData, 'room.temperature.unit'); const roomName = get(boxData, 'room.name'); + const valued = isNotNullOrUndefined(temperature); + return ( - + ); } } export default connect( - 'DashboardBoxDataTemperatureInRoom,DashboardBoxStatusTemperatureInRoom', + 'session,DashboardBoxDataTemperatureInRoom,DashboardBoxStatusTemperatureInRoom', actions )(RoomTemperatureBoxComponent); diff --git a/server/services/example/index.js b/server/services/example/index.js index c8b18e6caa..5a23268d9b 100644 --- a/server/services/example/index.js +++ b/server/services/example/index.js @@ -9,6 +9,8 @@ module.exports = function ExampleService(gladys) { const client = axios.create({ timeout: 1000, }); + const device = new ExampleLightHandler(gladys, client); + /** * @public * @description This function starts the ExampleService service @@ -32,6 +34,7 @@ module.exports = function ExampleService(gladys) { return Object.freeze({ start, stop, - light: new ExampleLightHandler(gladys, client), + light: device, + device, }); }; diff --git a/server/services/example/lib/light/index.js b/server/services/example/lib/light/index.js index 65f830748b..b03f891e33 100644 --- a/server/services/example/lib/light/index.js +++ b/server/services/example/lib/light/index.js @@ -1,6 +1,7 @@ const turnOn = require('./light.turnOn'); const turnOff = require('./light.turnOff'); const getState = require('./light.getState'); +const setValue = require('./light.setValue'); /** * @description Add ability to control a light @@ -17,5 +18,6 @@ const ExampleLightHandler = function ExampleLightHandler(gladys, client) { ExampleLightHandler.prototype.turnOn = turnOn; ExampleLightHandler.prototype.turnOff = turnOff; ExampleLightHandler.prototype.getState = getState; +ExampleLightHandler.prototype.setValue = setValue; module.exports = ExampleLightHandler; diff --git a/server/services/example/lib/light/light.setValue.js b/server/services/example/lib/light/light.setValue.js new file mode 100644 index 0000000000..4c39d1b827 --- /dev/null +++ b/server/services/example/lib/light/light.setValue.js @@ -0,0 +1,19 @@ +/** + * @private + * @description Set the current state of a device. + * @param {Object} device - The device to control. + * @param {Object} deviceFeature - The deviceFeature to control. + * @param {string|number} value - The new state to set. + * @returns {Promise} Resolving with deviceFeature state. + * @example + * setValue(device, deviceFeature, value); + */ +async function setValue(device, deviceFeature, value) { + // Implement the way you send a new value to the real device + // - call HTTP API + // - send a message to a MQTT broker + // - ... + return value; +} + +module.exports = setValue; diff --git a/server/test/services/example/lib/setValue.test.js b/server/test/services/example/lib/setValue.test.js new file mode 100644 index 0000000000..d93633bf47 --- /dev/null +++ b/server/test/services/example/lib/setValue.test.js @@ -0,0 +1,19 @@ +const { expect } = require('chai'); + +const ExampleService = require('../../../../services/example/lib/light'); + +const gladys = {}; +const serviceId = '264d191c-dae2-4893-bf35-3c7fee3cc262'; + +describe('example setValue', () => { + let service; + + beforeEach(() => { + service = new ExampleService(gladys, serviceId); + }); + + it('should return arg value', async () => { + const result = await service.setValue(null, null, 'value'); + expect(result).to.equal('value'); + }); +});