From 3ba5dd832ccf331540da367460a152e712af6400 Mon Sep 17 00:00:00 2001 From: Alexandre Trovato <1839717+atrovato@users.noreply.github.com> Date: Thu, 21 Jul 2022 11:55:49 +0200 Subject: [PATCH] Fix #1316: Update temperature/humidity in room box live when changes (#1465) --- .../routes/dashboard/DashboardHumidityBox.js | 148 +++++++++++++++++ .../dashboard/DashboardTemperatureBox.js | 150 ++++++++++++++++++ .../actions/dashboard/boxes/humidityInRoom.js | 20 ++- .../dashboard/boxes/temperatureInRoom.js | 20 ++- .../boxs/room-humidity/RoomHumidity.jsx | 15 +- .../boxs/room-temperature/RoomTemperature.jsx | 32 +++- server/services/example/index.js | 5 +- server/services/example/lib/light/index.js | 2 + .../example/lib/light/light.setValue.js | 19 +++ .../services/example/lib/setValue.test.js | 19 +++ 10 files changed, 419 insertions(+), 11 deletions(-) create mode 100644 front/cypress/integration/routes/dashboard/DashboardHumidityBox.js create mode 100644 front/cypress/integration/routes/dashboard/DashboardTemperatureBox.js create mode 100644 server/services/example/lib/light/light.setValue.js create mode 100644 server/test/services/example/lib/setValue.test.js 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 }) => (