Merge branch '79-addscene-getscenestates' into 'dev'

Able to add scenes and get scenes in a right way

Closes #79

See merge request sa4-2020/the-sanmarinoes/frontend!89
This commit is contained in:
Matteo Omenetti 2020-04-21 15:02:25 +02:00
commit 77f7abed92
13 changed files with 236 additions and 52 deletions

View file

@ -15,10 +15,12 @@ class DevicePanel extends Component {
} }
getDevices() { getDevices() {
if (this.props.tab === "Devices") {
this.props this.props
.fetchDevices() .fetchDevices()
.catch((err) => console.error(`error fetching devices:`, err)); .catch((err) => console.error(`error fetching devices:`, err));
} }
}
render() { render() {
return ( return (

View file

@ -30,12 +30,13 @@ class NewSceneDevice extends Component {
this.state = { this.state = {
openModal: false, openModal: false,
sceneDevices: this.props.scene ? this.props.scene.devices : {}, sceneDevices: this.props.scene ? this.props.scene.sceneStates : {},
deviceName: "", deviceName: "",
}; };
this.getDevices(); this.getDevices();
this.setSceneDevice = this.setSceneDevice.bind(this); this.setSceneState = this.setSceneState.bind(this);
this.createState = this.createState.bind(this);
} }
getDevices() { getDevices() {
@ -56,10 +57,25 @@ class NewSceneDevice extends Component {
this.handleClose(); this.handleClose();
}; };
setSceneDevice(e, d) { setSceneState(e, d) {
this.setState({ devicesAttached: d.value }); this.setState({ devicesAttached: d.value });
} }
createState() {
const device = this.props.devices.filter(
(e) => this.state.devicesAttached[0] === e.id
);
let data = {
sceneId: this.props.activeScene,
id: device[0].id,
kind: device[0].kind,
};
this.props
.saveState(data)
.catch((err) => console.error("error in creating state", err));
this.resetState();
}
render() { render() {
const availableDevices = []; const availableDevices = [];
this.props.devices.forEach((e) => { this.props.devices.forEach((e) => {
@ -83,7 +99,7 @@ class NewSceneDevice extends Component {
} }
centered={true} centered={true}
> >
<Modal.Header>Add a New Scene Device</Modal.Header> <Modal.Header>Add a New Scene State</Modal.Header>
<Modal.Content> <Modal.Content>
<Form> <Form>
<Form.Field style={{ marginTop: "1rem" }}> <Form.Field style={{ marginTop: "1rem" }}>
@ -93,7 +109,7 @@ class NewSceneDevice extends Component {
placeholder="Select Devices" placeholder="Select Devices"
fluid fluid
multiple multiple
onChange={this.setSceneDevice} onChange={this.setSceneState}
options={availableDevices} options={availableDevices}
/> />
</Form.Field> </Form.Field>
@ -101,7 +117,7 @@ class NewSceneDevice extends Component {
</Modal.Content> </Modal.Content>
<Modal.Actions> <Modal.Actions>
<Button <Button
onClick={this.createDevice} onClick={this.createState}
color="blue" color="blue"
icon icon
labelPosition="right" labelPosition="right"

View file

@ -1,6 +1,7 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { RemoteService } from "../../remote"; import { RemoteService } from "../../remote";
import Device from "./devices/Device";
import NewSceneDevice from "./NewSceneDevice"; import NewSceneDevice from "./NewSceneDevice";
import { Grid } from "semantic-ui-react"; import { Grid } from "semantic-ui-react";
@ -12,20 +13,38 @@ class ScenesPanel extends Component {
render() { render() {
return ( return (
<Grid doubling columns={2} divided="vertically"> <Grid doubling columns={2} divided="vertically">
{ {!this.props.isActiveDefaultScene
//TODO DISPLAY DEVICES IN SCENE ? this.props.sceneStates.map((e, i) => {
} return (
{this.props.isActiveDefaultScene ? ( <Grid.Column key={i}>
<Device tab={this.props.tab} id={e} />
</Grid.Column>
);
})
: null}
{!this.props.isActiveDefaultScene ? (
<Grid.Column> <Grid.Column>
<NewSceneDevice /> <NewSceneDevice />
</Grid.Column> </Grid.Column>
) : null} ) : (
<Grid.Column>Welcome to the Scene View, you add a Scene</Grid.Column>
)}
</Grid> </Grid>
); );
} }
} }
const mapStateToProps = (state, _) => ({ const mapStateToProps = (state, _) => ({
get sceneStates() {
if (state.active.activeScene !== -1) {
const stateArray = [
...state.scenes[state.active.activeScene].sceneStates,
].sort();
return stateArray.map((id) => state.sceneStates[id]);
} else {
return [];
}
},
get isActiveDefaultScene() { get isActiveDefaultScene() {
return state.active.activeScene === -1; return state.active.activeScene === -1;
}, },

View file

@ -30,7 +30,7 @@ class Device extends React.Component {
} }
renderDeviceComponent() { renderDeviceComponent() {
switch (this.props.device.kind) { switch (this.props.stateOrDevice.kind) {
case "regularLight": case "regularLight":
return <Light tab={this.props.tab} id={this.props.id} />; return <Light tab={this.props.tab} id={this.props.id} />;
case "sensor": case "sensor":
@ -57,13 +57,19 @@ class Device extends React.Component {
<Segment> <Segment>
<Grid columns={2}> <Grid columns={2}>
<Grid.Column>{this.renderDeviceComponent()}</Grid.Column> <Grid.Column>{this.renderDeviceComponent()}</Grid.Column>
{this.props.tab === "Devices" ? (
<Grid.Column textAlign="center"> <Grid.Column textAlign="center">
<Header as="h3">{this.props.device.name}</Header> <Header as="h3">{this.props.stateOrDevice.name}</Header>
<Button color="blue" icon onClick={this.edit} labelPosition="left"> <Button
color="blue"
icon
onClick={this.edit}
labelPosition="left"
>
<Icon name="pencil" /> <Icon name="pencil" />
Edit Edit
</Button> </Button>
{this.props.device.kind === "smartPlug" ? ( {this.props.stateOrDevice.kind === "smartPlug" ? (
<Button <Button
color="orange" color="orange"
icon icon
@ -75,6 +81,12 @@ class Device extends React.Component {
</Button> </Button>
) : null} ) : null}
</Grid.Column> </Grid.Column>
) : (
<Grid.Column textAlign="center">
<Header as="h3">{this.props.stateOrDevice.name}</Header>
</Grid.Column>
)}
<DeviceSettingsModal ref={this.modalRef} id={this.props.id} /> <DeviceSettingsModal ref={this.modalRef} id={this.props.id} />
</Grid> </Grid>
</Segment> </Segment>
@ -83,7 +95,14 @@ class Device extends React.Component {
} }
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state, ownProps) => ({
device: state.devices[ownProps.id], get stateOrDevice() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
const sceneState = state.sceneStates[ownProps.id];
return state.devices[sceneState];
}
},
}); });
const DeviceContainer = connect(mapStateToProps, RemoteService)(Device); const DeviceContainer = connect(mapStateToProps, RemoteService)(Device);
export default DeviceContainer; export default DeviceContainer;

View file

@ -37,7 +37,6 @@ class Light extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { intensity: this.props.device.intensity, timeout: null }; this.state = { intensity: this.props.device.intensity, timeout: null };
console.log(this.props);
this.iconOn = "/img/lightOn.svg"; this.iconOn = "/img/lightOn.svg";
this.iconOff = "/img/lightOff.svg"; this.iconOff = "/img/lightOff.svg";

View file

@ -405,6 +405,35 @@ export const RemoteService = {
}; };
}, },
/**
* Creates/Updates a state with the given data. If
* data.id is truthy, then a update call is performed,
* otherwise a create call is performed. The update URL
* is computed based on data.kind when data.flowType =
* 'OUTPUT', otherwise the PUT "/device" endpoint
* is used for updates and the POST "/<device.kind>"
* endpoints are used for creation.
* @param {State} data the device to update.
* @returns {Promise<Device, RemoteError>} promise that resolves to the saved device and rejects
* with user-fiendly errors as a RemoteError
*/
saveState: (data) => {
return (dispatch) => {
let url =
"/" + data.kind + "/" + data.id + "/state?sceneId=" + data.sceneId;
return Endpoint["post"](url, {}, data)
.then((res) => {
dispatch(actions.stateSave(res.data));
return res.data;
})
.catch((err) => {
console.warn("Update device: ", data, "error: ", err);
throw new RemoteError(["Network error"]);
});
};
},
/** /**
* Connetcs a series of output devices to an input device. * Connetcs a series of output devices to an input device.
* Output devices for Switch input can be: Normal Light, Dimmable Light, Smart Plug. * Output devices for Switch input can be: Normal Light, Dimmable Light, Smart Plug.

View file

@ -41,7 +41,7 @@ function reducer(previousState, action) {
const createOrUpdateScene = (scene) => { const createOrUpdateScene = (scene) => {
if (!newState.scenes[scene.id]) { if (!newState.scenes[scene.id]) {
newState = update(newState, { newState = update(newState, {
scenes: { [scene.id]: { $set: { ...scene, states: new Set() } } }, scenes: { [scene.id]: { $set: { ...scene, sceneStates: new Set() } } },
}); });
} else { } else {
newState = update(newState, { newState = update(newState, {
@ -58,7 +58,7 @@ function reducer(previousState, action) {
pendingJoins: { scenes: { $unset: [scene.id] } }, pendingJoins: { scenes: { $unset: [scene.id] } },
scenes: { scenes: {
[scene.id]: { [scene.id]: {
states: { sceneStates: {
$add: [...newState.pendingJoins.scenes[scene.id]], $add: [...newState.pendingJoins.scenes[scene.id]],
}, },
}, },
@ -79,6 +79,11 @@ function reducer(previousState, action) {
} }
}; };
const updateSceneStateProps = (state) => {
change.sceneStates[state.deviceId] = {};
change.sceneStates[state.deviceId] = { $set: state.deviceId };
};
switch (action.type) { switch (action.type) {
case "LOGIN_UPDATE": case "LOGIN_UPDATE":
newState = update(previousState, { login: { $set: action.login } }); newState = update(previousState, { login: { $set: action.login } });
@ -94,10 +99,70 @@ function reducer(previousState, action) {
break; break;
case "SCENES_UPDATE": case "SCENES_UPDATE":
newState = previousState; newState = previousState;
console.log(action.scenes);
for (const scene of action.scenes) { for (const scene of action.scenes) {
createOrUpdateScene(scene); createOrUpdateScene(scene);
} }
break;
case "STATE_UPDATE":
newState = previousState;
change = null;
// if room is given, delete all devices in that room
// and remove any join between that room and deleted
// devices
change = {
scenes: { [action.sceneId]: { sceneStates: { $set: new Set() } } },
sceneStates: { $unset: [] },
};
const scene = newState.scenes[action.sceneId];
for (const stateId of scene.sceneStates) {
change.sceneStates.$unset.push(stateId);
}
newState = update(previousState, change);
change = {
sceneStates: {},
scenes: {},
pendingJoins: { scenes: {} },
};
for (const sceneState of action.sceneStates) {
if (!newState.sceneStates[sceneState.deviceId]) {
change.sceneStates[sceneState.deviceId] = {
$set: sceneState.deviceId,
};
} else {
updateSceneStateProps(sceneState);
}
if (sceneState.sceneId in newState.scenes) {
change.scenes[sceneState.sceneId] =
change.scenes[sceneState.sceneId] || {};
change.scenes[sceneState.sceneId].sceneStates =
change.scenes[sceneState.sceneId].sceneStates || {};
const sceneStates = change.scenes[sceneState.sceneId].sceneStates;
sceneStates.$add = sceneStates.$add || [];
sceneStates.$add.push(sceneState.deviceId);
} else {
// room does not exist yet, so add to the list of pending
// joins
if (!change.pendingJoins.scenes[sceneState.sceneId]) {
change.pendingJoins.scenes[sceneState.sceneId] = {
$set: new Set([sceneState.deviceId]),
};
} else {
change.pendingJoins.scenes[sceneState.sceneId].$set.add(
sceneState.deviceId
);
}
}
}
newState = update(newState, change);
break; break;
case "DEVICES_UPDATE": case "DEVICES_UPDATE":
change = null; change = null;
@ -220,6 +285,31 @@ function reducer(previousState, action) {
} }
newState = update(previousState, change); newState = update(previousState, change);
break; break;
case "STATE_SAVE":
console.log("Store", action.sceneState);
change = {
sceneStates: { [action.sceneState.id]: { $set: action.sceneState } },
};
if (previousState.scenes[action.sceneState.sceneId]) {
change.scenes = {
[action.sceneState.sceneId]: {
sceneStates: {
$add: [action.sceneState.id],
},
},
};
} else {
change.pendingJoins = {
scenes: {
[action.sceneState.sceneId]: {
$add: [action.sceneState.id],
},
},
};
}
newState = update(previousState, change);
break;
case "ROOM_DELETE": case "ROOM_DELETE":
if (!(action.roomId in previousState.rooms)) { if (!(action.roomId in previousState.rooms)) {
console.warn(`Room to delete ${action.roomId} does not exist`); console.warn(`Room to delete ${action.roomId} does not exist`);
@ -255,8 +345,8 @@ function reducer(previousState, action) {
// all devices again if consistent update is desired // all devices again if consistent update is desired
change = { states: { $unset: [] } }; change = { states: { $unset: [] } };
for (const id of previousState.scenes[action.sceneId].states) { for (const id of previousState.scenes[action.sceneId].sceneStates) {
change.states.$unset.push(id); change.sceneStates.$unset.push(id);
} }
change.scenes = { $unset: [action.sceneId] }; change.scenes = { $unset: [action.sceneId] };
@ -368,6 +458,8 @@ const initState = {
automations: {}, automations: {},
/** @type {[integer]Device} */ /** @type {[integer]Device} */
devices: {}, devices: {},
/** @type {[integer]SceneState} */
sceneStates: {},
}; };
function createSmartHutStore() { function createSmartHutStore() {

View file

@ -25,6 +25,15 @@ const actions = {
type: "DEVICE_SAVE", type: "DEVICE_SAVE",
device, device,
}), }),
stateSave: (sceneState) => ({
type: "STATE_SAVE",
sceneState,
}),
statesUpdate: (sceneId, sceneStates) => ({
type: "STATE_UPDATE",
sceneId,
sceneStates,
}),
devicesUpdate: (roomId, devices, partial = false) => ({ devicesUpdate: (roomId, devices, partial = false) => ({
type: "DEVICES_UPDATE", type: "DEVICES_UPDATE",
roomId, roomId,

View file

@ -1,9 +1,6 @@
import React, { Component } from "react"; import React, { Component } from "react";
import HomeNavbar from "./../components/HomeNavbar";
import { import {
Image, Image,
Divider,
Message,
Grid, Grid,
Button, Button,
Icon, Icon,

View file

@ -1,9 +1,6 @@
import React, { Component } from "react"; import React, { Component } from "react";
import HomeNavbar from "./../components/HomeNavbar";
import { import {
Image, Image,
Divider,
Message,
Grid, Grid,
Button, Button,
Icon, Icon,

View file

@ -1,9 +1,6 @@
import React, { Component } from "react"; import React, { Component } from "react";
import HomeNavbar from "./../components/HomeNavbar";
import { import {
Image, Image,
Divider,
Message,
Grid, Grid,
Button, Button,
Icon, Icon,

View file

@ -53,7 +53,7 @@ class Dashboard extends Component {
case "Devices": case "Devices":
return <DevicePanel tab={this.state.activeTab} />; return <DevicePanel tab={this.state.activeTab} />;
case "Scenes": case "Scenes":
return <ScenesPanel />; return <ScenesPanel tab={this.state.activeTab} />;
case "Automations": case "Automations":
return <AutomationsPanel />; return <AutomationsPanel />;
default: default:

View file

@ -19,7 +19,6 @@ class ScenesNavbar extends Component {
this.state = { this.state = {
editMode: false, editMode: false,
}; };
console.log(this.props.scenes);
this.toggleEditMode = this.toggleEditMode.bind(this); this.toggleEditMode = this.toggleEditMode.bind(this);
this.openCurrentModalMobile = this.openCurrentModalMobile.bind(this); this.openCurrentModalMobile = this.openCurrentModalMobile.bind(this);
this.selectScene = this.selectScene.bind(this); this.selectScene = this.selectScene.bind(this);
@ -41,14 +40,11 @@ class ScenesNavbar extends Component {
} }
getScenes() { getScenes() {
this.props this.props.fetchAllScenes().catch(console.error);
.fetchAllScenes()
.then(() => console.log(this.props.scenes))
.catch(console.error);
} }
openCurrentModalMobile() { openCurrentModalMobile() {
console.log(this.activeItemScene, this.props.sceneModalRefs); //console.log(this.activeItemScene, this.props.sceneModalRefs);
const currentModal = this.props.sceneModalRefs[this.activeItemScene] const currentModal = this.props.sceneModalRefs[this.activeItemScene]
.current; .current;
currentModal.openModal(); currentModal.openModal();
@ -60,6 +56,15 @@ class ScenesNavbar extends Component {
selectScene(e, { id }) { selectScene(e, { id }) {
this.activeItemScene = id || -1; this.activeItemScene = id || -1;
this.getStates(id);
}
getStates(sceneId) {
if (sceneId) {
this.props
.fetchStates(sceneId)
.catch((err) => console.error(`error fetching states:`, err));
}
} }
render() { render() {
@ -194,11 +199,14 @@ const setActiveScene = (activeScene) => {
const mapStateToProps = (state, _) => ({ const mapStateToProps = (state, _) => ({
scenes: state.scenes, scenes: state.scenes,
activeScene: state.active.activeScene,
sceneModalRefs: Object.keys(state.scenes).reduce( sceneModalRefs: Object.keys(state.scenes).reduce(
(acc, key) => ({ ...acc, [key]: React.createRef() }), (acc, key) => ({ ...acc, [key]: React.createRef() }),
{} {}
), ),
get isActiveDefaultScene() {
return state.active.activeScene === -1;
},
activeScene: state.active.activeScene,
}); });
const ScenesNavbarContainer = connect(mapStateToProps, { const ScenesNavbarContainer = connect(mapStateToProps, {
...RemoteService, ...RemoteService,