This commit is contained in:
britea 2020-05-04 13:59:15 +02:00
commit 46c1c3caf3
28 changed files with 761 additions and 540 deletions

View file

@ -1,189 +0,0 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { RemoteService } from "../remote";
import { appActions } from "../storeActions";
class AutomationModal extends Component {
constructor(props) {
super(props);
this.state = this.initialState;
this.setInitialState();
this.addAutomationModal = this.addAutomationModal.bind(this);
this.modifyAutomationModal = this.modifyAutomationModal.bind(this);
this.deleteAutomation = this.deleteAutomation.bind(this);
}
get initialState() {
return {
//INITIAL STATE HERE
};
}
setInitialState() {
this.setState(this.initialState);
}
get type() {
return !this.props.id ? "new" : "modify";
}
addAutomationModal = (e) => {
/*let data = {
// DATA HERE
};*/
// TODO CALL TO REMOTE SERVER TO ADD SCENE
/*this.props
.saveRoom(data, null)
.then(() => {
this.setInitialState();
this.closeModal();
})
.catch((err) => console.error("error in creating room", err));*/
};
modifyAutomationModal = (e) => {
/* let data = {
// DATA HERE
};*/
// TODO CALL TO REMOTE SERVER TO MODIFY SCENE
/*this.props
.saveRoom(data, this.props.id)
.then(() => {
this.setInitialState();
this.closeModal();
})
.catch((err) => console.error("error in updating room", err));*/
};
deleteAutomation = (e) => {
// TODO CALL TO REMOTE SERVER TO DELETE SCENE
/*
this.props
.deleteRoom(this.props.id)
.then(() => this.closeModal())
.catch((err) => console.error("error in deleting room", err));*/
};
changeSomething = (event) => {
let nam = event.target.name;
let val = event.target.value;
this.setState({ [nam]: val });
};
closeModal = (e) => {
this.setState({ openModal: false });
};
openModal = (e) => {
this.setState({ openModal: true });
};
render() {
return (
<div>
{/*
{!this.props.nicolaStop ? (
<div>
<Responsive minWidth={768}>
{this.type === "new" ? (
<Button
icon
labelPosition="left"
inverted
onClick={this.openModal}
>
<Icon name="plus" size="small" />
ADD AUTOMATION
</Button>
) : (
<Icon name="pencil" size="small" onClick={this.openModal} />
)}
</Responsive>
<Responsive maxWidth={768}>
{this.type === "new" ? (
<Button
icon
fluid
labelPosition="left"
onClick={this.openModal}
>
<Icon name="plus" size="small" />
ADD AUTOMATION
</Button>
) : (
<Button
icon
fluid
labelPosition="left"
onClick={this.openModal}
>
<Icon name="pencil" size="small" />
EDIT AUTOMATION
</Button>
)}
</Responsive>
</div>
) : null}
<Modal closeIcon onClose={this.closeModal} open={this.state.openModal}>
<Header>
{this.type === "new" ? "Add new automation" : "Modify automation"}
</Header>
<Modal.Content>
{
//TODO FORM TO ADD OR MODIFY SCENE
}
{this.type === "modify" ? (
<Button
icon
labelPosition="left"
inverted
color="red"
onClick={this.deleteAutomation}
>
<Icon name="trash alternate" />
Delete Automation
</Button>
) : null}
</Modal.Content>
<Modal.Actions>
<Button color="red" onClick={this.closeModal}>
<Icon name="remove" />{" "}
{this.type === "new" ? "Cancel" : "Discard changes"}
</Button>
<Button
color="green"
onClick={
this.type === "new"
? this.addAutomationModal
: this.modifyAutomationModal
}
>
<Icon name="checkmark" />{" "}
{this.type === "new" ? "Add automation" : "Save changes"}
</Button>
</Modal.Actions>
</Modal>*/}
</div>
);
}
}
const setActiveAutomation = (activeAutomation) => {
return (dispatch) =>
dispatch(appActions.setActiveAutomation(activeAutomation));
};
const mapStateToProps = (state, ownProps) => ({
automations: ownProps.id ? state.automations[ownProps.id] : null,
});
const AutomationModalContainer = connect(
mapStateToProps,
{ ...RemoteService, setActiveAutomation },
null,
{ forwardRef: true }
)(AutomationModal);
export default AutomationModalContainer;

View file

@ -0,0 +1,137 @@
import React, { Component } from "react";
import {
Button,
Header,
Modal,
Icon,
Form,
Responsive,
Dropdown,
} from "semantic-ui-react";
import { connect } from "react-redux";
import { RemoteService, Forms } from "../remote";
import { appActions } from "../storeActions";
//import { update } from "immutability-helper";
class HostModal extends Component {
constructor(props) {
super(props);
this.state = { guests: [], users: [] };
this.props
.fetchGuests()
.then(() => {
this.setState({
...this.state,
guests: this.props.guests.map((u) => u.id),
});
})
.catch(console.error);
Forms.fetchAllUsers()
.then((users) =>
this.setState({
...this.state,
users: users.map((u) => ({
key: u.id,
text: `@${u.username} (${u.name})`,
value: u.id,
})),
})
)
.catch(console.error);
this.saveGuestSettings = this.saveGuestSettings.bind(this);
this.closeModal = this.closeModal.bind(this);
this.openModal = this.openModal.bind(this);
this.setGuests = this.setGuests.bind(this);
this.saveGuestSettings = this.saveGuestSettings.bind(this);
}
setGuests(_, guests) {
this.setState({ guests: guests.value });
}
closeModal() {
this.setState({ openModal: false });
}
openModal() {
this.setState({ openModal: true });
}
saveGuestSettings() {
this.props
.updateGuests(this.state.guests)
.then(this.closeModal)
.catch(console.error);
}
render() {
return (
<React.Fragment>
<Responsive minWidth={768}>
<Button icon labelPosition="left" inverted onClick={this.openModal}>
<Icon name="plus" size="small" />
Invitation settings
</Button>
</Responsive>
<Responsive maxWidth={768}>
<Button icon fluid labelPosition="left" onClick={this.openModal}>
<Icon name="plus" size="small" />
Invitation settings
</Button>
</Responsive>
<Modal closeIcon onClose={this.closeModal} open={this.state.openModal}>
<Header>Select guests</Header>
<Modal.Content>
<marquee scrollamount="50">
<h1>Spaghetti!</h1>
</marquee>
<Modal.Content>
<Form>
<Form.Field style={{ marginTop: "1rem" }}>
<label>Select which users are your guests: </label>
<Dropdown
name="guests"
placeholder="Select Guests"
fluid
multiple
onChange={this.setGuests}
options={this.state.users}
value={this.state.guests}
/>
</Form.Field>
</Form>
</Modal.Content>
</Modal.Content>
<Modal.Actions>
<Button color="red" onClick={this.closeModal}>
<Icon name="remove" /> Discard changes
</Button>
<Button color="green" onClick={this.saveGuestSettings}>
<Icon name="checkmark" /> Save changes
</Button>
</Modal.Actions>
</Modal>
</React.Fragment>
);
}
}
const setActiveHost = (activeHost) => {
return (dispatch) => dispatch(appActions.setActiveHost(activeHost));
};
const mapStateToProps = (state) => ({
guests: state.guests,
});
const HostModalContainer = connect(
mapStateToProps,
{ ...RemoteService, setActiveHost },
null,
{ forwardRef: true }
)(HostModal);
export default HostModalContainer;

