Skip to content

Commit

Permalink
Add user presence action in scene (#999)
Browse files Browse the repository at this point in the history
* backend: user presence

* Allow the house key in action model

* First frontend version of the user presence action box in scene

* Add english translation

* Add more tests

* Add message in UI in dashboard box about setting user presence
  • Loading branch information
Pierre-Gilles authored Dec 3, 2020
1 parent 6f92880 commit da901e8
Show file tree
Hide file tree
Showing 15 changed files with 337 additions and 12 deletions.
12 changes: 11 additions & 1 deletion front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@
"noTemperatureRecorded": "No temperature recorded recently."
},
"userPresence": {
"description": "Display who's at home and who is not.",
"description": "Display who's at home and who is not. You can change the user presence in scenes.",
"left": "Left ({{since}})",
"atHome": "At Home",
"neverSeenAtHome": "Never Seen At Home",
Expand Down Expand Up @@ -724,6 +724,12 @@
"removeLabel": "Remove",
"orText": "OR",
"orButton": "+ OR"
},
"userPresence": {
"userLabel": "User",
"houseLabel": "House",
"userSeenDescription": "This action set the user as \"at home\".",
"userLeftHomeDescription": "This action set the user as \"left home\"."
}
},
"actions": {
Expand Down Expand Up @@ -751,6 +757,10 @@
},
"condition": {
"only-continue-if": "Only continue if"
},
"user": {
"set-seen-at-home": "User seen at home",
"set-out-of-home": "User left home"
}
},
"variables": {
Expand Down
12 changes: 11 additions & 1 deletion front/src/config/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@
"noTemperatureRecorded": "Aucune température enregistrée récemment."
},
"userPresence": {
"description": "Cette box affiche qui est à la maison et qui ne l'est pas.",
"description": "Cette box affiche qui est à la maison et qui ne l'est pas. Vous pouvez changer la présence d'un utilisateur dans les scènes.",
"left": "Absent ({{since}})",
"atHome": "A la maison",
"neverSeenAtHome": "Jamais vu à la maison",
Expand Down Expand Up @@ -724,6 +724,12 @@
"removeLabel": "Effacer",
"orText": "OU",
"orButton": "+ OU"
},
"userPresence": {
"userLabel": "Utilisateur",
"houseLabel": "Maison",
"userSeenDescription": "Cette action marque l'utilisateur comme présent à la maison.",
"userLeftHomeDescription": "Cette action indique que l'utilisateur a quitté la maison."
}
},
"actions": {
Expand Down Expand Up @@ -751,6 +757,10 @@
},
"condition": {
"only-continue-if": "Continuer seulement si"
},
"user": {
"set-seen-at-home": "Utilisateur vu à la maison",
"set-out-of-home": "Utilisateur parti de la maison"
}
},
"variables": {
Expand Down
21 changes: 20 additions & 1 deletion front/src/routes/scene/edit-scene/ActionCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SendMessageParams from './actions/SendMessageParams';
import OnlyContinueIfParams from './actions/only-continue-if/OnlyContinueIfParams';
import TurnOnOffLightParams from './actions/TurnOnOffLightParams';
import TurnOnOffSwitchParams from './actions/TurnOnOffSwitchParams';
import UserPresence from './actions/UserPresence';

const deleteActionFromColumn = (columnIndex, rowIndex, deleteAction) => () => {
deleteAction(columnIndex, rowIndex);
Expand All @@ -23,7 +24,9 @@ const ACTION_ICON = {
[ACTIONS.TIME.DELAY]: 'fe fe-clock',
[ACTIONS.MESSAGE.SEND]: 'fe fe-message-square',
[ACTIONS.CONDITION.ONLY_CONTINUE_IF]: 'fe fe-shuffle',
[ACTIONS.DEVICE.GET_VALUE]: 'fe fe-refresh-cw'
[ACTIONS.DEVICE.GET_VALUE]: 'fe fe-refresh-cw',
[ACTIONS.USER.SET_SEEN_AT_HOME]: 'fe fe-home',
[ACTIONS.USER.SET_OUT_OF_HOME]: 'fe fe-home'
};

const ActionCard = ({ children, ...props }) => (
Expand Down Expand Up @@ -136,6 +139,22 @@ const ActionCard = ({ children, ...props }) => (
setVariables={props.setVariables}
/>
)}
{props.action.type === ACTIONS.USER.SET_SEEN_AT_HOME && (
<UserPresence
action={props.action}
columnIndex={props.columnIndex}
index={props.index}
updateActionProperty={props.updateActionProperty}
/>
)}
{props.action.type === ACTIONS.USER.SET_OUT_OF_HOME && (
<UserPresence
action={props.action}
columnIndex={props.columnIndex}
index={props.index}
updateActionProperty={props.updateActionProperty}
/>
)}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ const ACTION_LIST = [
ACTIONS.TIME.DELAY,
ACTIONS.MESSAGE.SEND,
ACTIONS.DEVICE.GET_VALUE,
ACTIONS.CONDITION.ONLY_CONTINUE_IF
ACTIONS.CONDITION.ONLY_CONTINUE_IF,
ACTIONS.USER.SET_SEEN_AT_HOME,
ACTIONS.USER.SET_OUT_OF_HOME
];

@connect('httpClient', {})
Expand Down
118 changes: 118 additions & 0 deletions front/src/routes/scene/edit-scene/actions/UserPresence.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Select from 'react-select';
import { Component } from 'preact';
import { connect } from 'unistore/preact';
import { Text } from 'preact-i18n';

import { ACTIONS } from '../../../../../../server/utils/constants';

@connect('httpClient', {})
class UserSeenAtHome extends Component {
getOptions = async () => {
try {
const [users, houses] = await Promise.all([
this.props.httpClient.get('/api/v1/user'),
this.props.httpClient.get('/api/v1/house')
]);
const userOptions = [];
users.forEach(user => {
userOptions.push({
label: user.firstname,
value: user.selector
});
});
const houseOptions = [];
houses.forEach(house => {
houseOptions.push({
label: house.name,
value: house.selector
});
});
await this.setState({ userOptions, houseOptions });
this.refreshSelectedOptions(this.props);
return userOptions;
} catch (e) {
console.log(e);
}
};
handleChange = selectedOption => {
if (selectedOption && selectedOption.value) {
this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'user', selectedOption.value);
} else {
this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'user', null);
}
};
handleHouseChange = selectedOption => {
if (selectedOption && selectedOption.value) {
this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'house', selectedOption.value);
} else {
this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'house', null);
}
};
refreshSelectedOptions = nextProps => {
let selectedOption = '';
let selectedHouseOption = '';
if (nextProps.action.user && this.state.userOptions) {
const userOption = this.state.userOptions.find(option => option.value === nextProps.action.user);

if (userOption) {
selectedOption = userOption;
}
}
if (nextProps.action.house && this.state.houseOptions) {
const houseOption = this.state.houseOptions.find(option => option.value === nextProps.action.house);

if (houseOption) {
selectedHouseOption = houseOption;
}
}
this.setState({ selectedOption, selectedHouseOption });
};
constructor(props) {
super(props);
this.props = props;
this.state = {
selectedOption: '',
selectedHouseOption: ''
};
}
componentDidMount() {
this.getOptions();
}
componentWillReceiveProps(nextProps) {
this.refreshSelectedOptions(nextProps);
}
render(props, { selectedOption, userOptions, houseOptions, selectedHouseOption }) {
return (
<div>
<p>
{props.action.type === ACTIONS.USER.SET_SEEN_AT_HOME && (
<Text id="editScene.actionsCard.userPresence.userSeenDescription" />
)}
{props.action.type === ACTIONS.USER.SET_OUT_OF_HOME && (
<Text id="editScene.actionsCard.userPresence.userLeftHomeDescription" />
)}
</p>
<div class="form-group">
<label class="form-label">
<Text id="editScene.actionsCard.userPresence.userLabel" />
<span class="form-required">
<Text id="global.requiredField" />
</span>
</label>
<Select options={userOptions} value={selectedOption} onChange={this.handleChange} />
</div>
<div class="form-group">
<label class="form-label">
<Text id="editScene.actionsCard.userPresence.houseLabel" />
<span class="form-required">
<Text id="global.requiredField" />
</span>
</label>
<Select options={houseOptions} value={selectedHouseOption} onChange={this.handleHouseChange} />
</div>
</div>
);
}
}

