Skip to content

Commit df87bc4

Browse files
authored
Dashboard: Add widget to start scene from dashboard (#1840)
1 parent de1a5de commit df87bc4

File tree

12 files changed

+297
-2
lines changed

12 files changed

+297
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Component } from 'preact';
2+
import { Localizer, Text } from 'preact-i18n';
3+
import BaseEditBox from '../baseEditBox';
4+
import withIntlAsProp from '../../../utils/withIntlAsProp';
5+
import { connect } from 'unistore/preact';
6+
import Select from 'react-select';
7+
import { RequestStatus } from '../../../utils/consts';
8+
9+
class EditSceneBox extends Component {
10+
updateScenes = selectedSceneOptions => {
11+
selectedSceneOptions = selectedSceneOptions || [];
12+
const selectedScenes = selectedSceneOptions.map(option => option.value);
13+
this.props.updateBoxConfig(this.props.x, this.props.y, {
14+
scenes: selectedScenes
15+
});
16+
this.setState({ selectedSceneOptions });
17+
};
18+
19+
updateName = e => {
20+
this.props.updateBoxConfig(this.props.x, this.props.y, {
21+
name: e.target.value
22+
});
23+
};
24+
25+
getScenes = async () => {
26+
try {
27+
this.setState({ status: RequestStatus.Getting });
28+
const params = {
29+
order_dir: 'asc'
30+
};
31+
const sceneOptions = [];
32+
const scenes = await this.props.httpClient.get(`/api/v1/scene`, params);
33+
scenes.forEach(scene => {
34+
const sceneOption = {
35+
value: scene.selector,
36+
label: scene.name
37+
};
38+
sceneOptions.push(sceneOption);
39+
});
40+
41+
await this.setState({
42+
sceneOptions,
43+
status: RequestStatus.Success
44+
});
45+
46+
await this.refreshSelectedOptions(this.props);
47+
} catch (e) {
48+
this.setState({
49+
status: RequestStatus.Error
50+
});
51+
}
52+
};
53+
54+
refreshSelectedOptions = async props => {
55+
const selectedSceneOptions = [];
56+
if (this.state.sceneOptions) {
57+
this.state.sceneOptions.forEach(sceneOption => {
58+
if (props.box.scenes && props.box.scenes.indexOf(sceneOption.value) !== -1) {
59+
selectedSceneOptions.push(sceneOption);
60+
}
61+
});
62+
}
63+
await this.setState({ selectedSceneOptions });
64+
};
65+
66+
componentDidMount = () => {
67+
this.getScenes();
68+
};
69+
70+
componentWillReceiveProps(nextProps) {
71+
if (nextProps.box && nextProps.box.scenes) {
72+
if (!this.props.box || !this.props.box.scenes || nextProps.box.scenes !== this.props.box.scenes) {
73+
this.refreshSelectedOptions(nextProps);
74+
}
75+
}
76+
}
77+
render(props, { status, selectedSceneOptions, sceneOptions }) {
78+
const loading = status === RequestStatus.Getting && !status;
79+
return (
80+
<BaseEditBox {...props} titleKey="dashboard.boxTitle.scene">
81+
<div class={loading ? 'dimmer active' : 'dimmer'}>
82+
<div class="loader" />
83+
<div class="dimmer-content">
84+
<div class="form-group">
85+
<label>
86+
<Text id="dashboard.boxes.scene.editNameLabel" />
87+
</label>
88+
<Localizer>
89+
<input
90+
type="text"
91+
className="form-control"
92+
placeholder={<Text id="dashboard.boxes.scene.editNamePlaceholder" />}
93+
value={props.box.name}
94+
onInput={this.updateName}
95+
/>
96+
</Localizer>
97+
</div>
98+
{sceneOptions && (
99+
<div class="form-group">
100+
<label>
101+
<Text id="dashboard.boxes.scene.editSceneLabel" />
102+
</label>
103+
<Select
104+
defaultValue={[]}
105+
value={selectedSceneOptions}
106+
options={sceneOptions}
107+
isMulti
108+
onChange={this.updateScenes}
109+
maxMenuHeight={220}
110+
/>
111+
</div>
112+
)}
113+
</div>
114+
</div>
115+
</BaseEditBox>
116+
);
117+
}
118+
}
119+
120+
export default withIntlAsProp(connect('httpClient', {})(EditSceneBox));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Component } from 'preact';
2+
import { connect } from 'unistore/preact';
3+
import { RequestStatus } from '../../../utils/consts';
4+
import SceneRow from './SceneRow';
5+
import cx from 'classnames';
6+
7+
class SceneBoxComponent extends Component {
8+
refreshData = () => {
9+
this.getScene();
10+
};
11+
12+
getScene = async () => {
13+
this.setState({ status: RequestStatus.Getting });
14+
try {
15+
const scenes = await this.props.httpClient.get(`/api/v1/scene`, {
16+
selectors: this.props.box.scenes.join(',')
17+
});
18+
this.setState({
19+
scenes,
20+
status: RequestStatus.Success
21+
});
22+
} catch (e) {
23+
this.setState({
24+
status: RequestStatus.Error
25+
});
26+
}
27+
};
28+
29+
componentDidMount() {
30+
this.refreshData();
31+
}
32+
33+
componentWillReceiveProps(nextProps) {
34+
if (nextProps.box.scenes !== this.props.box.scenes) {
35+
this.refreshData();
36+
}
37+
}
38+
39+
render(props, { scenes, status }) {
40+
const boxTitle = props.box.name;
41+
const loading = status === RequestStatus.Getting && !status;
42+
43+
return (
44+
<div class="card">
45+
<div class="card-header">
46+
<h3 class="card-title">{boxTitle}</h3>
47+
</div>
48+
49+
<div
50+
class={cx('dimmer', {
51+
active: loading
52+
})}
53+
>
54+
<div class="loader py-3" />
55+
<div class="dimmer-content">
56+
<div class="table-responsive">
57+
<table className="table card-table table-vcenter">
58+
<tbody>
59+
{scenes &&
60+
scenes.map(scene => (
61+
<SceneRow
62+
boxStatus={status}
63+
name={scene.name}
64+
icon={scene.icon}
65+
user={props.user}
66+
sceneSelector={scene.selector}
67+
/>
68+
))}
69+
</tbody>
70+
</table>
71+
</div>
72+
</div>
73+
</div>
74+
</div>
75+
);
76+
}
77+
}
78+
79+
export default connect('user,httpClient', {})(SceneBoxComponent);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Text } from 'preact-i18n';
2+
import { connect } from 'unistore/preact';
3+
import { Component } from 'preact';
4+
import cx from 'classnames';
5+
import style from './style.css';
6+
7+
class SceneRow extends Component {
8+
startScene = async () => {
9+
try {
10+
await this.setState({ loading: true });
11+
await this.props.httpClient.post(`/api/v1/scene/${this.props.sceneSelector}/start`);
12+
} catch (e) {
13+
console.error(e);
14+
}
15+
setTimeout(() => this.setState({ loading: false }), 500);
16+
};
17+
18+
render({ children, ...props }, { loading }) {
19+
return (
20+
<tr>
21+
<td>
22+
<i className={`fe fe-${props.icon}`} />
23+
</td>
24+
<td>{props.name}</td>
25+
<td className="text-right">
26+
<button
27+
onClick={this.startScene}
28+
type="button"
29+
class={cx('btn', 'btn-outline-success', 'btn-sm', style.btnLoading, {
30+
'btn-loading': loading
31+
})}
32+
disabled={loading}
33+
>
34+
<i class="fe fe-play" />
35+
<Text id="scene.startButton" />
36+
</button>
37+
</td>
38+
</tr>
39+
);
40+
}
41+
}
42+
export default connect('httpClient', {})(SceneRow);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.btnLoading {
2+
&::after {
3+
border: 2px solid #5eba00;
4+
}
5+
}