View file

@ -21,7 +21,6 @@ class RoomModal extends Component {
constructor(props) {
super(props);
this.state = this.initialState;
this.setInitialState();
this.fileInputRef = React.createRef();

View file

@ -255,8 +255,6 @@ class AutomationSaveModal extends Component {
}
_checkNewTrigger(trigger) {
const auxDevice = this.props.devices[trigger.device];
// Check for missing fields for creation
const error = {
result: false,
@ -271,6 +269,8 @@ class AutomationSaveModal extends Component {
case "rangeTrigger":
if (!trigger.device || !trigger.operand || !trigger.value) return error;
break;
default:
throw new Error("theoretically unreachable statement");
}
const isNotDuplicate = !this.state.triggerList.some(
@ -345,6 +345,8 @@ class AutomationSaveModal extends Component {
return "" + trigger.device + trigger.on;
case "rangeTrigger":
return "" + trigger.device + trigger.operand + trigger.value;
default:
throw new Error("theoretically unreachable statement");
}
};

View file

@ -1,4 +1,4 @@
import React, { Component, useState, useEffect } from "react";
import React, { Component } from "react";
import { connect } from "react-redux";
import { RemoteService } from "../../remote";
import "./Automations.css";

View file

@ -29,7 +29,7 @@ class DevicePanel extends Component {
return <Device key={i} tab={this.props.tab} id={e.id} />;
})}
{!this.props.isActiveRoomHome ? (
<Card style={{ height: "23em" }}>
<Card style={{ height: "27em" }}>
<Segment basic style={{ width: "100%", height: "100%" }}>
<NewDevice />
</Segment>

View file

@ -0,0 +1,51 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { RemoteService } from "../../remote";
import { Card, Segment, Header, Icon } from "semantic-ui-react";
import Device from "../../components/dashboard/devices/Device";
class HostsPanel extends Component {
componentDidUpdate(oldProps) {
if (
oldProps.activeHost !== this.props.activeHost &&
this.props.activeHost !== -1
) {
this.props.fetchDevices(null, this.props.activeHost).catch(console.error);
this.props.fetchAllRooms(this.props.activeHost).catch(console.error);
}
}
render() {
return (
<Card.Group centered style={{ paddingTop: "3rem" }}>
{this.props.isActiveDefaultHost && (
<Segment placeholder>
<Header icon>
<Icon name="folder open" />
Please select a host to visit on the left.
</Header>
</Segment>
)}
{this.props.hostDeviceIds.map((id) => {
return (
<Device
key={id}
hostId={this.props.activeHost}
tab="Hosts"
id={id}
/>
);
})}
</Card.Group>
);
}
}
const mapStateToProps = (state, _) => ({
isActiveDefaultHost: state.active.activeHost === -1,
activeHost: state.active.activeHost,
hostDevices: state.hostDevices,
hostDeviceIds: Object.keys(state.hostDevices[state.active.activeHost] || {}),
});
const HostsPanelContainer = connect(mapStateToProps, RemoteService)(HostsPanel);
export default HostsPanelContainer;

View file

@ -48,6 +48,7 @@ class NewSceneDevice extends Component {
handleOpen = () => {
this.setState({ openModal: true });
};
handleClose = () => {
this.setState({ openModal: false });
};

View file

@ -3,7 +3,7 @@ import { connect } from "react-redux";
import { RemoteService } from "../../remote";
import Device from "./devices/Device";
import NewSceneDevice from "./NewSceneDevice";
import { Grid, Button, Card, Segment, Header } from "semantic-ui-react";
import { Button, Card, Segment, Header, Icon } from "semantic-ui-react";
class ScenesPanel extends Component {
constructor(props) {
@ -42,7 +42,12 @@ class ScenesPanel extends Component {
</Card.Content>
</Card>
) : (
<Grid.Column>Welcome to the Scene View, you add a Scene</Grid.Column>
<Segment placeholder>
<Header icon>
<Icon name="folder open" />
Please select a scene on the left or add a new one.
</Header>
</Segment>
)}
{!this.props.isActiveDefaultScene
? this.props.sceneStates.map((e, i) => {
@ -60,7 +65,6 @@ const mapStateToProps = (state, _) => ({
const stateArray = [
...state.scenes[state.active.activeScene].sceneStates,
].sort();
console.log("STATESCENE", stateArray);
return stateArray.map((id) => state.sceneStates[id]);
} else {
return [];

View file

@ -2,6 +2,7 @@ import React, { Component } from "react";
import "./Curtains.css";
import { RemoteService } from "../../../remote";
import { connect } from "react-redux";
import mapStateToProps from "../../../deviceProps";
class Curtain extends Component {
constructor(props) {
@ -100,6 +101,7 @@ class Curtain extends Component {
{Math.round(this.props.stateOrDevice.intensity)}%
</span>
<input
disabled={this.props.disabled}
onChange={this.handleChange}
value={this.props.stateOrDevice.intensity}
className="slider"
@ -112,15 +114,5 @@ class Curtain extends Component {
}
}
const mapStateToProps = (state, ownProps) => ({
get stateOrDevice() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
return state.sceneStates[ownProps.id];
}
},
//device: state.devices[ownProps.id],
});
const CurtainContainer = connect(mapStateToProps, RemoteService)(Curtain);
export default CurtainContainer;

View file

@ -7,10 +7,11 @@ import Switcher from "./Switch";
import Videocam from "./Videocam";
import Curtains from "./Curtain";
import Thermostat from "./Thermostats";
import { Segment, Grid, Header, Button, Icon, Card } from "semantic-ui-react";
import { Header, Button, Icon, Card } from "semantic-ui-react";
import { RemoteService } from "../../../remote";
import { connect } from "react-redux";
import DeviceSettingsModal from "./DeviceSettingsModal";
import mapStateToProps from "../../../deviceProps";
const centerComponent = {
marginLeft: "50%",
@ -41,75 +42,37 @@ class Device extends React.Component {
}
deleteState() {
console.log("alpaca ", this.props);
this.props.deleteState(this.props.id, this.props.stateOrDevice.kind);
}
renderDeviceComponent() {
switch (
this.props.tab === "Devices"
? this.props.stateOrDevice.kind
: this.props.type
) {
case "curtains":
return (
<Curtains
tab={this.props.tab}
sceneState={this.props.sceneState}
id={this.props.id}
/>
);
case "thermostat":
return (
<Thermostat
tab={this.props.tab}
sceneState={this.props.sceneStat}
id={this.props.stateOrDevice.id}
/>
);
case "regularLight":
return (
<Light
tab={this.props.tab}
sceneState={this.props.sceneState}
id={this.props.stateOrDevice.id}
/>
);
case "sensor":
return (
<Sensor
tab={this.props.tab}
sceneState={this.props.sceneState}
id={this.props.stateOrDevice.id}
/>
);
case "motionSensor":
return <Sensor tab={this.props.tab} id={this.props.stateOrDevice.id} />;
case "buttonDimmer":
return (
<ButtonDimmer tab={this.props.tab} id={this.props.stateOrDevice.id} />
);
case "knobDimmer":
return (
<KnobDimmer tab={this.props.tab} id={this.props.stateOrDevice.id} />
);
case "smartPlug":
return (
<SmartPlug tab={this.props.tab} id={this.props.stateOrDevice.id} />
);
case "switch":
return (
<Switcher tab={this.props.tab} id={this.props.stateOrDevice.id} />
);
case "dimmableLight":
return <Light id={this.props.stateOrDevice.id} tab={this.props.tab} />;
case "securityCamera":
return (
<Videocam id={this.props.stateOrDevice.id} tab={this.props.tab} />
);
default:
throw new Error("Device type unknown");
const mapKindToComponent = {
curtains: Curtains,
thermostat: Thermostat,
regularLight: Light,
sensor: Sensor,
motionSensor: Sensor,
buttonDimmer: ButtonDimmer,
knobDimmer: KnobDimmer,
smartPlug: SmartPlug,
switch: Switcher,
dimmableLight: Light,
securityCamera: Videocam,
};
if (!(this.props.type in mapKindToComponent)) {
throw new Error(`device kind ${this.props.type} not known`);
}
return React.createElement(
mapKindToComponent[this.props.type],
{
tab: this.props.tab,
id: this.props.id,
hostId: this.props.hostId,
},
""
);
}
deviceDescription() {
@ -151,89 +114,40 @@ class Device extends React.Component {
}
get deviceName() {
return this.props.tab === "Devices"
? this.props.stateOrDevice.name
: this.props.device.name;
return this.props.device.name;
}
render() {
{
return (
<Card
style={{ height: this.props.tab === "Devices" ? "23em" : "27em" }}
>
<Card.Content>
<Card.Header textAlign="center">
<Header as="h3">{this.deviceName}</Header>
{this.props.tab === "Scenes" ? (
<Header as="h4">{this.props.roomName}</Header>
) : (
""
)}
</Card.Header>
return (
<Card style={{ height: "27em" }}>
<Card.Content>
<Card.Header textAlign="center">
<Header as="h3">{this.deviceName}</Header>
<Header as="h4">{this.props.roomName}</Header>
</Card.Header>
<Card.Description
style={
this.props.device.kind !== "curtains" ? centerComponent : null
}
>
{this.renderDeviceComponent()}
</Card.Description>
</Card.Content>
<Card.Content extra>
{this.props.tab === "Devices"
? this.deviceDescription()
: this.stateDescription()}
</Card.Content>
{this.props.tab === "Devices" ? (
<DeviceSettingsModal
ref={this.modalRef}
id={this.props.stateOrDevice.id}
/>
) : null}
</Card>
);
}
<Card.Description
style={
this.props.device.kind !== "curtains" ? centerComponent : null
}>
{this.renderDeviceComponent()}
</Card.Description>
</Card.Content>
<Card.Content extra>
{this.props.tab === "Devices"
? this.deviceDescription()
: this.props.tab === "Scenes" && this.stateDescription()}
</Card.Content>
{this.props.tab === "Devices" ? (
<DeviceSettingsModal
ref={this.modalRef}
id={this.props.stateOrDevice.id}
/>
) : null}
</Card>
);
}
}
const mapStateToProps = (state, ownProps) => ({
get stateOrDevice() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
return state.sceneStates[ownProps.id];
}
},
get device() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
return state.devices[state.sceneStates[ownProps.id].deviceId];
}
},
get roomName() {
if (state.active.activeTab === "Scenes") {
const device = state.devices[state.sceneStates[ownProps.id].deviceId];
return state.rooms[device.roomId].name;
} else {
return "";
}
},
get type() {
if (state.active.activeTab === "Scenes") {
if (state.sceneStates[ownProps.id]) {
//console.log(state.sceneStates[ownProps.id], ownProps.id);
const id = state.sceneStates[ownProps.id].deviceId;
//console.log(id, state.devices[id].kind);
return state.devices[id].kind;
} else {
return "";
}
} else {
return null;
}
},
});
const DeviceContainer = connect(mapStateToProps, RemoteService)(Device);
export default DeviceContainer;

View file

@ -29,6 +29,7 @@ import {
} from "./DimmerStyle";
import { RemoteService } from "../../../remote";
import { connect } from "react-redux";
import mapStateToProps from "../../../deviceProps";
export class ButtonDimmerComponent extends Component {
increaseIntensity = () => {
@ -104,7 +105,7 @@ export class KnobDimmerComponent extends Component {
<CircularInput
style={KnobDimmerStyle}
value={+(Math.round(this.state.intensity / 100 + "e+2") + "e-2")}
onChange={this.setIntensity}
onChange={this.props.disabled ? null : this.setIntensity}
>
<text
style={textStyle}
@ -128,15 +129,6 @@ export class KnobDimmerComponent extends Component {
}
}
const mapStateToProps = (state, ownProps) => ({
get stateOrDevice() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
return state.sceneStates[ownProps.id];
}
},
});
const conn = connect(mapStateToProps, RemoteService);
export const KnobDimmer = conn(KnobDimmerComponent);

View file

@ -31,6 +31,7 @@ import {
} from "./LightStyle";
import { RemoteService } from "../../../remote";
import { connect } from "react-redux";
import mapStateToProps from "../../../deviceProps";
class Light extends Component {
constructor(props) {
@ -62,14 +63,17 @@ class Light extends Component {
}
get intensity() {
return this.props.stateOrDevice.intensity || 0;
return this.state.intensity || 0;
}
onClickDevice = () => {
const on = !this.turnedOn;
if (this.props.tab === "Devices") {
if (this.props.tab !== "Scenes") {
this.props
.saveDevice({ ...this.props.stateOrDevice, on })
.saveDevice(
{ ...this.props.stateOrDevice, on },
this.props.tab === "Hosts" ? this.props.activeHost : null
)
.catch((err) => console.error("regular light update error", err));
} else {
if (this.props.device.kind === "regularLight") {
@ -114,12 +118,14 @@ class Light extends Component {
saveIntensity = () => {
const intensity = Math.round(this.state.intensity);
if (this.props.tab === "Devices") {
if (this.props.tab !== "Scenes") {
this.props
.saveDevice({ ...this.props.stateOrDevice, intensity })
.catch((err) => console.error("regular light update error", err));
.saveDevice(
{ ...this.props.stateOrDevice, intensity },
this.props.tab === "Hosts" ? this.props.activeHost : null
)
.catch((err) => console.error("dimmable light update error", err));
} else {
console.log("CIAOOOOOOOOO", this.props.stateOrDevice);
this.props
.updateState(
{ id: this.props.stateOrDevice.id, intensity: intensity },
@ -136,9 +142,9 @@ class Light extends Component {
<div style={LightDimmerContainer}>
<CircularInput
style={LightDimmerStyle}
value={+(Math.round(this.state.intensity / 100 + "e+2") + "e-2")}
onChange={this.setIntensity}
onMouseUp={this.saveIntensity}
value={+(Math.round(this.intensity / 100 + "e+2") + "e-2")}
onChange={this.props.disabled ? null : this.setIntensity}
onMouseUp={this.props.disabled ? null : this.saveIntensity}
>
<text
style={textStyle}
@ -153,7 +159,7 @@ class Light extends Component {
<CircularProgress
style={{
...KnobProgress,
opacity: this.state.intensity / 100 + 0.3,
opacity: this.intensity / 100 + 0.3,
}}
/>
<CircularThumb style={CircularThumbStyle} />
@ -184,22 +190,5 @@ class Light extends Component {
}
}
const mapStateToProps = (state, ownProps) => ({
get stateOrDevice() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
return state.sceneStates[ownProps.id];
}
},
get device() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
return state.devices[state.sceneStates[ownProps.id].deviceId];
}
},
});
const LightContainer = connect(mapStateToProps, RemoteService)(Light);
export default LightContainer;

View file

@ -38,6 +38,7 @@ import {
import { Image } from "semantic-ui-react";
import { RemoteService } from "../../../remote";
import { connect } from "react-redux";
import mapStateToProps from "../../../deviceProps";
class Sensor extends Component {
constructor(props) {
@ -194,14 +195,5 @@ class Sensor extends Component {
}
}
const mapStateToProps = (state, ownProps) => ({
get stateOrDevice() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
return state.sceneStates[ownProps.id];
}
},
});
const SensorContainer = connect(mapStateToProps, RemoteService)(Sensor);
export default SensorContainer;

View file

@ -15,6 +15,7 @@ import {
} from "./SmartPlugStyle";
import { RemoteService } from "../../../remote";
import { connect } from "react-redux";
import mapStateToProps from "../../../deviceProps";
class SmartPlug extends Component {
constructor(props) {
@ -51,7 +52,7 @@ class SmartPlug extends Component {
render() {
return (
<StyledDiv onClick={this.onClickDevice}>
<StyledDiv onClick={this.props.disabled ? () => {} : this.onClickDevice}>
<Image src={this.getIcon()} style={imageStyle} />
<span style={nameStyle}>Smart Plug</span>
@ -70,21 +71,5 @@ class SmartPlug extends Component {
}
}
const mapStateToProps = (state, ownProps) => ({
get stateOrDevice() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
return state.sceneStates[ownProps.id];
}
},
get device() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
return state.devices[state.sceneStates[ownProps.id].deviceId];
}
},
});
const SmartPlugContainer = connect(mapStateToProps, RemoteService)(SmartPlug);
export default SmartPlugContainer;

View file

@ -11,6 +11,7 @@ import { Image } from "semantic-ui-react";
import { imageStyle, nameStyle, turnedOnStyle } from "./SwitchStyle";
import { RemoteService } from "../../../remote";
import { connect } from "react-redux";
import mapStateToProps from "../../../deviceProps";
class Switch extends Component {
constructor(props) {
@ -37,7 +38,7 @@ class Switch extends Component {
render() {
return (
<StyledDiv onClick={this.onClickDevice}>
<StyledDiv onClick={this.props.disabled ? () => {} : this.onClickDevice}>
<Image src={this.getIcon()} style={imageStyle} />
<span style={nameStyle}>Switch</span>
@ -55,8 +56,5 @@ class Switch extends Component {
}
}
const mapStateToProps = (state, ownProps) => ({
device: state.devices[ownProps.id],
});
const SwitchContainer = connect(mapStateToProps, RemoteService)(Switch);
export default SwitchContainer;

View file

@ -28,4 +28,4 @@
background: white;
border-radius: 5px;
}
*/
*/

View file

@ -5,6 +5,7 @@ import { connect } from "react-redux";
import "./Thermostat.css";
import Slider from "react-rangeslider";
import "react-rangeslider/lib/index.css";
import mapStateToProps from "../../../deviceProps";
import {
stateTag,
@ -24,7 +25,6 @@ class Thermostats extends Component {
measuredTemperature: this.props.device.measuredTemperature,
useExternalSensors: this.props.device.useExternalSensors,
};
console.log(this.state);
this.setMode = this.setMode.bind(this);
this.setTargetTemperature = this.setTargetTemperature.bind(this);
}
@ -33,13 +33,11 @@ class Thermostats extends Component {
if (this.state.timeout) {
clearTimeout(this.state.timeout);
}
console.log(mode);
//i came to the conclusion that is not possible to set mode.
// Good job Jacob (Claudio)
//this.mode = "HEATING";
const turnOn = mode;
console.log(turnOn);
if (this.props.tab === "Devices") {
this.props
.saveDevice({ ...this.props.stateOrDevice, turnOn })
@ -87,43 +85,6 @@ class Thermostats extends Component {
}
}
setTargetTemperature(newTemp) {
if (this.state.timeout) {
clearTimeout(this.state.timeout);
}
this.setState({
newTemp,
timeout: setTimeout(() => {
this.saveTargetTemperature(newTemp);
this.setState({
targetTemperature: this.state.targetTemperature,
timeout: null,
});
}, 100),
});
}
saveTargetTemperature(targetTemperature) {
if (this.props.tab === "Devices") {
this.props
.saveDevice({
...this.props.device,
turnOn: this.props.device.mode !== "OFF",
targetTemperature,
})
.catch((err) => console.error("thermostat update error", err));
} else {
this.props.updateState(
{
id: this.props.stateOrDevice.id,
targetTemperature: targetTemperature,
},
this.props.stateOrDevice.kind
);
}
}
setTargetTemperature() {
this.saveTargetTemperature(this.state.targetTemperature);
}
@ -138,6 +99,7 @@ class Thermostats extends Component {
<h3 style={deviceName}>
Thermostat
<Checkbox
disabled={this.props.disabled}
checked={
this.props.tab === "Devices"
? this.props.device.mode !== "OFF"
@ -157,6 +119,7 @@ class Thermostats extends Component {
</div>
{this.props.tab === "Devices" ? (
<Slider
disabled={this.props.disabled}
min={10}
max={30}
step={0.1}
@ -169,7 +132,7 @@ class Thermostats extends Component {
) : null}
<div style={stateTagContainer}>
<span style={stateTag}>
{this.props.tab === "Devices"
{this.props.tab !== "Scenes"
? this.props.device.mode
: this.props.stateOrDevice.on
? "WILL TURN ON"
@ -181,24 +144,6 @@ class Thermostats extends Component {
}
}
const mapStateToProps = (state, ownProps) => ({
get stateOrDevice() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
return state.sceneStates[ownProps.id];
}
},
get device() {
if (state.active.activeTab === "Devices") {
return state.devices[ownProps.id];
} else {
return state.devices[state.sceneStates[ownProps.id].deviceId];
}
},
activeTab: state.activeTab,
});
const ThermostatContainer = connect(
mapStateToProps,
RemoteService

View file

@ -7,6 +7,7 @@ import { RemoteService } from "../../../remote";
import { endpointURL } from "../../../endpoint";
import { connect } from "react-redux";
import VideocamModal from "./VideocamModal";
import mapStateToProps from "../../../deviceProps";
class Videocam extends Component {
constructor(props) {
@ -86,12 +87,5 @@ class Videocam extends Component {
}
}
const mapStateToProps = (state, ownProps) => ({
device:
ownProps.tab === "Devices"
? state.devices[ownProps.id]
: state.devices[state.sceneStates[ownProps.id].deviceId],
state: state.sceneStates[ownProps.id],
});
const VideocamContainer = connect(mapStateToProps, RemoteService)(Videocam);
export default VideocamContainer;

View file

@ -0,0 +1,69 @@
function getStateOrDevice(state, ownProps) {
switch (state.active.activeTab) {
case "Devices":
return state.devices[ownProps.id];
case "Scenes":
return state.sceneStates[ownProps.id];
case "Hosts":
return state.hostDevices[ownProps.hostId][ownProps.id];
default:
throw new Error(
`stateOrDevice has no value in tab "${state.active.activeTab}"`
);
}
}
function getDevice(state, ownProps) {
switch (state.active.activeTab) {
case "Scenes":
return state.devices[getStateOrDevice(state, ownProps).deviceId];
case "Devices":
case "Hosts":
return getStateOrDevice(state, ownProps);
default:
throw new Error(`device has no value in tab "${state.active.activeTab}"`);
}
}
function getRoomName(state, ownProps) {
switch (state.active.activeTab) {
case "Scenes":
case "Devices":
return (state.rooms[getDevice(state, ownProps).roomId] || {}).name;
case "Hosts":
const hostRooms = state.hostRooms[ownProps.hostId];
if (!hostRooms) return "";
const room = hostRooms[getDevice(state, ownProps).roomId];
if (!room) return "";
return room.name;
default:
throw new Error(
`room name has no value in tab "${state.active.activeTab}"`
);
}
}
export default function mapStateToProps(state, ownProps) {
return {
activeHost: state.active.activeHost,
get stateOrDevice() {
return getStateOrDevice(state, ownProps);
},
get device() {
return getDevice(state, ownProps);
},
get roomName() {
return getRoomName(state, ownProps);
},
get type() {
return getDevice(state, ownProps).kind;
},
get disabled() {
return (
ownProps.tab === "Hosts" &&
["dimmableLight", "light"].indexOf(getDevice(state, ownProps).kind) ===
-1
);
},
};
}

View file

@ -261,13 +261,22 @@ export const RemoteService = {
/**
* Fetches all rooms that belong to this user. This call does not
* populate the devices attribute in rooms.
* @param {Number|null} hostId the user id of the host we need to fetch the rooms from.
* Null if we need to fetch our own rooms.
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
fetchAllRooms: () => {
fetchAllRooms: (hostId = null) => {
return (dispatch) => {
return Endpoint.get("/room")
.then((res) => void dispatch(actions.roomsUpdate(res.data)))
return Endpoint.get("/room", hostId ? { hostId } : null)
.then(
(res) =>
void dispatch(
hostId
? actions.hostRoomsUpdate(hostId, res.data)
: actions.roomsUpdate(res.data)
)
)
.catch((err) => {
console.error("Fetch all rooms error", err);
throw new RemoteError(["Network error"]);
@ -297,13 +306,25 @@ export const RemoteService = {
* This also updates the devices attribute on values in the map rooms.
* @param {Number|null} roomId the rsoom to which fetch devices
* from, null to fetch from all rooms
* @param {Number|null} hostId the user id of the owner of the devices to get
* (can be used for host view)
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
fetchDevices: (roomId = null) => {
fetchDevices: (roomId = null, hostId = null) => {
return (dispatch) => {
return Endpoint.get(roomId ? `/room/${roomId}/device` : "/device")
.then((res) => void dispatch(actions.devicesUpdate(roomId, res.data)))
return Endpoint.get(
roomId ? `/room/${roomId}/device` : "/device",
hostId ? { hostId } : null
)
.then(
(res) =>
void dispatch(
!hostId
? actions.devicesUpdate(roomId, res.data, hostId)
: actions.hostDevicesUpdate(hostId, res.data)
)
)
.catch((err) => {
console.error(`Fetch devices roomId=${roomId} error`, err);
throw new RemoteError(["Network error"]);
@ -346,6 +367,56 @@ export const RemoteService = {
};
},
/**
* Fetches all hosts of a particular user.
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
fetchHosts: () => {
return (dispatch) => {
return Endpoint.get(`/user/hosts`)
.then((res) => void dispatch(actions.hostsUpdate(res.data)))
.catch((err) => {
console.error(`Fetch hosts error`, err);
throw new RemoteError(["Network error"]);
});
};
},
/**
* Fetches all guests of a particular user.
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
fetchGuests: () => {
return (dispatch) => {
return Endpoint.get(`/user/guests`)
.then((res) => void dispatch(actions.guestsUpdate(res.data)))
.catch((err) => {
console.error(`Fetch guests error`, err);
throw new RemoteError(["Network error"]);
});
};
},
/**
* Adds the current user as a guest to another user
* identified through a user id.
* @param {Number[]} userId the users to add.
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
updateGuests: (userIds) => {
return (dispatch) => {
return Endpoint.put(`/user/guests`, {}, { ids: userIds })
.then((res) => void dispatch(actions.guestsUpdate(res.data)))
.catch((err) => {
console.error(`Guest save error`, err);
throw new RemoteError(["Network Error"]);
});
};
},
/**
* Creates/Updates a room with the given data
* @param {String} data.name the room's name,
@ -458,16 +529,24 @@ export const RemoteService = {
* @returns {Promise<Device, RemoteError>} promise that resolves to the saved device and rejects
* with user-fiendly errors as a RemoteError
*/
saveDevice: (data) => {
saveDevice: (data, hostId = null) => {
return (dispatch) => {
let url = "/device";
if ((data.id && data.flowType === "OUTPUT") || !data.id) {
url = "/" + data.kind;
}
return Endpoint[data.id ? "put" : "post"](url, {}, data)
return Endpoint[data.id ? "put" : "post"](
url,
hostId ? { hostId } : {},
data
)
.then((res) => {
dispatch(actions.deviceSave(res.data));
dispatch(
hostId
? actions.hostDeviceSave(hostId, res.data)
: actions.deviceSave(res.data)
);
return res.data;
})
.catch((err) => {
@ -793,6 +872,15 @@ for (const key in RemoteService) {
}
export class Forms {
static fetchAllUsers() {
return Endpoint.get(`/user`)
.then((res) => res.data)
.catch((err) => {
console.error(`Fetch users error`, err);
throw new RemoteError(["Network error"]);
});
}
/**
* Attempts to create a new user from the given data.
* This method does not update the global state,

View file

@ -3,6 +3,7 @@ import thunk from "redux-thunk";
import update from "immutability-helper";
import reduxWebSocket, { connect } from "@giantmachines/redux-websocket";
import { socketURL } from "./endpoint";
import actions from "./storeActions";
function reducer(previousState, action) {
let newState, change;
@ -99,6 +100,20 @@ function reducer(previousState, action) {
createOrUpdateRoom(room);
}
break;
case "HOST_ROOMS_UPDATE":
change = {
hostRooms: {
[action.hostId]: { $set: {} },
},
};
const rooms = change.hostRooms[action.hostId].$set;
for (const room of action.rooms) {
rooms[room.id] = room;
}
newState = update(previousState, change);
break;
case "SCENES_UPDATE":
newState = previousState;
for (const scene of action.scenes) {
@ -255,7 +270,27 @@ function reducer(previousState, action) {
newState = update(newState, change);
break;
case "HOST_DEVICES_UPDATE":
newState = action.partial
? previousState
: update(previousState, {
hostDevices: { [action.hostId]: { $set: {} } },
});
newState.hostDevices[action.hostId] =
newState.hostDevices[action.hostId] || {};
change = {
hostDevices: {
[action.hostId]: {},
},
};
const deviceMap = change.hostDevices[action.hostId];
for (const device of action.devices) {
deviceMap[device.id] = { $set: device };
}
newState = update(newState, change);
break;
case "AUTOMATION_UPDATE":
const automations = {};
for (const automation of action.automations) {
@ -299,6 +334,28 @@ function reducer(previousState, action) {
}
newState = update(previousState, change);
break;
case "HOST_DEVICE_SAVE":
change = {
hostDevices: {
[action.hostId]: {
[action.device.id]: {
$set: action.device,
},
},
},
};
newState = update(previousState, change);
break;
case "HOST_DEVICES_DELETE":
change = {
hostDevices: {
[action.hostId]: {
$unset: [action.deviceIds],
},
},
};
newState = update(previousState, change);
break;
case "AUTOMATION_SAVE":
change = {
@ -433,51 +490,62 @@ function reducer(previousState, action) {
case "LOGOUT":
newState = update(initState, {});
break;
case "SET_ACTIVE_ROOM":
case "SET_ACTIVE":
newState = update(previousState, {
active: {
activeRoom: {
$set: action.activeRoom,
},
},
});
break;
case "SET_ACTIVE_TAB":
newState = update(previousState, {
active: {
activeTab: {
$set: action.activeTab,
},
},
});
break;
case "SET_ACTIVE_SCENE":
newState = update(previousState, {
active: {
activeScene: {
$set: action.activeScene,
},
},
});
break;
case "SET_ACTIVE_AUTOMATION":
newState = update(previousState, {
active: {
activeAutomation: {
$set: action.activeAutomation,
[action.key]: {
$set: action.value,
},
},
});
break;
case "REDUX_WEBSOCKET::MESSAGE":
const devices = JSON.parse(action.payload.message);
//console.log("socket", JSON.stringify(devices, null, 2));
const allDevices = JSON.parse(action.payload.message);
const devices = allDevices.filter(
(d) =>
(d.fromHostId === null || d.fromHostId === undefined) && !d.deleted
);
const hostDevicesMapByHostId = allDevices
.filter((d) => d.fromHostId)
.reduce((a, e) => {
const hostId = e.fromHostId;
//delete e.fromHostId;
a[hostId] = a[hostId] || { updated: [], deletedIds: [] };
if (e.deleted) {
a[hostId].deletedIds.push(e.id);
} else {
a[hostId].updated.push(e);
}
return a;
}, {});
newState = reducer(previousState, {
type: "DEVICES_UPDATE",
partial: true,
devices,
});
for (const hostId in hostDevicesMapByHostId) {
if (hostDevicesMapByHostId[hostId].updated.length > 0)
newState = reducer(newState, {
type: "HOST_DEVICES_UPDATE",
devices: hostDevicesMapByHostId[hostId].updated,
partial: true,
hostId,
});
if (hostDevicesMapByHostId[hostId].deletedIds.length > 0) {
newState = reducer(newState, {
type: "HOST_DEVICES_DELETE",
deviceIds: hostDevicesMapByHostId[hostId].deletedIds,
partial: true,
hostId,
});
}
}
break;
case "HG_UPDATE":
newState = update(previousState, {
[action.key]: { $set: action.value },
});
break;
default:
console.warn(`Action type ${action.type} unknown`, action);
@ -497,6 +565,7 @@ const initState = {
activeTab: "Devices",
activeScene: -1,
activeAutomation: -1,
activeHost: -1,
},
login: {
loggedIn: false,
@ -513,6 +582,14 @@ const initState = {
devices: {},
/** @type {[integer]SceneState} */
sceneStates: {},
/** @type {User[]} */
guests: [],
/** @type {User[]} */
hosts: [],
/** @type {[integer]Device} */
hostDevices: {},
/** @type {[integer]Eoom} */
hostRooms: {},
};
function createSmartHutStore() {

View file

@ -25,6 +25,11 @@ const actions = {
type: "DEVICE_SAVE",
device,
}),
hostDeviceSave: (hostId, device) => ({
type: "HOST_DEVICE_SAVE",
hostId,
device,
}),
triggerSave: (automation) => ({
type: "TRIGGER_SAVE",
automation,
@ -58,6 +63,11 @@ const actions = {
devices,
partial,
}),
hostDevicesUpdate: (hostId, devices) => ({
type: "HOST_DEVICES_UPDATE",
hostId,
devices,
}),
stateDelete: (stateId) => ({
type: "STATE_DELETE",
stateId,
@ -71,6 +81,11 @@ const actions = {
type: "ROOMS_UPDATE",
rooms,
}),
hostRoomsUpdate: (hostId, rooms) => ({
type: "HOST_ROOMS_UPDATE",
hostId,
rooms,
}),
roomDelete: (roomId) => ({
type: "ROOM_DELETE",
roomId,
@ -91,25 +106,48 @@ const actions = {
type: "DEVICE_DELETE",
deviceId,
}),
hostsUpdate: (hosts) => ({
type: "HG_UPDATE",
key: "hosts",
value: hosts,
}),
guestsUpdate: (hosts) => ({
type: "HG_UPDATE",
key: "guests",
value: hosts,
}),
getHostDevices: (host) => ({
type: "GET_HOST_DEVICES",
host,
}),
guestUpdate: (guests) => ({
type: "HG_UPDATE",
key: "guests",
value: guests,
}),
};
export const appActions = {
// -1 for home view
setActiveRoom: (activeRoom = -1) => ({
type: "SET_ACTIVE_ROOM",
activeRoom,
type: "SET_ACTIVE",
key: "activeRoom",
value: activeRoom,
}),
setActiveTab: (activeTab) => ({
type: "SET_ACTIVE_TAB",
activeTab,
type: "SET_ACTIVE",
key: "activeTab",
value: activeTab,
}),
setActiveScene: (activeScene = -1) => ({
type: "SET_ACTIVE_SCENE",
activeScene,
type: "SET_ACTIVE",
key: "activeScene",
value: activeScene,
}),
setActiveAutomations: (activeAutomation = -1) => ({
type: "SET_ACTIVE_AUTOMATION",
activeAutomation,
setActiveHost: (activeHost = -1) => ({
type: "SET_ACTIVE",
key: "activeHost",
value: activeHost,
}),
};

View file

@ -2,8 +2,10 @@ import React, { Component } from "react";
import DevicePanel from "../components/dashboard/DevicePanel";
import ScenesPanel from "../components/dashboard/ScenesPanel";
import AutomationsPanel from "../components/dashboard/AutomationsPanel";
import HostsPanel from "../components/dashboard/HostsPanel";
import Navbar from "./Navbar";
import ScenesNavbar from "./ScenesNavbar";
import HostsNavbar from "./HostsNavbar";
import MyHeader from "../components/HeaderController";
import { Grid, Responsive, Button, Menu } from "semantic-ui-react";
import {
@ -19,7 +21,6 @@ class Dashboard extends Component {
constructor(props) {
super(props);
this.state = this.initialState;
this.setInitialState();
this.activeTab = "Devices";
this.selectTab = this.selectTab.bind(this);
}
@ -55,6 +56,8 @@ class Dashboard extends Component {
return <ScenesPanel tab={this.state.activeTab} />;
case "Automations":
return <AutomationsPanel />;
case "Hosts":
return <HostsPanel />;
default:
return <h1>ERROR</h1>;
}
@ -66,6 +69,8 @@ class Dashboard extends Component {
return <Navbar />;
case "Scenes":
return <ScenesNavbar />;
case "Hosts":
return <HostsNavbar />;
default:
return <h1>ERROR</h1>;
}
@ -87,7 +92,7 @@ class Dashboard extends Component {
</Grid.Row>
<Grid.Row color="black">
<Grid.Column textAlign="center" width={16}>
<Menu fluid widths={3} inverted color="grey">
<Menu fluid widths={4} inverted color="grey">
<Menu.Item
name="Devices"
content="Devices"
@ -106,6 +111,12 @@ class Dashboard extends Component {
active={this.activeTab === "Automations"}
onClick={this.selectTab}
/>
<Menu.Item
name="Hosts"
content="Hosts and Guests"
active={this.activeTab === "Hosts"}
onClick={this.selectTab}
/>
</Menu>
</Grid.Column>
@ -153,6 +164,16 @@ class Dashboard extends Component {
color={this.activeTab === "Automations" ? "yellow" : "grey"}
onClick={this.selectTab}
/>
<Button
basic
name="Hosts"
content="Hosts"
active={this.activeTab === "Hosts"}
color={
this.activeTab === "Hosts and Guests" ? "yellow" : "grey"
}
onClick={this.selectTab}
/>
</Grid.Column>
</Grid.Row>
{this.hasNavbar && (

View file

@ -0,0 +1,121 @@
import React, { Component } from "react";
import { Menu, Grid, Responsive, Dropdown } from "semantic-ui-react";
import HostModal from "../components/HostModal";
import { RemoteService } from "../remote";
import { connect } from "react-redux";
import { appActions } from "../storeActions";
class HostsNavbar extends Component {
constructor(props) {
super(props);
this.state = {
editMode: false,
};
this.getHosts();
this.selectHosts = this.selectHosts.bind(this);
}
getHosts() {
this.props.fetchHosts().catch(console.error);
}
selectHosts(_, { id }) {
this.props.setActiveHost(id || -1);
}
get activeItem() {
return this.props.activeHost;
}
get activeItemHostsName() {
if (this.props.activeItem === -1) return "Home";
return this.props.hosts[this.props.activeHost].name;
}
render() {
return (
<div>
<Responsive minWidth={768}>
<Grid style={{ margin: "1em -1em 0 1em" }}>
<Grid.Row>
<Menu inverted fluid vertical>
<Menu.Item
key={-1}
id={null}
name="hosts"
active={this.activeItem === -1}
onClick={this.selectHosts}
>
<strong>Hosts</strong>
</Menu.Item>
{Object.values(this.props.hosts).map((e, i) => {
return (
<Menu.Item
id={e.id}
key={i}
name={e.name}
active={this.activeItem === e.id}
onClick={this.selectHosts}
>
{e.name}
</Menu.Item>
);
})}
<Menu.Item name="newM">
<HostModal />
</Menu.Item>
</Menu>
</Grid.Row>
</Grid>
</Responsive>
<Responsive maxWidth={768}>
<Menu>
<Dropdown item fluid text={this.activeItemHostName}>
<Dropdown.Menu>
<Dropdown.Item
key={-1}
id={null}
name="scene"
active={this.activeItem === -1}
onClick={this.selectHosts}
>
<strong>Hosts</strong>
</Dropdown.Item>
{Object.values(this.props.hosts).map((e, i) => {
return (
<Menu.Item
id={e.id}
key={i}
name={e.name}
active={this.activeItem === e.id}
onClick={this.selectHosts}
>
{e.name}
</Menu.Item>
);
})}
</Dropdown.Menu>
</Dropdown>
</Menu>
</Responsive>
</div>
);
}
}
const setActiveHost = (activeHost) => {
return (dispatch) => dispatch(appActions.setActiveHost(activeHost));
};
const mapStateToProps = (state, _) => ({
hosts: state.hosts,
activeHost: state.active.activeHost,
});
const HostsNavbarContainer = connect(mapStateToProps, {
...RemoteService,
setActiveHost,
})(HostsNavbar);
export default HostsNavbarContainer;

View file

@ -31,7 +31,6 @@ class Login extends Component {
.login(this.state.user, this.state.password)
.then(() => this.props.history.push("/dashboard"))
.catch((err) => {
console.log("CIAO", err);
this.setState({
error: { state: true, message: err.messages.join(" - ") },
});

View file

@ -82,7 +82,9 @@ class Navbar extends Component {
<Grid.Column>
<Icon name="home" size="small" />
</Grid.Column>
<Grid.Column width={8}>House View</Grid.Column>
<Grid.Column width={8}>
<strong>Home view</strong>
</Grid.Column>
</Grid.Row>
</Grid>
</Menu.Item>

View file

@ -86,7 +86,7 @@ class ScenesNavbar extends Component {
active={this.activeItemScene === -1}
onClick={this.selectScene}
>
SCENES
<strong>Scenes</strong>
</Menu.Item>
{Object.values(this.props.scenes).map((e, i) => {
@ -137,7 +137,7 @@ class ScenesNavbar extends Component {
>
<Grid>
<Grid.Row>
<Grid.Column>Scene</Grid.Column>
<Grid.Column>Scenes</Grid.Column>
</Grid.Row>
</Grid>
</Dropdown.Item>