Skip to content

Commit 5bcb307

Browse files
committed
First pass at event responders for showing modals
1 parent 0bc1d1d commit 5bcb307

18 files changed

+327
-208
lines changed

public/config.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1-
/* global TILE, React */
1+
/* global MODAL, TILE, React */
22

33
window.CONFIG = {
44
url: "http://localhost:8123",
55
accessToken: "",
6+
events: [
7+
{
8+
entityId: "binary_sensor.motion_front_door_exterior",
9+
state: "on",
10+
modal: {
11+
type: MODAL.DOOR_CONTROL,
12+
camera: "camera.front_door_exterior",
13+
title: "Front Door",
14+
},
15+
},
16+
],
617
tiles: [
718
{
819
width: 2,

src/App.tsx

Lines changed: 4 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
11
import React, { Component } from "react";
22
import styled from "styled-components";
3-
import { ToastContainer, toast } from "react-toastify";
4-
import { Flex, Box } from "reflexbox/styled-components";
5-
import * as Sentry from "@sentry/browser";
6-
import { CaptureConsole as CaptureConsoleIntegration } from "@sentry/integrations";
73

8-
import "react-toastify/dist/ReactToastify.css";
9-
import "./Toast.css";
10-
11-
import { Config, TileConfig } from "./types";
12-
import HomeAssistant from "./hass";
13-
import Header from "./components/Header";
4+
import { Config } from "./types";
5+
import PanelKit from "./components/PanelKit";
146

157
const ConfigContainer = styled.div`
168
position: absolute;
@@ -53,194 +45,12 @@ const ConfigErrorContainer = styled.div`
5345
}
5446
`;
5547

56-
const Container = styled.div``;
57-
58-
interface PanelKitProps {
59-
config: Config;
60-
gridWidth: number;
61-
tileSize: number;
62-
}
63-
64-
interface PanelKitState {
65-
isReady: boolean;
66-
}
67-
68-
class TileErrorBoundary extends Component {
69-
readonly state: {
70-
error: Error | ErrorEvent | null;
71-
} = {
72-
error: null,
73-
};
74-
75-
componentDidCatch(error: ErrorEvent | Error) {
76-
this.setState({ error });
77-
}
78-
79-
render() {
80-
if (this.state.error)
81-
return (
82-
<div>
83-
<h3>Tile Error</h3>
84-
{this.state.error.message}
85-
</div>
86-
);
87-
return this.props.children;
88-
}
89-
}
90-
91-
class PanelKit extends Component<PanelKitProps, PanelKitState> {
92-
static defaultProps = {
93-
gridWidth: 8,
94-
tileSize: 167,
95-
};
96-
97-
hass: HomeAssistant;
98-
99-
readonly state = {
100-
isReady: false,
101-
};
102-
103-
constructor(props: PanelKitProps, context: any) {
104-
super(props, context);
105-
106-
const sentryDsn =
107-
process.env.REACT_APP_SENTRY_DSN || this.props.config.sentryDsn;
108-
if (sentryDsn) {
109-
console.log(`[sentry] Initialized with DSN: ${sentryDsn}`);
110-
Sentry.init({
111-
dsn: sentryDsn,
112-
integrations: [
113-
new CaptureConsoleIntegration({
114-
levels: ["warn", "error"],
115-
}),
116-
],
117-
});
118-
}
119-
120-
const url =
121-
process.env.REACT_APP_HASS_URL ||
122-
this.props.config.url ||
123-
"http://localhost:8123";
124-
125-
Sentry.setTag("hass.url", url);
126-
127-
this.hass = new HomeAssistant({
128-
// we prioritize the environment variables to ease development
129-
url,
130-
accessToken:
131-
process.env.REACT_APP_HASS_ACCESS_TOKEN ||
132-
this.props.config.accessToken ||
133-
"",
134-
onReady: this.onReady,
135-
onError: (error: Error | ErrorEvent) => {
136-
Sentry.captureException(error);
137-
toast.error(error.message);
138-
},
139-
onOpen: () => {
140-
toast.success("Connected to Home Assistant.");
141-
},
142-
});
143-
}
144-
145-
componentDidMount() {
146-
this.hass.connect();
147-
}
148-
149-
componentWillUnmount() {
150-
this.hass.disconnect();
151-
delete this.hass;
152-
}
153-
154-
onReady = () => {
155-
this.setState({ isReady: true });
156-
};
157-
158-
// TODO: should cache this somewhere
159-
getCameraList(): string[] {
160-
const results: string[] = [];
161-
function recurse(tiles: TileConfig[]) {
162-
tiles.forEach((tile) => {
163-
if (tile.tiles) recurse(tile.tiles);
164-
else if (tile.entityId && tile.entityId.indexOf("camera.") === 0)
165-
results.push(tile.entityId);
166-
});
167-
}
168-
recurse(this.props.config.tiles);
169-
return results;
170-
}
171-
172-
renderTiles(tiles: TileConfig[], colWidth = 1, depth = 0) {
173-
const hass = this.hass;
174-
const cameraList = this.getCameraList();
175-
const { tileSize } = this.props;
176-
return (
177-
<Flex flexWrap="wrap" alignContent="space-evenly" p={depth ? "10px" : 0}>
178-
{tiles.map((tile, index) => {
179-
let { width, height } = tile;
180-
if (!width) width = 1;
181-
if (!height) height = 1;
182-
183-
width = Math.min(width, colWidth);
184-
185-
if (tile.tiles) {
186-
return (
187-
<Box key={index} width={[1, 1 / 2, width / colWidth]}>
188-
{this.renderTiles(tile.tiles, width, depth + 1)}
189-
</Box>
190-
);
191-
} else {
192-
return (
193-
<Box
194-
key={index}
195-
width={[1 / 2, width / colWidth]}
196-
p="4px"
197-
style={{ minHeight: tileSize * height }}
198-
>
199-
<TileErrorBoundary>
200-
<tile.type hass={hass} cameraList={cameraList} {...tile} />
201-
</TileErrorBoundary>
202-
</Box>
203-
);
204-
}
205-
})}
206-
</Flex>
207-
);
208-
}
209-
210-
renderContent() {
211-
return (
212-
<React.Fragment>
213-
<Header />
214-
{this.renderTiles(this.props.config.tiles, this.props.gridWidth)}
215-
</React.Fragment>
216-
);
217-
}
218-
219-
render() {
220-
return (
221-
<Container>
222-
{this.state.isReady ? (
223-
this.renderContent()
224-
) : (
225-
<p>Connecting to Home Assistant...</p>
226-
)}
227-
<ToastContainer
228-
position="bottom-right"
229-
hideProgressBar
230-
newestOnTop
231-
limit={1}
232-
/>
233-
</Container>
234-
);
235-
}
236-
}
237-
238-
interface AppProps {
48+
interface Props {
23949
config: Config;
24050
configError?: ErrorEvent | null;
24151
}
24252

243-
export default class App extends Component<AppProps> {
53+
export default class App extends Component<Props> {
24454
render() {
24555
const { configError } = this.props;
24656
if (configError) {

src/components/EventManager.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React, { Component } from "react";
2+
import HomeAssistant from "../hass";
3+
import { Entity, EventConfig } from "../types";
4+
5+
interface Props {
6+
hass: HomeAssistant;
7+
events: EventConfig[];
8+
}
9+
10+
export interface State {
11+
activeEvent: number | null;
12+
}
13+
14+
export default class EventManager extends Component<Props, State> {
15+
private _activeSubscriptions: string[] = [];
16+
17+
readonly state: State = {
18+
activeEvent: null,
19+
};
20+
21+
componentDidMount() {
22+
this.props.events.forEach(({ entityId }) => {
23+
this._activeSubscriptions.push(
24+
this.props.hass.subscribe(entityId, this.onStateChange)
25+
);
26+
});
27+
}
28+
29+
componentWillUnmount() {
30+
this._activeSubscriptions.forEach((sub) => {
31+
this.props.hass.unsubscribe(sub);
32+
});
33+
}
34+
35+
onStateChange = (entityId: string, newState: Entity) => {
36+
const matchingConfig = this.props.events.find((eventConfig, idx) => {
37+
if (eventConfig.entityId !== entityId) return false;
38+
if (this.state.activeEvent && this.state.activeEvent !== idx)
39+
return false;
40+
return true;
41+
});
42+
if (!matchingConfig) return;
43+
if (this.state.activeEvent !== null) {
44+
// event is active, should it become inactive?
45+
if (newState.state !== matchingConfig.state) {
46+
this.setState({ activeEvent: null });
47+
}
48+
} else if (newState.state === matchingConfig.state) {
49+
this.setState({ activeEvent: this.props.events.indexOf(matchingConfig) });
50+
}
51+
};
52+
53+
render() {
54+
if (this.state.activeEvent === null) {
55+
return null;
56+
}
57+
const eventConfig = this.props.events[this.state.activeEvent];
58+
const ModalComponent = eventConfig.modal.type;
59+
return (
60+
<ModalComponent
61+
hass={this.props.hass}
62+
entityId={eventConfig.entityId}
63+
isOpen
64+
cameraList={[]}
65+
callService={this.props.hass.callService}
66+
{...eventConfig.modal}
67+
/>
68+
);
69+
}
70+
}

0 commit comments

Comments
 (0)