front/src/config/i18n/en.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,8 @@
215215
"devices": "Devices",
216216
"chart": "Chart",
217217
"ecowatt": "Ecowatt (France)",
218-
"clock": "Clock"
218+
"clock": "Clock",
219+
"scene": "Scene"
219220
},
220221
"boxes": {
221222
"column": "Column {{index}}",
@@ -329,6 +330,11 @@
329330
"displaySecond": "Display seconds?",
330331
"yes": "Yes",
331332
"no": "No"
333+
},
334+
"scene": {
335+
"editNameLabel": "Enter the name of this box",
336+
"editNamePlaceholder": "Name displayed on the dashboard",
337+
"editSceneLabel": "Select the scene you want to display here."
332338
}
333339
}
334340
},

front/src/config/i18n/fr.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,8 @@
215215
"devices": "Appareils",
216216
"chart": "Graphique",
217217
"ecowatt": "Ecowatt ( France )",
218-
"clock": "Horloge"
218+
"clock": "Horloge",
219+
"scene": "Scène"
219220
},
220221
"boxes": {
221222
"column": "Colonne {{index}}",
@@ -329,6 +330,11 @@
329330
"displaySecond": "Afficher les secondes ?",
330331
"yes": "Oui",
331332
"no": "Non"
333+
},
334+
"scene": {
335+
"editNameLabel": "Entrez le nom de cette box",
336+
"editNamePlaceholder": "Nom affiché sur le tableau de bord",
337+
"editSceneLabel": "Sélectionnez la scène que vous souhaitez afficher ici."
332338
}
333339
}
334340
},

