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() {
if (this.props.tab === "Devices") {
this.props
.fetchDevices()
.catch((err) => console.error(`error fetching devices:`, err));
}
}
render() {
return (

View file

@ -30,12 +30,13 @@ class NewSceneDevice extends Component {
this.state = {
openModal: false,
sceneDevices: this.props.scene ? this.props.scene.devices : {},
sceneDevices: this.props.scene ? this.props.scene.sceneStates : {},
deviceName: "",
};
this.getDevices();
this.setSceneDevice = this.setSceneDevice.bind(this);
this.setSceneState = this.setSceneState.bind(this);
this.createState = this.createState.bind(this);
}
getDevices() {
@ -56,10 +57,25 @@ class NewSceneDevice extends Component {
this.handleClose();
};
setSceneDevice(e, d) {
setSceneState(e, d) {
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() {
const availableDevices = [];
this.props.devices.forEach((e) => {
@ -83,7 +99,7 @@ class NewSceneDevice extends Component {
}
centered={true}
>
<Modal.Header>Add a New Scene Device</Modal.Header>
<Modal.Header>Add a New Scene State</Modal.Header>
<Modal.Content>
<Form>
<Form.Field style={{ marginTop: "1rem" }}>
@ -93,7 +109,7 @@ class NewSceneDevice extends Component {
placeholder="Select Devices"
fluid
multiple
onChange={this.setSceneDevice}
onChange={this.setSceneState}
options={availableDevices}
/>
</Form.Field>
@ -101,7 +117,7 @@ class NewSceneDevice extends Component {
</Modal.Content>
<Modal.Actions>
<Button
onClick={this.createDevice}
onClick={this.createState}
color="blue"
icon
labelPosition="right"

View file

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

View file

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

View file

@ -37,7 +37,6 @@ class Light extends Component {
constructor(props) {
super(props);
this.state = { intensity: this.props.device.intensity, timeout: null };
console.log(this.props);
this.iconOn = "/img/lightOn.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.
* 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) => {
if (!newState.scenes[scene.id]) {
newState = update(newState, {
scenes: { [scene.id]: { $set: { ...scene, states: new Set() } } },
scenes: { [scene.id]: { $set: { ...scene, sceneStates: new Set() } } },
});
} else {
newState = update(newState, {
@ -58,7 +58,7 @@ function reducer(previousState, action) {
pendingJoins: { scenes: { $unset: [scene.id] } },
scenes: {
[scene.id]: {
states: {
sceneStates: {
$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) {
case "LOGIN_UPDATE":
newState = update(previousState, { login: { $set: action.login } });
@ -94,10 +99,70 @@ function reducer(previousState, action) {
break;
case "SCENES_UPDATE":
newState = previousState;
console.log(action.scenes);
for (const scene of action.scenes) {
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;
case "DEVICES_UPDATE":
change = null;
@ -220,6 +285,31 @@ function reducer(previousState, action) {
}
newState = update(previousState, change);
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":
if (!(action.roomId in previousState.rooms)) {
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
change = { states: { $unset: [] } };
for (const id of previousState.scenes[action.sceneId].states) {
change.states.$unset.push(id);
for (const id of previousState.scenes[action.sceneId].sceneStates) {
change.sceneStates.$unset.push(id);
}
change.scenes = { $unset: [action.sceneId] };
@ -368,6 +458,8 @@ const initState = {
automations: {},
/** @type {[integer]Device} */
devices: {},
/** @type {[integer]SceneState} */
sceneStates: {},
};
function createSmartHutStore() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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