Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add device.setValue action in scene #1239

Merged
merged 7 commits into from
Jul 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions front/cypress/integration/routes/scene/Scene.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,41 @@
describe('Scene view', () => {
before(() => {
cy.login();
const serverUrl = Cypress.env('serverUrl');
cy.request({
method: 'GET',
url: `${serverUrl}/api/v1/room`
}).then(res => {
const device = {
name: 'One device',
external_id: 'one-device',
selector: 'one-device',
room_id: res.body[0].id,
features: [
{
name: 'Multilevel',
category: 'light',
type: 'temperature',
external_id: 'light-temperature',
selector: 'light-temperature',
read_only: false,
keep_history: true,
has_feedback: true,
min: 0,
max: 1
}
]
};
cy.createDevice(device, 'mqtt');
});
});
beforeEach(() => {
cy.login();
});
after(() => {
// Delete all Bluetooth devices
cy.deleteDevices('mqtt');
});
it('Should create new scene', () => {
cy.visit('/dashboard/scene');
cy.contains('scene.newButton')
Expand Down Expand Up @@ -45,6 +79,28 @@ describe('Scene view', () => {
.click()
.type('My House{enter}');
});
it('Should add new condition device set value', () => {
cy.visit('/dashboard/scene/my-scene');
cy.contains('editScene.addActionButton')
.should('have.class', 'btn-outline-primary')
.click();

const i18n = Cypress.env('i18n');

cy.get('.choose-scene-action-type')
.click()
.type(`${i18n.editScene.actions.device['set-value']}{enter}`);

// I don't know why, but I'm unable to get this button with
// the text. Using the class but it's not recommended otherwise!!
cy.get('.btn-success').then(buttons => {
cy.wrap(buttons[1]).click();
});

cy.get('.select-device-feature')
.click()
.type('One device{enter}');
});
it('Should disable scene', () => {
cy.visit('/dashboard/scene');

Expand Down
61 changes: 61 additions & 0 deletions front/src/components/device/ColorPicker.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Component, createRef } from 'preact';
import cx from 'classnames';
import iro from '@jaames/iro';

import { intToHex, hexToInt } from '../../../../server/utils/colors';

class ColorDeviceType extends Component {
colorPickerRef = createRef();

updateValue = color => {
if (color) {
const colorInt = hexToInt(color.hexString);
this.props.updateValue(colorInt);
}
};

componentDidMount() {
const { value } = this.props;
const color = !value ? undefined : `#${intToHex(value)}`;

this.colorPicker = new iro.ColorPicker(this.colorPickerRef.current, {
width: 150,
color,
layout: [
{
component: iro.ui.Wheel,
options: {}
}
]
});
this.colorPicker.on('input:end', color => this.updateValue(color));
}

componentDidUpdate(previousProps) {
if (previousProps.value !== this.props.value) {
const { value } = this.props;
if (value) {
const color = `#${intToHex(value)}`;
this.colorPicker.color.hexString = color;
}
}
}

render(props, {}) {
return (
<div
class={cx('fade', 'w-100', 'mw-100', {
show: true
})}
>
<div class="row justify-content-end">
<div class="col-12 py-3 d-flex justify-content-center">
<div ref={this.colorPickerRef} />
</div>
</div>
</div>
);
}
}

export default ColorDeviceType;
14 changes: 13 additions & 1 deletion front/src/components/device/SelectDeviceFeature.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class SelectDeviceFeature extends Component {
deviceFeaturesDictionnary[feature.selector] = feature;
deviceDictionnary[feature.selector] = device;

if (this.props.exclude_read_only_device_features === true && feature.read_only) {
return;
}

roomDeviceFeatures.push({
value: feature.selector,
label: getDeviceFeatureName(this.props.intl.dictionary, device, feature)
Expand Down Expand Up @@ -106,7 +110,15 @@ class SelectDeviceFeature extends Component {
if (!deviceOptions) {
return null;
}
return <Select defaultValue={''} value={selectedOption} onChange={this.handleChange} options={deviceOptions} />;
return (
<Select
class="select-device-feature"
defaultValue={''}
value={selectedOption}
onChange={this.handleChange}
options={deviceOptions}
/>
);
}
}

Expand Down
8 changes: 7 additions & 1 deletion front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,11 @@
"description": "This will get the last value of the selected device, so you can use it later in the scene.",
"deviceLabel": "Select a device"
},
"deviceSetValue": {
"description": "This action let you control a device.",
"deviceLabel": "Device",
"valueLabel": "Value"
},
"onlyContinueIf": {
"variableLabel": "Variable",
"operatorLabel": "Operator",
Expand Down Expand Up @@ -985,7 +990,8 @@
},
"actions": {
"device": {
"get-value": "Get device value"
"get-value": "Get device value",
"set-value": "Set device value"
},
"message": {
"send": "Send Message"
Expand Down
8 changes: 7 additions & 1 deletion front/src/config/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,11 @@
"description": "La scène récupèrera le dernier état de l'appareil sélectionné, vous pourrez l'utiliser plus tard dans celle-ci.",
"deviceLabel": "Sélectionnez un appareil"
},
"deviceSetValue": {
"description": "Cette action vous permet de contrôler un appareil.",
"deviceLabel": "Appareil",
"valueLabel": "Valeur"
},
"onlyContinueIf": {
"variableLabel": "Variable",
"operatorLabel": "Opérateur",
Expand Down Expand Up @@ -985,7 +990,8 @@
},
"actions": {
"device": {
"get-value": "Récupérer le dernier état"
"get-value": "Récupérer le dernier état",
"set-value": "Contrôler un appareil"
},
"message": {
"send": "Envoyer un message"
Expand Down
12 changes: 11 additions & 1 deletion front/src/routes/scene/edit-scene/ActionCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ACTIONS } from '../../../../../server/utils/constants';
import ChooseActionTypeParams from './actions/ChooseActionTypeCard';
import DelayActionParams from './actions/DelayActionParams';
import DeviceGetValueParams from './actions/DeviceGetValueParams';
import DeviceSetValue from './actions/DeviceSetValue';
import SendMessageParams from './actions/SendMessageParams';
import OnlyContinueIfParams from './actions/only-continue-if/OnlyContinueIfParams';
import TurnOnOffLightParams from './actions/TurnOnOffLightParams';
Expand Down Expand Up @@ -39,7 +40,8 @@ const ACTION_ICON = {
[ACTIONS.CONDITION.CHECK_TIME]: 'fe fe-watch',
[ACTIONS.SCENE.START]: 'fe fe-fast-forward',
[ACTIONS.HOUSE.IS_EMPTY]: 'fe fe-home',
[ACTIONS.HOUSE.IS_NOT_EMPTY]: 'fe fe-home'
[ACTIONS.HOUSE.IS_NOT_EMPTY]: 'fe fe-home',
[ACTIONS.DEVICE.SET_VALUE]: 'fe fe-radio'
};

const ActionCard = ({ children, ...props }) => (
Expand Down Expand Up @@ -229,6 +231,14 @@ const ActionCard = ({ children, ...props }) => (
updateActionProperty={props.updateActionProperty}
/>
)}
{props.action.type === ACTIONS.DEVICE.SET_VALUE && (
<DeviceSetValue
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 @@ -21,7 +21,8 @@ const ACTION_LIST = [
ACTIONS.CONDITION.CHECK_TIME,
ACTIONS.SCENE.START,
ACTIONS.HOUSE.IS_EMPTY,
ACTIONS.HOUSE.IS_NOT_EMPTY
ACTIONS.HOUSE.IS_NOT_EMPTY,
ACTIONS.DEVICE.SET_VALUE
];

const TRANSLATIONS = ACTION_LIST.reduce((acc, action) => {
Expand Down
146 changes: 146 additions & 0 deletions front/src/routes/scene/edit-scene/actions/DeviceSetValue.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Component } from 'preact';
import { connect } from 'unistore/preact';
import { Text, Localizer } from 'preact-i18n';
import cx from 'classnames';
import ColorPicker from '../../../../components/device/ColorPicker';

import SelectDeviceFeature from '../../../../components/device/SelectDeviceFeature';
import withIntlAsProp from '../../../../utils/withIntlAsProp';

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

import '../../../../components/boxs/device-in-room/device-features/style.css';

class DeviceSetValue extends Component {
onDeviceFeatureChange = (deviceFeature, device) => {
const { columnIndex, index } = this.props;
const deviceFeatureChanged = this.props.action.device_feature !== deviceFeature.selector;
if (deviceFeature) {
this.props.updateActionProperty(columnIndex, index, 'device_feature', deviceFeature.selector);
} else {
this.props.updateActionProperty(columnIndex, index, 'device_feature', null);
}
if (deviceFeatureChanged) {
this.props.updateActionProperty(columnIndex, index, 'value', undefined);
}
this.setState({ deviceFeature, device });
};

handleNewValue = e => {
const { columnIndex, index } = this.props;
this.props.updateActionProperty(columnIndex, index, 'value', e.target.value);
};

handleNewColorValue = color => {
const { columnIndex, index } = this.props;
this.props.updateActionProperty(columnIndex, index, 'value', color);
};

toggleBinaryValue = e => {
const { columnIndex, index, action } = this.props;
const previousValue = action.value !== undefined ? action.value : 0;
const newValue = previousValue === 1 ? 0 : 1;
this.props.updateActionProperty(columnIndex, index, 'value', newValue);
};

getDeviceFeatureControl = () => {
if (!this.state.deviceFeature) {
return null;
}

if (this.state.deviceFeature.type === DEVICE_FEATURE_TYPES.SWITCH.BINARY) {
return (
<label class="custom-switch">
<input
type="radio"
name={this.state.deviceFeature.id}
value="1"
class="custom-switch-input"
checked={this.props.action.value === 1}
onClick={this.toggleBinaryValue}
/>
<span class="custom-switch-indicator" />
</label>
);
}

if (this.state.deviceFeature.type === DEVICE_FEATURE_TYPES.LIGHT.COLOR) {
return <ColorPicker value={this.props.action.value} updateValue={this.handleNewColorValue} />;
}

return (
<div>
<div class="input-group">
<Localizer>
<input
type="text"
placeholder={<Text id="editScene.actionsCard.deviceSetValue.valueLabel" />}
class="form-control"
onChange={this.handleNewValue}
value={this.props.action.value}
/>
</Localizer>
{this.state.deviceFeature.unit && (
<span class="input-group-append" id="basic-addon2">
<span class="input-group-text">
<Text id={`deviceFeatureUnitShort.${this.state.deviceFeature.unit}`} />
</span>
</span>
)}
</div>

<input
style={{
minHeight: '30px'
}}
type="range"
value={this.props.action.value}
onChange={this.handleNewValue}
class={cx('form-control custom-range', {
'light-temperature': this.state.deviceFeature.type === DEVICE_FEATURE_TYPES.LIGHT.TEMPERATURE
})}
step="1"
min={this.state.deviceFeature.min}
max={this.state.deviceFeature.max}
/>
</div>
);
};

render(props, {}) {
const { action } = props;
return (
<div>
<div class="form-group">
<p>
<Text id="editScene.actionsCard.deviceSetValue.description" />
</p>
</div>
<div class="form-group">
<label class="form-label">
<Text id="editScene.actionsCard.deviceSetValue.deviceLabel" />
<span class="form-required">
<Text id="global.requiredField" />
</span>
</label>
<SelectDeviceFeature
exclude_read_only_device_features
value={action.device_feature}
onDeviceFeatureChange={this.onDeviceFeatureChange}
/>
</div>
<div class="form-group">
<label class="form-label">
<Text id="editScene.actionsCard.deviceSetValue.valueLabel" />
<span class="form-required">
<Text id="global.requiredField" />
</span>
</label>
</div>
<div class="form-group">{this.getDeviceFeatureControl()}</div>
</div>
);
}
}

export default withIntlAsProp(connect('httpClient', {})(DeviceSetValue));