front/src/routes/dashboard/Box.jsx

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import DevicesBox from '../../components/boxs/device-in-room/DevicesBox';
88
import ChartBox from '../../components/boxs/chart/Chart';
99
import EcowattBox from '../../components/boxs/ecowatt/Ecowatt';
1010
import ClockBox from '../../components/boxs/clock/Clock';
11+
import SceneBox from '../../components/boxs/scene/SceneBox';
1112

1213
const Box = ({ children, ...props }) => {
1314
switch (props.box.type) {
@@ -31,6 +32,8 @@ const Box = ({ children, ...props }) => {
3132
return <EcowattBox {...props} />;
3233
case 'clock':
3334
return <ClockBox {...props} />;
35+
case 'scene':
36+
return <SceneBox {...props} />;
3437
}
3538
};
3639

front/src/routes/dashboard/edit-dashboard/EditBox.jsx

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import EditEcowatt from '../../../components/boxs/ecowatt/EditEcowatt';
1010
import EditClock from '../../../components/boxs/clock/EditClock';
1111

1212
import SelectBoxType from '../../../components/boxs/SelectBoxType';
13+
import EditSceneBox from '../../../components/boxs/scene/EditSceneBox';
1314

1415
const Box = ({ children, ...props }) => {
1516
switch (props.box.type) {
@@ -33,6 +34,8 @@ const Box = ({ children, ...props }) => {
3334
return <EditEcowatt {...props} />;
3435
case 'clock':
3536
return <EditClock {...props} />;
37+
case 'scene':
38+
return <EditSceneBox {...props} />;
3639
default:
3740
return <SelectBoxType {...props} />;
3841
}

server/lib/scene/scene.get.js

+9
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ async function get(options) {
3838
});
3939
}
4040

41+
// search by device feature selectors
42+
if (optionsWithDefault.selectors) {
43+
queryParams.where = {
44+
[Op.or]: optionsWithDefault.selectors.split(',').map((selector) => ({
45+
selector,
46+
})),
47+
};
48+
}
49+
4150
const scenes = await db.Scene.findAll(queryParams);
4251

4352
const scenesPlain = scenes.map((scene) => scene.get({ plain: true }));

server/models/dashboard.js

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const boxesSchema = Joi.array().items(
2828
clock_display_second: Joi.boolean(),
2929
camera_latency: Joi.string(),
3030
camera_live_auto_start: Joi.boolean(),
31+
scenes: Joi.array().items(Joi.string()),
3132
}),
3233
),
3334
);

server/test/lib/scene/scene.get.test.js

+20
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,24 @@ describe('SceneManager.get', () => {
5353
expect(scenes).to.be.instanceOf(Array);
5454
expect(scenes).to.deep.equal([]);
5555
});
56+
57+
it('should filter by selector', async () => {
58+
const sceneManager = new SceneManager({}, event);
59+
const scenes = await sceneManager.get({
60+
selectors: 'test-scene',
61+
});
62+
expect(scenes).to.be.instanceOf(Array);
63+
expect(scenes).to.deep.equal([
64+
{
65+
id: '3a30636c-b3f0-4251-a347-90787f0fe940',
66+
name: 'Test scene',
67+
icon: 'fe fe-bell',
68+
active: true,
69+
description: null,
70+
selector: 'test-scene',
71+
last_executed: null,
72+
updated_at: new Date('2019-02-12T07:49:07.556Z'),
73+
},
74+
]);
75+
});
5676
});

server/utils/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,7 @@ const DASHBOARD_BOX_TYPE = {
895895
CHART: 'chart',
896896
ECOWATT: 'ecowatt',
897897
CLOCK: 'clock',
898+
SCENE: 'scene',
898899
};
899900

900901
const ERROR_MESSAGES = {

0 commit comments

Comments
 (0)