export default UserSeenAtHome;
55 changes: 55 additions & 0 deletions server/lib/house/house.userLeft.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const db = require('../../models');
const { NotFoundError } = require('../../utils/coreErrors');
const logger = require('../../utils/logger');
const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../utils/constants');

/**
* @description User left the house.
* @param {string} houseSelector - The selector of the house.
* @param {string} userSelector - The selector of the user.
* @example
* gladys.house.userLeft('main-house', 'john');
*/
async function userLeft(houseSelector, userSelector) {
const house = await db.House.findOne({
where: {
selector: houseSelector,
},
});

if (house === null) {
throw new NotFoundError('House not found');
}

const user = await db.User.findOne({
attributes: ['id', 'firstname', 'lastname', 'selector', 'email', 'current_house_id', 'last_house_changed'],
where: {
selector: userSelector,
},
});

if (user === null) {
throw new NotFoundError('User not found');
}

let userFinal = user;

// user was in the house before
if (userFinal.get({ plain: true }).current_house_id === house.id) {
logger.debug(`User ${userSelector} left home ${houseSelector}`);
userFinal = await user.update({ current_house_id: null, last_house_changed: new Date() });
// so we emit left home event
this.event.emit(EVENTS.USER_PRESENCE.LEFT_HOME, userFinal.get({ plain: true }));
// and we emit websocket event so that the change is sent to UI
this.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
type: WEBSOCKET_MESSAGE_TYPES.USER_PRESENCE.LEFT_HOME,
payload: userFinal.get({ plain: true }),
});
}

return userFinal.get({ plain: true });
}

module.exports = {
userLeft,
};
12 changes: 6 additions & 6 deletions server/lib/house/house.userSeen.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,24 @@ async function userSeen(houseSelector, userSelector) {
throw new NotFoundError('User not found');
}

const userPlain = user.get({ plain: true });
let userFinal = user;

// user was not in this house before
if (user.current_house_id !== house.id) {
await user.update({ current_house_id: house.id, last_house_changed: new Date() });
userFinal = await user.update({ current_house_id: house.id, last_house_changed: new Date() });
// so we emit back at home event
this.event.emit(EVENTS.USER_PRESENCE.BACK_HOME, userPlain);
this.event.emit(EVENTS.USER_PRESENCE.BACK_HOME, userFinal.get({ plain: true }));
// and we emit websocket event so that the change is sent to UI
this.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
type: WEBSOCKET_MESSAGE_TYPES.USER_PRESENCE.BACK_HOME,
payload: userPlain,
payload: userFinal.get({ plain: true }),
});
} else {
// otherwise, we just emit user seen event
this.event.emit(EVENTS.USER_PRESENCE.SEEN_AT_HOME, userPlain);
this.event.emit(EVENTS.USER_PRESENCE.SEEN_AT_HOME, userFinal.get({ plain: true }));
}

return userPlain;
return userFinal.get({ plain: true });
}

module.exports = {
Expand Down
2 changes: 2 additions & 0 deletions server/lib/house/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { destroy } = require('./house.destroy');
const { get } = require('./house.get');
const { getRooms } = require('./house.getRooms');
const { update } = require('./house.update');
const { userLeft } = require('./house.userLeft');
const { userSeen } = require('./house.userSeen');
const { getBySelector } = require('./house.getBySelector');

Expand All @@ -15,6 +16,7 @@ House.prototype.destroy = destroy;
House.prototype.get = get;
House.prototype.getRooms = getRooms;
House.prototype.update = update;
House.prototype.userLeft = userLeft;
House.prototype.userSeen = userSeen;
House.prototype.getBySelector = getBySelector;

Expand Down
2 changes: 1 addition & 1 deletion server/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function Gladys(params = {}) {
const user = new User(session, stateManager, variable);
const location = new Location(user, event);
const device = new Device(event, message, stateManager, service, room, variable);
const scene = new Scene(stateManager, event, device, message, variable);
const scene = new Scene(stateManager, event, device, message, variable, house);
const scheduler = new Scheduler(event);
const system = new System(db.sequelize, event, config);
const weather = new Weather(service, event, message, house);
Expand Down
3 changes: 2 additions & 1 deletion server/lib/scene/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ const { eventFunctionWrapper } = require('../../utils/functionsWrapper');

const DEFAULT_TIMEZONE = 'Europe/Paris';

const SceneManager = function SceneManager(stateManager, event, device, message, variable) {
const SceneManager = function SceneManager(stateManager, event, device, message, variable, house) {
this.stateManager = stateManager;
this.event = event;
this.device = device;
this.message = message;
this.variable = variable;
this.house = house;
this.scenes = {};
this.timezone = DEFAULT_TIMEZONE;
// @ts-ignore
Expand Down
6 changes: 6 additions & 0 deletions server/lib/scene/scene.actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ const actionsFunc = {
throw new AbortScene('CONDITION_NOT_VERIFIED');
}
},
[ACTIONS.USER.SET_SEEN_AT_HOME]: async (self, action) => {
await self.house.userSeen(action.house, action.user);
},
[ACTIONS.USER.SET_OUT_OF_HOME]: async (self, action) => {
await self.house.userLeft(action.house, action.user);
},
};

module.exports = {
Expand Down
1 change: 1 addition & 0 deletions server/models/scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const actionSchema = Joi.array().items(
device: Joi.string(),
devices: Joi.array().items(Joi.string()),
user: Joi.string(),
house: Joi.string(),
text: Joi.string(),
value: Joi.number(),
unit: Joi.string(),
Expand Down
Loading

0 comments on commit da901e8

Please sign in to comment.