├── .eslintignore
├── .eslintrc.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE.md
├── README.md
├── generate-code-graphs.sh
├── images
├── dashboard.png
├── notes.kra
├── server-graph.svg
└── website-graph.svg
├── package-lock.json
├── package.json
├── postcss.config.js
├── server
├── accounts
│ ├── account.js
│ └── accounts-manager.js
├── automator
│ ├── automation.js
│ ├── automations-manager.js
│ └── automator.js
├── client-api.js
├── client-connection.js
├── constants.js
├── database.js
├── device-relay.js
├── devices
│ ├── device-settings.js
│ ├── device-websocket-wrapper.js
│ ├── device.js
│ ├── devices-manager.js
│ └── drivers
│ │ ├── device-driver.js
│ │ ├── generic
│ │ ├── generic-driver.js
│ │ └── service-adapters
│ │ │ ├── access-control-adapter.js
│ │ │ ├── button-adapter.js
│ │ │ ├── dimmer-adapter.js
│ │ │ ├── grow-pod-adapter.js
│ │ │ ├── light-adapter.js
│ │ │ ├── microphone-adapter.js
│ │ │ ├── motion-adapter.js
│ │ │ ├── scale-adapter.js
│ │ │ ├── service-adapter.js
│ │ │ └── thermostat-adapter.js
│ │ ├── liger-driver.js
│ │ └── standard-driver.js
├── http-server.js
├── index.js
├── locations
│ └── rooms-list.js
├── notifications.js
├── scenes
│ ├── scene.js
│ └── scenes-manager.js
├── service.routes.js
├── services
│ ├── access-control-service.js
│ ├── bill-acceptor-service.js
│ ├── button-service.js
│ ├── camera-service.js
│ ├── contact-sensor-service.js
│ ├── dimmer-service.js
│ ├── event-mock-service.js
│ ├── game-machine-service.js
│ ├── gateway-service.js
│ ├── global-alarm-service.js
│ ├── grow-pod-service.js
│ ├── light-service.js
│ ├── lock-service.js
│ ├── media-service.js
│ ├── microphone-service.js
│ ├── motion-service.js
│ ├── network-camera-service.js
│ ├── scale-service.js
│ ├── service.js
│ ├── services-manager.js
│ ├── siren-service.js
│ └── thermostat-service.js
├── stream-relay.js
├── utilities-server.js
├── utils.js
├── website.js
├── website.routes.js
└── website.setup.js
├── src
├── api.js
├── index.html
├── index.js
├── lib
│ ├── hls
│ │ └── hls.js
│ └── jsmpeg
│ │ ├── README.md
│ │ └── jsmpeg.min.js
├── state
│ ├── ducks
│ │ ├── automations-list
│ │ │ ├── actions.js
│ │ │ ├── index.js
│ │ │ ├── models
│ │ │ │ └── automation-record.js
│ │ │ ├── operations.js
│ │ │ ├── reducers.js
│ │ │ ├── selectors.js
│ │ │ └── types.js
│ │ ├── config
│ │ │ ├── actions.js
│ │ │ ├── index.js
│ │ │ ├── operations.js
│ │ │ ├── reducers.js
│ │ │ ├── selectors.js
│ │ │ └── types.js
│ │ ├── devices-list
│ │ │ ├── actions.js
│ │ │ ├── index.js
│ │ │ ├── models
│ │ │ │ └── device-record.js
│ │ │ ├── operations.js
│ │ │ ├── reducers.js
│ │ │ ├── selectors.js
│ │ │ └── types.js
│ │ ├── index.js
│ │ ├── navigation
│ │ │ ├── actions.js
│ │ │ ├── index.js
│ │ │ ├── operations.js
│ │ │ ├── reducers.js
│ │ │ ├── selectors.js
│ │ │ └── types.js
│ │ ├── rooms-list
│ │ │ ├── actions.js
│ │ │ ├── index.js
│ │ │ ├── models
│ │ │ │ └── room-record.js
│ │ │ ├── operations.js
│ │ │ ├── reducers.js
│ │ │ ├── selectors.js
│ │ │ └── types.js
│ │ ├── services-list
│ │ │ ├── actions.js
│ │ │ ├── index.js
│ │ │ ├── models
│ │ │ │ ├── camera-recording-record.js
│ │ │ │ ├── camera.js
│ │ │ │ ├── gateway.js
│ │ │ │ ├── service-record.js
│ │ │ │ └── service.js
│ │ │ ├── operations.js
│ │ │ ├── recordings.worker.js
│ │ │ ├── reducers.js
│ │ │ ├── selectors.js
│ │ │ └── types.js
│ │ └── session
│ │ │ ├── actions.js
│ │ │ ├── index.js
│ │ │ ├── operations.js
│ │ │ ├── reducers.js
│ │ │ ├── selectors.js
│ │ │ └── types.js
│ └── store.js
├── utilities.js
└── views
│ ├── AppContext.js
│ ├── components
│ ├── AccessControlCard.css
│ ├── AccessControlCard.js
│ ├── AccessControlServiceDetails.js
│ ├── Actions.css
│ ├── Actions.js
│ ├── AlarmCardGlobal.css
│ ├── AlarmCardGlobal.js
│ ├── AppToolbar.css
│ ├── AppToolbar.js
│ ├── ArmMenu.css
│ ├── ArmMenu.js
│ ├── AudioPlayer.css
│ ├── AudioPlayer.js
│ ├── AudioStream.css
│ ├── AudioStream.js
│ ├── AutomationChooseServiceActionScreen.js
│ ├── AutomationChooseServiceTriggerScreen.js
│ ├── AutomationConditionArmedScreen.js
│ ├── AutomationEditAction copy.js
│ ├── AutomationEditAction.js
│ ├── AutomationEditCondition.js
│ ├── AutomationEditScreen.css
│ ├── AutomationEditScreen.js
│ ├── AutomationEditTrigger.js
│ ├── AutomationNotificationScreen copy.js
│ ├── AutomationNotificationScreen.js
│ ├── AutomationRoute.js
│ ├── AutomationsScreen.js
│ ├── BlankState.css
│ ├── BlankState.js
│ ├── Button.css
│ ├── Button.js
│ ├── ButtonCard.css
│ ├── ButtonCard.js
│ ├── CameraCard.js
│ ├── CameraRecordingsScreen.css
│ ├── CameraRecordingsScreen.js
│ ├── ChangePasswordForm.js
│ ├── ChooseDeviceActionScreen-bak.js
│ ├── ChooseDeviceActionScreen.js
│ ├── ChooseDeviceScreen copy 2.js
│ ├── ChooseDeviceScreen copy.js
│ ├── ChooseDeviceScreen.js
│ ├── ConsoleInterface.js
│ ├── DashboardScreen.js
│ ├── DatePicker.css
│ ├── DatePicker.js
│ ├── DeviceAddScreen.js
│ ├── DeviceDetailsScreen.css
│ ├── DeviceDetailsScreen.js
│ ├── DeviceLogScreen.css
│ ├── DeviceLogScreen.js
│ ├── DeviceRoomField.js
│ ├── DeviceSettingsScreen.js
│ ├── DevicesList.js
│ ├── DevicesListScreen.js
│ ├── DimmerCard.css
│ ├── DimmerCard.js
│ ├── Form.css
│ ├── Form.js
│ ├── FormField.js
│ ├── GameMachineCard.css
│ ├── GameMachineCard.js
│ ├── GrowPodCard.css
│ ├── GrowPodCard.js
│ ├── GrowServiceDetails.js
│ ├── HlsPlayer.js
│ ├── LightCard.css
│ ├── LightCard.js
│ ├── LightServiceDetails.css
│ ├── LightServiceDetails.js
│ ├── List.css
│ ├── List.js
│ ├── ListField.js
│ ├── ListItem.css
│ ├── ListItem.js
│ ├── LockCard.js
│ ├── LoginForm.js
│ ├── Logout.js
│ ├── MediaCard.css
│ ├── MediaCard.js
│ ├── MediaServiceDetails.js
│ ├── MetaList.css
│ ├── MetaList.js
│ ├── ModalShelf.css
│ ├── ModalShelf.js
│ ├── NavigationScreen.css
│ ├── NavigationScreen.js
│ ├── PercentageField.js
│ ├── PrivateRoute.js
│ ├── PureComponent.js
│ ├── RangeControl.js
│ ├── RegisterForm.js
│ ├── RoomEditScreen.js
│ ├── RoomsScreen.js
│ ├── RoomsSettingsScreen.js
│ ├── Route.js
│ ├── ScaleCard.css
│ ├── ScaleCard.js
│ ├── SelectField.css
│ ├── SelectField.js
│ ├── ServiceCard.js
│ ├── ServiceCardBase.css
│ ├── ServiceCardBase.js
│ ├── ServiceCardGrid.js
│ ├── ServiceDetails.css
│ ├── ServiceDetails.js
│ ├── ServiceDetailsScreen.js
│ ├── ServiceHeader.css
│ ├── ServiceHeader.js
│ ├── ServiceLogScreen.css
│ ├── ServiceLogScreen.js
│ ├── ServiceSettingsScreen.css
│ ├── ServiceSettingsScreen.js
│ ├── SettingValue.js
│ ├── SettingsScreen.js
│ ├── SettingsScreenContainer.css
│ ├── SettingsScreenContainer.js
│ ├── SliderControl.js
│ ├── Switch.css
│ ├── Switch.js
│ ├── SwitchField.css
│ ├── SwitchField.js
│ ├── TabBar.css
│ ├── TabBar.js
│ ├── TextField.css
│ ├── TextField.js
│ ├── ThermostatCard.css
│ ├── ThermostatCard.js
│ ├── ThermostatServiceDetails.js
│ ├── TimeField.css
│ ├── TimeField.js
│ ├── Toolbar.css
│ ├── Toolbar.js
│ ├── VideoPlayer.css
│ ├── VideoPlayer.js
│ ├── VideoStream.css
│ └── VideoStream.js
│ ├── form-validation.js
│ ├── icons
│ ├── AddIcon.js
│ ├── AutomationsIcon.js
│ ├── CameraIcon.js
│ ├── DashboardIcon.js
│ ├── DoorIcon.js
│ ├── DownloadIcon.css
│ ├── DownloadIcon.js
│ ├── ExpandIcon.css
│ ├── ExpandIcon.js
│ ├── GameControllerIcon.js
│ ├── IconBase.css
│ ├── IconBase.js
│ ├── LeftCarrotIcon.css
│ ├── LeftCarrotIcon.js
│ ├── MenuIndicatorIcon.js
│ ├── MuteButtonIcon.css
│ ├── MuteButtonIcon.js
│ ├── PlayButtonIcon.css
│ ├── PlayButtonIcon.js
│ ├── PlayMediaButtonIcon.css
│ ├── PlayMediaButtonIcon.js
│ ├── RightCarrotIcon.css
│ ├── RightCarrotIcon.js
│ ├── ServiceIcon.js
│ ├── SettingsIcon.js
│ ├── ShieldCrossedIcon.js
│ ├── ShieldIcon.js
│ ├── StopButtonIcon.css
│ ├── StopButtonIcon.js
│ ├── UnMuteButtonIcon.css
│ ├── UnMuteButtonIcon.js
│ ├── UserIcon.js
│ └── downCarrotIcon.js
│ ├── layouts
│ ├── App.css
│ ├── App.js
│ ├── Grid.css
│ ├── Grid.js
│ ├── GridColumn.css
│ ├── GridColumn.js
│ ├── LoginScreen.css
│ └── LoginScreen.js
│ └── styles
│ ├── base.css
│ ├── colors.css
│ ├── helpers.css
│ └── reset.css
├── testEmail.js
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | server/*
2 | src/lib/**/
3 | public/*
4 | /*
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Configuration
2 | .env*
3 | !.env.example
4 | config.json
5 |
6 | # Dependencies
7 | /node_modules
8 |
9 | # Logs
10 | *.log
11 | logs
12 | npm-debug.log*
13 |
14 | # Testing
15 | /coverage
16 |
17 | # Build
18 | /public
19 |
20 | # Keys
21 | *.key
22 | *.pem
23 | *.csr
24 |
25 | # Misc
26 | .DS_Store
27 | .Spotlight-V100
28 | .Trashes
29 | Thumbs.db
30 | ehthumbs.db
31 | Desktop.ini
32 | $RECYCLE.BIN/
33 | *autosave.kra
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.watcherExclude": {
3 | "**/target": true
4 | }
5 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Objective
2 | This repository contains the front facing and backend portions of __open-automation__. Its function is to route, process, and display data from devices and users. It provides a user interface for controlling devices, setting schedules, and has customizable text/email alert system.
3 |
4 | #### Dashboard
5 | 
6 |
7 | ## Technologies
8 | ReactJS
9 | NodeJS
10 | MongoDB
11 | Websockets
12 | Z-Wave
13 | Hls
14 | FFmpeg
15 | FreeRTOS
16 |
17 | ## Supported Devices
18 |
19 | ### Gateway (https://github.com/physiii/open-automation-gateway)
20 | This is used to interface devices like cameras, thermostats, and various z-wave devices from other ecosystems into open-automation. Allows devices to operate while disconnected from the front end server.
21 |
22 | ### Cameras (https://github.com/physiii/open-automation-gateway)
23 | Handled by gateway which performs computer vision tasks, motion detection, and supports 4K live streaming. Recordings can be stored remotely and played from video player component.
24 |
25 | ### Access Control (https://github.com/physiii/access-controller)
26 | Can be interfaced with commercial strike/magnetic locks through my open-source controllers or third-party Z-wave deadbolts through the gateway software.
27 |
28 | ### Thermostat
29 | Supports wifi thermostats through the gateway with scheduling and energy efficient features.
30 |
31 | ### Lights (https://github.com/physiii/led-controller)
32 | Can use my open-source LED controller or Philips Hue lights through the gateway.
33 |
34 | ### Liger (https://github.com/physiii/liger)
35 | General purpose relay board for attaching sirens and various sensors.
36 |
37 | ### Garage Opener (https://github.com/physiii/garage-opener)
38 |
39 | ### Beacon (https://github.com/physiii/beacon)
40 | Android app for location tracking
41 |
42 | ## Graphs
43 |
44 | #### Server
45 | 
46 |
47 | #### Client
48 | 
49 |
50 |
51 | # Installation
52 | git clone https://github.com/physiii/open-automation
53 | cd open-automation
54 | cp .env.example .env
55 | nano .env
56 | echo "export NODE_ENV=development" >> ~/.bashrc
57 | source ~/.bashrc
58 | npm install
59 | #get .env key.pem cert.pem
60 | npm run build
61 | npm run start
62 |
--------------------------------------------------------------------------------
/generate-code-graphs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | madge --image website-graph.svg --exclude '.css|Icon|Button.js|utilities.js|Route.js|Switch.js|SliderControl.js' src
4 | madge --image server-graph.svg --exclude 'utils.js|constants.js' server
5 |
--------------------------------------------------------------------------------
/images/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/physiii/open-automation/a72fa76912605fb855d78e6d0de47df35d2723c1/images/dashboard.png
--------------------------------------------------------------------------------
/images/notes.kra:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/physiii/open-automation/a72fa76912605fb855d78e6d0de47df35d2723c1/images/notes.kra
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | const autoprefixer = require('autoprefixer'),
2 | icssValues = require('postcss-icss-values');
3 |
4 | module.exports = {
5 | plugins: [
6 | autoprefixer,
7 | icssValues
8 | ]
9 | };
10 |
--------------------------------------------------------------------------------
/server/automator/automation.js:
--------------------------------------------------------------------------------
1 | const uuid = require('uuid').v4,
2 | moment = require('moment'),
3 | TAG = '[Automation]';
4 |
5 | class Automation {
6 | constructor (data) {
7 | this.id = data.id || uuid();
8 | this.is_enabled = data.is_enabled || false;
9 | this.type = data.type;
10 | this.source = data.source || {unknown: true};
11 | this.user_editable = data.user_editable || false;
12 | this.name = data.name || '';
13 | this.account_id = data.account_id;
14 | this.triggers = data.triggers || [];
15 | this.actions = data.actions || [];
16 | this.conditions = data.conditions || [];
17 | this.scenes = data.scenes || [];
18 | this.notifications = data.notifications || [];
19 | }
20 |
21 | getNextTimeOfDayDate (trigger) {
22 | if (!trigger || trigger.type !== 'time-of-day' || !Number.isInteger(trigger.time)) {
23 | throw new Error('A valid time-of-day trigger must be provided.');
24 | }
25 |
26 | const date = moment().utc(),
27 | getNextDate = (_date) => _date.startOf('day').add(trigger.time, 'minutes').startOf('minute');
28 |
29 | let next_date = getNextDate(date);
30 |
31 | if (next_date.isSameOrBefore(new Date())) {
32 | next_date = getNextDate(date.add(1, 'day'));
33 | }
34 |
35 | return next_date;
36 | }
37 |
38 | serialize () {
39 | return {
40 | id: this.id,
41 | is_enabled: this.is_enabled,
42 | type: this.type,
43 | source: this.source,
44 | user_editable: this.user_editable,
45 | name: this.name,
46 | account_id: this.account_id,
47 | triggers: this.triggers,
48 | actions: this.actions,
49 | conditions: this.conditions,
50 | scenes: this.scenes,
51 | notifications: this.notifications
52 | };
53 | }
54 |
55 | dbSerialize () {
56 | return {
57 | ...this.serialize()
58 | };
59 | }
60 |
61 | clientSerialize () {
62 | return {
63 | ...this.serialize()
64 | };
65 | }
66 | }
67 |
68 | module.exports = Automation;
69 |
--------------------------------------------------------------------------------
/server/client-api.js:
--------------------------------------------------------------------------------
1 | const cookie = require('cookie'),
2 | ClientConnection = require('./client-connection.js'),
3 | SOCKET_IO_NAMESPACE = '/client-api',
4 | TAG = '[client-api.js]';
5 |
6 | module.exports = function (socket_io_server, jwt_secret) {
7 | socket_io_server.of(SOCKET_IO_NAMESPACE).on('connection', (socket) => {
8 | const cookies = socket.handshake.headers.cookie ? cookie.parse(socket.handshake.headers.cookie) : {};
9 |
10 | new ClientConnection(socket, cookies.access_token, socket.handshake.headers['x-xsrf-token'], jwt_secret);
11 | });
12 |
13 | console.log(TAG, 'Listening for Socket.IO Client API connections on namespace ' + SOCKET_IO_NAMESPACE + '.');
14 | }
15 |
--------------------------------------------------------------------------------
/server/constants.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | SERVICE_EVENT_DELIMITER: '::'
3 | };
4 |
--------------------------------------------------------------------------------
/server/device-relay.js:
--------------------------------------------------------------------------------
1 | const url = require('url'),
2 | WebSocket = require('ws'),
3 | DevicesManager = require('./devices/devices-manager.js'),
4 | DeviceWebSocketWrapper = require('./devices/device-websocket-wrapper.js'),
5 | WEBSOCKET_DEVICE_PATH = '/device-relay',
6 | SOCKET_IO_NAMESPACE = '/device-relay',
7 | TAG = '[device-relay.js]';
8 |
9 | function handleDeviceConnection (socket, headers) {
10 | const device_id = headers['x-device-id'],
11 | device_token = headers['x-device-token'],
12 | device_type = headers['x-device-type'];
13 |
14 | socket.on('error', (error) => console.error(TAG, device_id, 'Connection error:', error));
15 |
16 | if (!device_id) {
17 | // Connection is not a device.
18 | return;
19 | }
20 |
21 | DevicesManager.handleDeviceConnection(device_id, device_token, device_type, socket);
22 | }
23 |
24 | module.exports = (http_server, socket_io_server) => {
25 | const websocket_server = new WebSocket.Server({noServer: true});
26 |
27 | // Handle websocket server errors.
28 | websocket_server.on('error', (error) => console.error(TAG, 'Server error:', error));
29 |
30 | // Listen for devices connecting over WebSockets.
31 | http_server.on('upgrade', (request, ws, head) => {
32 | // Only listen to device connections.
33 | if (url.parse(request.url).pathname !== WEBSOCKET_DEVICE_PATH) {
34 | return;
35 | }
36 |
37 | websocket_server.handleUpgrade(request, ws, head, (socket) => handleDeviceConnection(new DeviceWebSocketWrapper(socket), request.headers));
38 | });
39 |
40 | console.log(TAG, 'Listening for WebSocket device connections at ' + (process.env.OA_SSL ? 'wss': 'ws') + '://localhost:' + http_server.address().port + WEBSOCKET_DEVICE_PATH + '.');
41 |
42 | // Listen for devices connecting over Socket.IO.
43 | socket_io_server.of(SOCKET_IO_NAMESPACE).on('connection', (socket) => handleDeviceConnection(socket, socket.handshake.headers));
44 |
45 | console.log(TAG, 'Listening for Socket.IO device connections on namespace ' + SOCKET_IO_NAMESPACE + '.');
46 | };
47 |
--------------------------------------------------------------------------------
/server/devices/drivers/device-driver.js:
--------------------------------------------------------------------------------
1 | const TAG = '[DeviceDriver]';
2 |
3 | class DeviceDriver {
4 | constructor (data, device_id) {
5 | this.device_id = device_id;
6 | }
7 |
8 | init () {
9 | // no-op
10 | }
11 |
12 | on () {
13 | // no-op
14 | }
15 |
16 | emit () {
17 | // no-op
18 | }
19 |
20 | setSocket (socket) {
21 | if (socket === this.socket) {
22 | return;
23 | }
24 |
25 | // Disconnect the current socket.
26 | if (this.socket) {
27 | console.log(TAG, 'Closing device socket to set new socket.', this.device_id);
28 | this.socket.removeAllListeners();
29 | this.socket.disconnect();
30 | }
31 |
32 | this.socket = socket;
33 |
34 | this._subscribeToSocket();
35 | }
36 |
37 | _subscribeToSocket () {
38 | this.socket.on('error', (error) => console.log(TAG, 'Socket error:', error, this.device_id));
39 | this.socket.on('disconnect', (reason) => console.log(TAG, 'Socket disconnected. Reason:', reason, this.device_id));
40 | }
41 |
42 | destroy () {
43 | if (this.socket && this.socket.connected) {
44 | this.socket.disconnect(true);
45 | }
46 | }
47 | }
48 |
49 | module.exports = DeviceDriver;
50 |
--------------------------------------------------------------------------------
/server/devices/drivers/generic/service-adapters/button-adapter.js:
--------------------------------------------------------------------------------
1 | const GenericServiceAdapter = require('./service-adapter.js'),
2 | TAG = '[GenericButtonAdapter]',
3 | SENSITIVITY_SCALE = 255;
4 |
5 | class GenericButtonAdapter extends GenericServiceAdapter {
6 | _adaptSocketEmit (event, data, callback = () => { /* no-op */ }) {
7 | let adapted_data = {...data},
8 | adapted_event = event,
9 | adapted_callback = callback,
10 | should_emit = true;
11 |
12 | switch (event) {
13 | case 'settings':
14 | if ('sensitivity' in data.settings) {
15 | adapted_data.settings.sensitivity = this._adaptPercentageToDevice(data.settings.sensitivity, SENSITIVITY_SCALE);
16 | }
17 | break;
18 | }
19 |
20 | return GenericServiceAdapter.prototype._adaptSocketEmit.call(this, adapted_event, adapted_data, adapted_callback, should_emit);
21 | }
22 | };
23 |
24 | GenericButtonAdapter.generic_type = 'button';
25 | GenericButtonAdapter.relay_type = 'button';
26 | GenericButtonAdapter.settings_definitions = new Map([...GenericServiceAdapter.settings_definitions])
27 | .set('sensitivity', {
28 | type: 'percentage',
29 | label: 'Sensitivity',
30 | default_value: 0.5,
31 | validation: {is_required: true}
32 | });
33 |
34 | module.exports = GenericButtonAdapter;
35 |
--------------------------------------------------------------------------------
/server/devices/drivers/generic/service-adapters/microphone-adapter.js:
--------------------------------------------------------------------------------
1 | const GenericServiceAdapter = require('./service-adapter.js'),
2 | TAG = '[GenericMicrophoneAdapter]',
3 | SENSITIVITY_SCALE = 255;
4 |
5 | class GenericMicrophoneAdapter extends GenericServiceAdapter {
6 | _adaptSocketEmit (event, data, callback = () => { /* no-op */ }) {
7 | let adapted_data = {...data},
8 | adapted_event = event,
9 | adapted_callback = callback,
10 | should_emit = true;
11 |
12 | switch (event) {
13 | case 'settings':
14 | if ('sensitivity' in data.settings) {
15 | adapted_data.settings.sensitivity = this._adaptPercentageToDevice(data.settings.sensitivity, SENSITIVITY_SCALE);
16 | }
17 | break;
18 | }
19 |
20 | return GenericServiceAdapter.prototype._adaptSocketEmit.call(this, adapted_event, adapted_data, adapted_callback, should_emit);
21 | }
22 | };
23 |
24 | GenericMicrophoneAdapter.generic_type = 'microphone';
25 | GenericMicrophoneAdapter.relay_type = 'microphone';
26 | GenericMicrophoneAdapter.settings_definitions = new Map([...GenericServiceAdapter.settings_definitions])
27 | .set('sensitivity', {
28 | type: 'percentage',
29 | label: 'Sensitivity',
30 | default_value: 1,
31 | validation: {is_required: true}
32 | });
33 |
34 | module.exports = GenericMicrophoneAdapter;
35 |
--------------------------------------------------------------------------------
/server/devices/drivers/generic/service-adapters/motion-adapter.js:
--------------------------------------------------------------------------------
1 | const GenericServiceAdapter = require('./service-adapter.js'),
2 | TAG = '[GenericMotionAdapter]',
3 | SENSITIVITY_SCALE = 255;
4 |
5 | class GenericMotionAdapter extends GenericServiceAdapter {
6 | _adaptSocketEmit (event, data, callback = () => { /* no-op */ }) {
7 | let adapted_data = {...data},
8 | adapted_event = event,
9 | adapted_callback = callback,
10 | should_emit = true;
11 |
12 | switch (event) {
13 | case 'settings':
14 | if ('sensitivity' in data.settings) {
15 | adapted_data.settings.sensitivity = this._adaptPercentageToDevice(data.settings.sensitivity, SENSITIVITY_SCALE);
16 | }
17 | break;
18 | }
19 |
20 | return GenericServiceAdapter.prototype._adaptSocketEmit.call(this, adapted_event, adapted_data, adapted_callback, should_emit);
21 | }
22 | };
23 |
24 | GenericMotionAdapter.generic_type = 'motion';
25 | GenericMotionAdapter.relay_type = 'motion';
26 | GenericMotionAdapter.settings_definitions = new Map([...GenericServiceAdapter.settings_definitions])
27 | .set('sensitivity', {
28 | type: 'percentage',
29 | label: 'Sensitivity',
30 | default_value: 0.5,
31 | validation: {is_required: true}
32 | });
33 |
34 | module.exports = GenericMotionAdapter;
35 |
--------------------------------------------------------------------------------
/server/devices/drivers/standard-driver.js:
--------------------------------------------------------------------------------
1 | const DeviceDriver = require('./device-driver.js'),
2 | constants = require('../../constants.js'),
3 | noOp = () => {},
4 | TAG = '[StandardDeviceDriver]';
5 |
6 | class StandardDeviceDriver extends DeviceDriver {
7 | constructor (data, socket, device_id) {
8 | super(socket, device_id);
9 |
10 | this._socket_listeners = [];
11 |
12 | if (socket) {
13 | this.setSocket(socket);
14 | }
15 | }
16 |
17 | on (event, callback, service_id, service_type) {
18 | const prefixed_event = this._getPrefixedEvent(event, service_id, service_type);
19 |
20 | this._socket_listeners.push([prefixed_event, callback]);
21 |
22 | if (this.socket) {
23 | this.socket.on(prefixed_event, callback);
24 | }
25 | }
26 |
27 | emit (event, data, callback = noOp, service_id, service_type) {
28 | const prefixed_event = this._getPrefixedEvent(event, service_id, service_type);
29 |
30 | if (!this.socket) {
31 | console.log(TAG, this.device_id, 'Tried to emit socket event "' + prefixed_event + '" but the device does not have a socket.');
32 | callback('Device not connected');
33 | return;
34 | }
35 |
36 | if (!this.socket.connected) {
37 | console.log(TAG, this.device_id, 'Tried to emit socket event "' + prefixed_event + '" but the socket is not connected.');
38 | callback('Device not connected');
39 | return;
40 | }
41 |
42 | this.socket.emit(prefixed_event, data, callback);
43 | }
44 |
45 | _getPrefixedEvent (event, service_id, service_type) {
46 | return service_id
47 | ? service_id + constants.SERVICE_EVENT_DELIMITER + service_type + constants.SERVICE_EVENT_DELIMITER + event
48 | : event;
49 | }
50 |
51 | _subscribeToSocket () {
52 | // Set up listeners on new socket.
53 | this._socket_listeners.forEach((listener) => {
54 | this.socket.on.apply(this.socket, listener);
55 | });
56 |
57 | DeviceDriver.prototype._subscribeToSocket.call(this, arguments);
58 | }
59 | }
60 |
61 | module.exports = StandardDeviceDriver;
62 |
--------------------------------------------------------------------------------
/server/http-server.js:
--------------------------------------------------------------------------------
1 | const https = require('https'),
2 | http = require('http'),
3 | TAG = '[http-server.js]';
4 |
5 | module.exports = (website, key, cert) => {
6 | let server;
7 |
8 | if (process.env.OA_SSL) {
9 | server = https.createServer({key, cert}, website);
10 | } else {
11 | server = http.createServer(website);
12 | }
13 |
14 | server.listen(
15 | process.env.OA_WEBSITE_PORT,
16 | null,
17 | () => console.log(TAG, (process.env.OA_SSL ? 'Secure' : 'Insecure') + ' server listening on port ' + server.address().port + '.')
18 | );
19 |
20 | return server;
21 | };
22 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const dotenv = require('dotenv'),
2 | dotenvExpand = require('dotenv-expand'),
3 | fs = require('fs'),
4 | path = require('path'),
5 | { Server: SocketServer } = require('socket.io'),
6 | uuidV4 = require('uuid').v4,
7 | setUpWebsite = require('./website.js'),
8 | startHttpServer = require('./http-server.js'),
9 | startClientApi = require('./client-api.js'),
10 | startDeviceRelay = require('./device-relay.js'),
11 | startStreamRelay = require('./stream-relay.js'),
12 | startUtilitiesServer = require('./utilities-server.js'),
13 | AccountsManager = require('./accounts/accounts-manager.js'),
14 | DevicesManager = require('./devices/devices-manager.js'),
15 | ScenesManager = require('./scenes/scenes-manager.js'),
16 | Notifications = require('./notifications.js'),
17 | AutomationsManager = require('./automator/automations-manager.js'),
18 | Automator = require('./automator/automator.js');
19 |
20 | let key,
21 | cert;
22 |
23 | dotenvExpand(dotenv.config());
24 |
25 | if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'development') {
26 | throw new Error('The NODE_ENV environment variable must be either "production" or "development".');
27 | }
28 |
29 | // TODO: Validate configuration.
30 |
31 | if (process.env.OA_SSL) {
32 | const key_path = process.env.OA_SSL_KEY_PATH || 'key.pem',
33 | cert_path = process.env.OA_SSL_CERT_PATH || 'cert.pem';
34 |
35 | try {
36 | key = fs.readFileSync(path.resolve(__dirname, '..', key_path));
37 | cert = fs.readFileSync(path.resolve(__dirname, '..', cert_path));
38 | } catch (error) {
39 | throw new Error('The SSL key or certificate could not be loaded. Check the SSL configuration. (' + error + ')');
40 | }
41 | }
42 |
43 | AccountsManager.init()
44 | .then(DevicesManager.init)
45 | .then(ScenesManager.init)
46 | .then(Notifications.init)
47 | .then(() => Promise.resolve(new Automator()))
48 | .then(AutomationsManager.init)
49 | .then(() => {
50 | const jwt_secret = process.env.OA_JWT_SECRET || key || uuidV4(),
51 | website = setUpWebsite(jwt_secret),
52 | http_server = startHttpServer(website, key, cert),
53 | socket_io_server = new SocketServer(http_server, {
54 | pingTimeout: 60000,
55 | pingInterval: 60000,
56 | });
57 |
58 | startClientApi(socket_io_server, jwt_secret);
59 | startDeviceRelay(http_server, socket_io_server);
60 | startStreamRelay(http_server, key, cert);
61 | startUtilitiesServer(http_server);
62 | })
63 | .catch((error) => {
64 | console.error('An error was encountered while starting.', error);
65 | process.exit(1);
66 | });
67 |
--------------------------------------------------------------------------------
/server/scenes/scene.js:
--------------------------------------------------------------------------------
1 | const uuid = require('uuid').v4,
2 | TAG = '[Scene]';
3 |
4 | class Scene {
5 | constructor (data) {
6 | this.id = data.id || uuid();
7 | this.account_id = data.account_id;
8 | this.type = data.type || '';
9 | this.actions = data.actions || [];
10 | }
11 |
12 | serialize () {
13 | return {
14 | id: this.id,
15 | account_id: this.account_id,
16 | type: this.type,
17 | actions: this.actions
18 | };
19 | }
20 |
21 | dbSerialize () {
22 | return {
23 | ...this.serialize()
24 | };
25 | }
26 | }
27 |
28 | module.exports = Scene;
29 |
--------------------------------------------------------------------------------
/server/scenes/scenes-manager.js:
--------------------------------------------------------------------------------
1 | const database = require('../database.js'),
2 | Scene = require('./scene.js'),
3 | DevicesManager = require('../devices/devices-manager.js'),
4 | scenes_list = new Map(),
5 | TAG = '[ScenesManager]';
6 |
7 | class ScenesManager {
8 | constructor () {
9 | this.init = this.init.bind(this);
10 | }
11 |
12 | init () {
13 | return this.loadScenesFromDb();
14 | }
15 |
16 | setScene (scene_id, account_id, fallback_values = {}) {
17 | const scene = this.getSceneById(scene_id, account_id);
18 |
19 | if (!scene) {
20 | console.error(TAG, 'Tried to set scene ' + scene_id + ', but scene was not found or it does not belong to the account.');
21 | return;
22 | }
23 |
24 | scene.actions.forEach((action) => {
25 | const service = DevicesManager.getServiceById(action.service_id, scene.account_id);
26 |
27 | if (!service) {
28 | console.error(TAG, 'Tried to set an action for scene ' + scene_id + ', but the service was not found or it does not belong to the account.');
29 | return;
30 | }
31 |
32 | const action_fallback_values = fallback_values[service.constructor.type] || {};
33 |
34 | let value = action.value;
35 |
36 | // If there's no value for the action, use the fallback value.
37 | if (!action.hasOwnProperty('value')) {
38 | value = action_fallback_values[action.property];
39 | }
40 |
41 | service.action({...action, value});
42 | });
43 | }
44 |
45 | addScene (data) {
46 | let scene = this.getSceneById(data.id, null, true);
47 |
48 | if (scene) {
49 | return scene;
50 | }
51 |
52 | scene = new Scene(data);
53 | scenes_list.set(scene.id, scene);
54 | return scene;
55 | }
56 |
57 | createScene (data) {
58 | return new Promise((resolve, reject) => {
59 | const scene = this.addScene(data);
60 |
61 | database.saveScene(scene.serialize()).then(() => {
62 | resolve(scene);
63 | }).catch(reject);
64 | });
65 | }
66 |
67 | // NOTE: Use skip_account_access_check with caution. Never use for requests
68 | // originating from the client API.
69 | getSceneById (scene_id, account_id, skip_account_access_check) {
70 | const scene = scenes_list.get(scene_id);
71 |
72 | // Verify account has the access to the scene.
73 | if ((scene && (scene.account_id === account_id)) || skip_account_access_check) {
74 | return scene;
75 | }
76 | }
77 |
78 | loadScenesFromDb () {
79 | return new Promise((resolve, reject) => {
80 | database.getScenes().then((scenes) => {
81 | scenes_list.clear();
82 |
83 | scenes.forEach((scene) => {
84 | this.addScene(scene)
85 | });
86 |
87 | resolve(scenes_list);
88 | }).catch(reject);
89 | });
90 | }
91 | }
92 |
93 | module.exports = new ScenesManager();
94 |
--------------------------------------------------------------------------------
/server/services/bill-acceptor-service.js:
--------------------------------------------------------------------------------
1 | const Service = require('./service.js');
2 |
3 | class BillAcceptorService extends Service {
4 | subscribeToDevice () {
5 | Service.prototype.subscribeToDevice.apply(this, arguments);
6 |
7 | this.deviceOn('accepted', ({denomination}) => this._emit('accepted', {denomination}));
8 | }
9 | }
10 |
11 | BillAcceptorService.type = 'bill-acceptor';
12 | BillAcceptorService.friendly_type = 'Bill Acceptor';
13 | BillAcceptorService.indefinite_article = 'A';
14 |
15 | module.exports = BillAcceptorService;
16 |
--------------------------------------------------------------------------------
/server/services/button-service.js:
--------------------------------------------------------------------------------
1 | const Service = require('./service.js');
2 |
3 | class ButtonService extends Service {
4 | subscribeToDevice () {
5 | Service.prototype.subscribeToDevice.apply(this, arguments);
6 |
7 | this.deviceOn('pressed', (event_data) => this._emit('pressed', event_data));
8 | this.deviceOn('state', (event_data) => {
9 | // TODO: validate this value before passing it through
10 | this._emit(event_data.state.value, event_data);
11 | });
12 |
13 | }
14 | }
15 |
16 | ButtonService.type = 'button';
17 | ButtonService.friendly_type = 'Button';
18 | ButtonService.indefinite_article = 'A';
19 |
20 | module.exports = ButtonService;
21 |
--------------------------------------------------------------------------------
/server/services/contact-sensor-service.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment'),
2 | Service = require('./service.js');
3 |
4 | class ContactSensorService extends Service {
5 | subscribeToDevice () {
6 | Service.prototype.subscribeToDevice.apply(this, arguments);
7 |
8 | this.deviceOn('open', () => this._emit('open'));
9 | this.deviceOn('closed', () => this._emit('closed'));
10 | }
11 | }
12 |
13 | ContactSensorService.type = 'contact-sensor';
14 | ContactSensorService.friendly_type = 'Contact Sensor';
15 | ContactSensorService.indefinite_article = 'A';
16 |
17 | ContactSensorService.event_definitions = new Map([...Service.event_definitions])
18 | .set('open', {
19 | label: 'Machine Opened',
20 | generateNotification: function (event_data) {
21 | return 'Movement was detected on ' + this.getNameOrType(true, false, true) + ' opened at ' + moment(event_data.date).format('h:mm a on dddd, MMMM Do.');
22 | }
23 | })
24 | .set('closed', {
25 | label: 'Machine Closed',
26 | generateNotification: function (event_data) {
27 | return 'Movement on ' + this.getNameOrType(true, false, true) + ' closed at ' + moment(event_data.date).format('h:mm a on dddd, MMMM Do.');
28 | }
29 | });
30 |
31 | ContactSensorService.event_strings = {
32 | 'open': {
33 | getFriendlyName: () => 'Opened',
34 | getDescription: function (event_data) {
35 | return 'Movement was detected on ' + this.getNameOrType(true, false, true) + ' at ' + moment(event_data.date).format('h:mm a on dddd, MMMM Do.');
36 | }
37 | },
38 | 'closed': {
39 | getFriendlyName: () => 'Closed',
40 | getDescription: function (event_data) {
41 | return 'Movement on ' + this.getNameOrType(true, false, true) + ' stopped at ' + moment(event_data.date).format('h:mm a on dddd, MMMM Do.');
42 | }
43 | }
44 | };
45 |
46 | module.exports = ContactSensorService;
47 |
--------------------------------------------------------------------------------
/server/services/dimmer-service.js:
--------------------------------------------------------------------------------
1 | const Service = require('./service.js');
2 |
3 | class DimmerService extends Service {}
4 |
5 | DimmerService.type = 'dimmer';
6 | DimmerService.friendly_type = 'Dimmer';
7 | DimmerService.indefinite_article = 'A';
8 |
9 | module.exports = DimmerService;
10 |
--------------------------------------------------------------------------------
/server/services/event-mock-service.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment'),
2 | Service = require('./service.js'),
3 | EVENT_INTERVAL_DELAY = 5000;
4 |
5 | class EventMockService extends Service {
6 | constructor (data, onUpdate, deviceOn, deviceEmit, save) {
7 | super(data, onUpdate, deviceOn, deviceEmit, save);
8 |
9 | this.setState({connected: true});
10 |
11 | setInterval(() => {
12 | this._emit('mock', {date: new Date()});
13 | }, EVENT_INTERVAL_DELAY);
14 | }
15 | }
16 |
17 | EventMockService.type = 'event-mock';
18 | EventMockService.friendly_type = 'Event Mock';
19 | EventMockService.indefinite_article = 'An';
20 | EventMockService.event_strings = {
21 | 'mock': {
22 | getFriendlyName: () => 'Event Mocked',
23 | getDescription: (event_data) => 'Event mock mocked at ' + moment(event_data.date).format('h:mm a on dddd, MMMM Do.')
24 | }
25 | };
26 |
27 | module.exports = EventMockService;
28 |
--------------------------------------------------------------------------------
/server/services/game-machine-service.js:
--------------------------------------------------------------------------------
1 | const Service = require('./service.js');
2 |
3 | class GameMachineService extends Service {
4 | addCredit (dollar_value) {
5 | return new Promise((resolve, reject) => {
6 | this.deviceEmit('credit/add', {dollar_value}, (error, data) => {
7 | if (error) {
8 | reject(error);
9 | return;
10 | }
11 |
12 | resolve();
13 | });
14 | });
15 | }
16 | }
17 |
18 | GameMachineService.type = 'game-machine';
19 | GameMachineService.friendly_type = 'Game Machine';
20 | GameMachineService.indefinite_article = 'A';
21 |
22 | module.exports = GameMachineService;
23 |
--------------------------------------------------------------------------------
/server/services/gateway-service.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto'),
2 | Service = require('./service.js'),
3 | COMMAND_TOKEN_SIZE = 8,
4 | TAG = '[GatewayService]';
5 |
6 | class GatewayService extends Service {
7 | getDevices () {
8 | return new Promise((resolve, reject) => {
9 | this.deviceEmit('devices/get', {}, (error, data) => {
10 | if (error) {
11 | reject(error);
12 | return;
13 | }
14 |
15 | resolve(data);
16 | });
17 | });
18 | }
19 |
20 | getCommandToken () {
21 | return new Promise((resolve, reject) => {
22 | crypto.randomBytes(COMMAND_TOKEN_SIZE, (error, token_buffer) => {
23 | if (error) {
24 | reject(error);
25 | return;
26 | }
27 |
28 | const token = token_buffer.toString('hex');
29 |
30 | this.command_token = token;
31 |
32 | console.log(TAG, this.id, 'command token', token);
33 |
34 | // NOTE: DO NOT SEND THE TOKEN TO CLIENT.
35 | resolve();
36 | });
37 | });
38 | }
39 |
40 | verifyCommandToken (token) {
41 | return token && token === this.command_token;
42 | }
43 |
44 | command (command) {
45 | return new Promise((resolve, reject) => {
46 | this.deviceEmit('command', {command}, (error, data) => {
47 | if (error) {
48 | reject(error);
49 | return;
50 | }
51 |
52 | resolve(data);
53 | });
54 | });
55 | }
56 | }
57 |
58 | GatewayService.type = 'gateway';
59 | GatewayService.friendly_type = 'Gateway';
60 | GatewayService.indefinite_article = 'A';
61 |
62 | module.exports = GatewayService;
63 |
--------------------------------------------------------------------------------
/server/services/global-alarm-service.js:
--------------------------------------------------------------------------------
1 | const Service = require('./service.js'),
2 | TAG = '[GlobalAlarmService]';
3 |
4 | class GlobalAlarmService extends Service {
5 | subscribeToDevice () {
6 | Service.prototype.subscribeToDevice.apply(this, arguments);
7 |
8 | this.events.on('mode', (event_data) => {
9 | let trigger_data = { state: { value: null } };
10 |
11 | console.log(TAG, 'mode', event_data);
12 |
13 | if (event_data.value > 0) {
14 | trigger_data.state.value = 'arm';
15 | } else {
16 | trigger_data.state.value = 'disarm';
17 | }
18 |
19 | // TODO: validate this value before passing it through
20 | this._emit(trigger_data.state.value, trigger_data);
21 |
22 | console.log(TAG, event_data, trigger_data);
23 | });
24 | }
25 | }
26 |
27 | GlobalAlarmService.type = 'global-alarm';
28 | GlobalAlarmService.friendly_type = 'Global Alarm';
29 | GlobalAlarmService.indefinite_article = 'A';
30 |
31 | module.exports = GlobalAlarmService;
32 |
--------------------------------------------------------------------------------
/server/services/light-service.js:
--------------------------------------------------------------------------------
1 | const Service = require('./service.js');
2 |
3 | class LightService extends Service {
4 | setPower (power) {
5 | return new Promise((resolve, reject) => {
6 | this.deviceEmit(power ? 'lightOn/set' : 'lightOff/set', {}, (error, data) => {
7 | if (error) {
8 | reject(error);
9 | return;
10 | }
11 |
12 | resolve();
13 | });
14 | });
15 | }
16 |
17 | setTheme (theme) {
18 | return new Promise((resolve, reject) => {
19 | this.deviceEmit('theme/set', {temp}, (error, data) => {
20 | if (error) {
21 | reject(error);
22 | return;
23 | }
24 |
25 | resolve();
26 | });
27 | });
28 | }
29 |
30 | setBrightness (brightness) {
31 | return new Promise((resolve, reject) => {
32 | if (brightness > 0) {
33 | if (this.state.power === false) {
34 | this.setPower(true);
35 | }
36 | } else if (this.state.power === true) {
37 | this.setPower(false);
38 | }
39 |
40 | this.deviceEmit('brightness/set', {brightness}, (error, data) => {
41 | if (error) {
42 | reject(error);
43 | return;
44 | }
45 |
46 | resolve();
47 | });
48 | });
49 | }
50 |
51 | setColor (color) {
52 | return new Promise((resolve, reject) => {
53 | this.deviceEmit('color/set', {color}, (error, data) => {
54 | if (error) {
55 | reject(error);
56 | return;
57 | }
58 |
59 | resolve();
60 | });
61 | });
62 | }
63 | }
64 |
65 | LightService.type = 'light';
66 | LightService.friendly_type = 'Light';
67 | LightService.indefinite_article = 'A';
68 | LightService.action_definitions = new Map([...Service.action_definitions])
69 | .set('turn-on-lights', {
70 | label: 'Turn on lights'
71 | })
72 | .set('turn-off-lights', {
73 | label: 'Turn off lights'
74 | })
75 | .set('alert', {
76 | label: 'Alert'
77 | });
78 | LightService.state_definitions = new Map()
79 | .set('power', {
80 | type: 'boolean',
81 | setter: 'setPower'
82 | })
83 | .set('brightness', {
84 | type: 'percentage',
85 | setter: 'setBrightness'
86 | })
87 | .set('color', {
88 | type: 'color',
89 | setter: 'setColor'
90 | });
91 |
92 | module.exports = LightService;
93 |
--------------------------------------------------------------------------------
/server/services/lock-service.js:
--------------------------------------------------------------------------------
1 | const Service = require('./service.js');
2 |
3 | class LockService extends Service {
4 | constructor (data, onUpdate, deviceOn, deviceEmit, save) {
5 | super(data, onUpdate, deviceOn, deviceEmit, save);
6 |
7 | this.lock = this.lock.bind(this);
8 | this.unlock = this.unlock.bind(this);
9 | }
10 |
11 | lock () {
12 | return new Promise((resolve, reject) => {
13 | this.deviceEmit('lock/set', {}, (error, data) => {
14 | if (error) {
15 | reject(error);
16 | return;
17 | }
18 |
19 | resolve();
20 | });
21 | });
22 | }
23 |
24 | unlock () {
25 | return new Promise((resolve, reject) => {
26 | this.deviceEmit('unlock/set', {}, (error, data) => {
27 | if (error) {
28 | reject(error);
29 | return;
30 | }
31 |
32 | resolve();
33 | });
34 | });
35 | }
36 | }
37 |
38 | LockService.type = 'lock';
39 | LockService.friendly_type = 'Lock';
40 | LockService.indefinite_article = 'A';
41 |
42 | module.exports = LockService;
43 |
--------------------------------------------------------------------------------
/server/services/microphone-service.js:
--------------------------------------------------------------------------------
1 | const Service = require('./service.js');
2 |
3 | class MicrophoneService extends Service {
4 | subscribeToDevice () {
5 | Service.prototype.subscribeToDevice.apply(this, arguments);
6 |
7 | this.deviceOn('state', (event_data) => {
8 | // TODO: validate this value before passing it through
9 | this._emit(event_data.state.value, event_data);
10 | });
11 | }
12 | }
13 |
14 | MicrophoneService.type = 'microphone';
15 | MicrophoneService.friendly_type = 'Microphone';
16 | MicrophoneService.indefinite_article = 'A';
17 |
18 | module.exports = MicrophoneService;
19 |
--------------------------------------------------------------------------------
/server/services/motion-service.js:
--------------------------------------------------------------------------------
1 | const Service = require('./service.js');
2 |
3 | class MotionService extends Service {}
4 |
5 | MotionService.type = 'motion';
6 | MotionService.friendly_type = 'Motion Sensor';
7 | MotionService.indefinite_article = 'A';
8 |
9 | module.exports = MotionService;
10 |
--------------------------------------------------------------------------------
/server/services/siren-service.js:
--------------------------------------------------------------------------------
1 | const Service = require('./service.js');
2 |
3 | class SirenService extends Service {
4 | setSiren (trigger) {
5 | return new Promise((resolve, reject) => {
6 | if (trigger) {
7 | this.deviceEmit('siren', {chirp: 2});
8 | }
9 |
10 | resolve();
11 | });
12 | }
13 | }
14 |
15 | SirenService.type = 'siren';
16 | SirenService.friendly_type = 'Siren';
17 | SirenService.indefinite_article = 'A';
18 | SirenService.state_definitions = new Map()
19 | .set('trigger', {
20 | type: 'boolean',
21 | setter: 'setSiren'
22 | });
23 |
24 | module.exports = SirenService;
25 |
--------------------------------------------------------------------------------
/server/utilities-server.js:
--------------------------------------------------------------------------------
1 | const url = require('url'),
2 | WebSocket = require('ws'),
3 | uuidV4 = require('uuid').v4,
4 | UTILITIES_SERVER_PATH = '/utilities',
5 | TAG = '[utilities-server.js]',
6 | DEFAULT_TIME = '0001-01-01T12:00:00.000Z',
7 | HOURS = 12,
8 | MINUTES = 60,
9 | MINUTE_WIDTH = 2;
10 |
11 | module.exports = (http_server) => {
12 | const websocket_server = new WebSocket.Server({noServer: true});
13 |
14 | // Handle websocket server errors.
15 | websocket_server.on('error', (error) => console.error(TAG, 'Server error:', error));
16 |
17 | // Listen for devices connecting over WebSockets.
18 | http_server.on('upgrade', (request, ws, head) => {
19 | // Only listen to device connections.
20 | if (url.parse(request.url).pathname !== UTILITIES_SERVER_PATH) {
21 | return;
22 | }
23 |
24 | websocket_server.handleUpgrade(request, ws, head, (socket) => {
25 | socket.on('message', (data) => {
26 | let message;
27 |
28 | try {
29 | message = JSON.parse(data);
30 | } catch (error) {
31 | return;
32 | }
33 |
34 | if (message.event_type === 'generate-uuid') {
35 | socket.send(JSON.stringify({
36 | id: message.id,
37 | callback: true,
38 | payload: {uuid: uuidV4()}
39 | }));
40 | }
41 |
42 | if (message.event_type === 'time') {
43 | socket.send(JSON.stringify({
44 | id: message.id,
45 | callback: true,
46 | event_type: "time",
47 | payload: {time: Math.trunc(Date.now() / 1000)}
48 | }));
49 | }
50 |
51 | });
52 | });
53 | });
54 | };
55 |
--------------------------------------------------------------------------------
/server/website.setup.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const AccountsManager = require('./accounts/accounts-manager.js');
3 | const path = require('path');
4 | const busboy = require('connect-busboy');
5 | const passport = require('passport');
6 | const LocalStrategy = require('passport-local').Strategy;
7 | const compression = require('compression');
8 | const bodyParser = require('body-parser');
9 | const TAG = '[website.setup.js]';
10 | const app = express();
11 |
12 | const streamDir = path.join('/tmp', 'open-automation', 'stream');
13 |
14 | app.use(busboy());
15 | app.use(bodyParser.json({ limit: "500mb" }));
16 | app.use(express.urlencoded({ limit: '500mb', extended: true, parameterLimit: 50000 }));
17 | app.use(compression());
18 | app.use(passport.initialize());
19 | app.use(passport.session());
20 | app.use('/', express.static(path.join(__dirname, '..', 'public')));
21 | app.use('/stream', express.static(streamDir, {
22 | setHeaders: function(res, path, stat) {
23 | if (path.indexOf(".ts") > -1) {
24 | res.set("cache-control", "public, max-age=300");
25 | }
26 | }
27 | }));
28 | app.use('/recording', express.static('/usr/local/lib/open-automation/recording', {
29 | setHeaders: function(res, path, stat) {
30 | if (path.indexOf(".ts") > -1) {
31 | res.set("cache-control", "public, max-age=300");
32 | }
33 | }
34 | }));
35 |
36 | passport.serializeUser((user, done) => {
37 | done(null, user);
38 | });
39 |
40 | passport.use(new LocalStrategy((username, password, done) => {
41 | const account = AccountsManager.getAccountByUsername(username);
42 |
43 | if (!account) {
44 | console.log(TAG, `Login ${username}: account not found.`);
45 | return done(null, false);
46 | }
47 |
48 | account.isCorrectPassword(password).then((is_correct) => {
49 | if (!is_correct) {
50 | console.log(TAG, `Login ${username}: incorrect password.`);
51 | return done(null, false);
52 | }
53 |
54 | // Password is correct.
55 | return done(null, account);
56 | }).catch(() => {
57 | return done(null, false);
58 | });
59 | }));
60 |
61 | module.exports = app;
62 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= htmlWebpackPlugin.options.title %>
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // index.js
2 | import 'normalize.css';
3 | import './views/styles/base.css';
4 | import React from 'react';
5 | import ReactDOM from 'react-dom';
6 | import {Provider as ReduxProvider} from 'react-redux';
7 | import {ConnectedRouter} from 'connected-react-router';
8 | import createHistory from 'history/createBrowserHistory';
9 | import moment from 'moment';
10 | import momentDurationFormatSetup from 'moment-duration-format';
11 | import configureStore from './state/store';
12 | import {initialize as initializeConfig} from './state/ducks/config/operations.js';
13 | import {initialize as initializeSession} from './state/ducks/session/operations.js';
14 | import {listenForDeviceChanges} from './state/ducks/devices-list/operations.js';
15 | import {listenForRoomChanges} from './state/ducks/rooms-list/operations.js';
16 | import {listenForAutomationChanges} from './state/ducks/automations-list/operations.js';
17 | import AppContext from './views/AppContext.js';
18 | import App from './views/layouts/App';
19 |
20 | const history = createHistory(), // History object to share between router and store.
21 | reduxStore = configureStore(history), // Create store.
22 | THREE_SECONDS = 3,
23 | ONE_MINUTE_IN_SECONDS = 60,
24 | ONE_HOUR_IN_MINUTES = 60;
25 |
26 | // Configure moment.
27 | moment.relativeTimeThreshold('s', ONE_MINUTE_IN_SECONDS);
28 | moment.relativeTimeThreshold('ss', THREE_SECONDS);
29 | moment.relativeTimeThreshold('m', ONE_HOUR_IN_MINUTES);
30 | momentDurationFormatSetup(moment);
31 |
32 | // Save configuration to store.
33 | reduxStore.dispatch(initializeConfig(window.OpenAutomation.config));
34 |
35 | // Set up user if already logged in.
36 | reduxStore.dispatch(initializeSession());
37 |
38 | // Listen for changes pushed from server.
39 | reduxStore.dispatch(listenForDeviceChanges());
40 | reduxStore.dispatch(listenForRoomChanges());
41 | reduxStore.dispatch(listenForAutomationChanges());
42 |
43 | ReactDOM.render(
44 |
45 |
46 |
47 |
48 |
49 |
50 | ,
51 | document.getElementById('open-automation')
52 | );
53 |
--------------------------------------------------------------------------------
/src/state/ducks/automations-list/actions.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 |
3 | export const fetchAutomations = () => ({
4 | type: types.FETCH_AUTOMATIONS
5 | });
6 |
7 | export const fetchAutomationsSuccess = (automations) => ({
8 | type: types.FETCH_AUTOMATIONS_SUCCESS,
9 | payload: {automations}
10 | });
11 |
12 | export const fetchAutomationsError = (error) => ({
13 | type: types.FETCH_AUTOMATIONS_ERROR,
14 | payload: {error},
15 | error: true
16 | });
17 |
18 | export const addAutomation = (tempAutomationId, automation) => ({
19 | type: types.ADD_AUTOMATION,
20 | payload: {tempAutomationId, automation}
21 | });
22 |
23 | export const addAutomationSuccess = (tempAutomationId, automation) => ({
24 | type: types.ADD_AUTOMATION_SUCCESS,
25 | payload: {tempAutomationId, automation}
26 | });
27 |
28 | export const addAutomationError = (tempAutomationId, error) => ({
29 | type: types.ADD_AUTOMATION_ERROR,
30 | payload: {tempAutomationId, error},
31 | error: true
32 | });
33 |
34 | export const saveAutomation = (automation) => ({
35 | type: types.SAVE_AUTOMATION,
36 | payload: {automation}
37 | });
38 |
39 | export const saveAutomationError = (automation, originalAutomation, error) => ({
40 | type: types.SAVE_AUTOMATION_ERROR,
41 | payload: {automation, originalAutomation, error},
42 | error: true
43 | });
44 |
45 | export const deleteAutomation = (automationId) => ({
46 | type: types.DELETE_AUTOMATION,
47 | payload: {automationId}
48 | });
49 |
50 | export const deleteAutomationError = (automation, error) => ({
51 | type: types.DELETE_AUTOMATION_ERROR,
52 | payload: {automation, error},
53 | error: true
54 | });
55 |
--------------------------------------------------------------------------------
/src/state/ducks/automations-list/index.js:
--------------------------------------------------------------------------------
1 | import reducer from './reducers';
2 |
3 | import * as selectors from './selectors';
4 | import * as operations from './operations';
5 | import * as types from './types';
6 |
7 | export {
8 | selectors,
9 | operations,
10 | types
11 | };
12 |
13 | export default reducer;
14 |
--------------------------------------------------------------------------------
/src/state/ducks/automations-list/models/automation-record.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 |
3 | class AutomationRecord extends Immutable.Record({
4 | id: null,
5 | name: null,
6 | is_enabled: true,
7 | type: 'user',
8 | source: {web: true},
9 | user_editable: true,
10 | triggers: Immutable.List(),
11 | actions: Immutable.List(),
12 | conditions: Immutable.List(),
13 | scenes: Immutable.List(),
14 | notifications: Immutable.List(),
15 | isUnsaved: false,
16 | error: null
17 | }) {
18 | constructor (values = {}) {
19 | super({
20 | ...values,
21 | triggers: Immutable.List(values.triggers),
22 | actions: Immutable.List(values.actions),
23 | conditions: Immutable.List(values.conditions),
24 | scenes: Immutable.List(values.scenes),
25 | notifications: Immutable.List(values.notifications)
26 | });
27 | }
28 |
29 | set (key, value) {
30 | let _value;
31 |
32 | switch (key) {
33 | case 'triggers':
34 | case 'actions':
35 | case 'conditions':
36 | case 'scenes':
37 | case 'notifications':
38 | _value = Immutable.List(value);
39 | break;
40 | default:
41 | _value = value;
42 | break;
43 | }
44 |
45 | return super.set(key, _value);
46 | }
47 | }
48 |
49 | export default AutomationRecord;
50 |
--------------------------------------------------------------------------------
/src/state/ducks/automations-list/operations.js:
--------------------------------------------------------------------------------
1 | import * as actions from './actions';
2 | import Api from '../../../api.js';
3 | const {v4: uuidV4} = require('uuid'),
4 | listenForAutomationChanges = () => (dispatch) => {
5 | Api.on('automations', (data) => dispatch(actions.fetchAutomationsSuccess(data.automations)));
6 | },
7 | fetchAutomations = () => (dispatch) => {
8 | dispatch(actions.fetchAutomations());
9 | Api.getAutomations()
10 | .then((data) => {
11 | dispatch(actions.fetchAutomationsSuccess(data.automations));
12 | })
13 | .catch((error) => {
14 | dispatch(actions.fetchAutomationsError(error));
15 | });
16 | },
17 | addAutomation = (automation) => (dispatch) => {
18 | const tempId = uuidV4();
19 |
20 | dispatch(actions.addAutomation(tempId, automation));
21 | Api.addAutomation(automation)
22 | .then((data) => {
23 | dispatch(actions.addAutomationSuccess(tempId, data.automation));
24 | })
25 | .catch((error) => {
26 | dispatch(actions.addAutomationError(tempId, error));
27 | });
28 | },
29 | saveAutomation = (automation, originalAutomation) => (dispatch) => {
30 | dispatch(actions.saveAutomation(automation));
31 | Api.saveAutomation(automation)
32 | .catch((error) => {
33 | dispatch(actions.saveAutomationError(automation, originalAutomation, error));
34 | });
35 | },
36 | deleteAutomation = (automation) => (dispatch) => {
37 | dispatch(actions.deleteAutomation(automation.id));
38 | Api.deleteAutomation(automation.id)
39 | .catch((error) => {
40 | dispatch(actions.deleteAutomationError(automation, error));
41 | });
42 | };
43 |
44 | export {
45 | listenForAutomationChanges,
46 | fetchAutomations,
47 | addAutomation,
48 | saveAutomation,
49 | deleteAutomation
50 | };
51 |
--------------------------------------------------------------------------------
/src/state/ducks/automations-list/reducers.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import {immutableMapFromArray} from '../../../utilities.js';
3 | import Automation from './models/automation-record.js';
4 | import * as types from './types';
5 | import * as sessionTypes from '../session/types';
6 |
7 | const initialState = Immutable.Map({
8 | automations: Immutable.Map(),
9 | loading: false,
10 | fetched: false, // Whether first fetch has completed.
11 | error: false
12 | }),
13 | reducer = (state = initialState, action) => {
14 | switch (action.type) {
15 | case types.FETCH_AUTOMATIONS:
16 | return state.set('loading', true);
17 | case types.FETCH_AUTOMATIONS_SUCCESS:
18 | return state.merge({
19 | loading: false,
20 | fetched: true,
21 | error: false,
22 | automations: immutableMapFromArray(action.payload.automations, (automation) => new Automation(automation))
23 | });
24 | case types.FETCH_AUTOMATIONS_ERROR:
25 | return state.merge({
26 | loading: false,
27 | error: action.payload.error.message
28 | });
29 | case types.ADD_AUTOMATION:
30 | return state.setIn(
31 | ['automations', action.payload.tempAutomationId],
32 | new Automation({
33 | ...action.payload.automation,
34 | id: action.payload.tempAutomationId,
35 | isUnsaved: true
36 | })
37 | );
38 | case types.ADD_AUTOMATION_SUCCESS:
39 | return state.setIn(
40 | ['automations', action.payload.automation.id],
41 | new Automation(action.payload.automation)
42 | ).deleteIn(['automations', action.payload.tempAutomationId]);
43 | case types.ADD_AUTOMATION_ERROR:
44 | return state.merge({
45 | error: action.payload.error.message
46 | }).deleteIn(['automations', action.payload.tempAutomationId]);
47 | case types.SAVE_AUTOMATION:
48 | return state.mergeIn(
49 | ['automations', action.payload.automation.id],
50 | {
51 | ...action.payload.automation,
52 | error: null
53 | }
54 | );
55 | case types.SAVE_AUTOMATION_ERROR:
56 | return state.mergeIn(
57 | ['automations', action.payload.automation.id],
58 | {
59 | ...action.payload.originalAutomation,
60 | error: action.payload.error.message
61 | }
62 | );
63 | case types.DELETE_AUTOMATION:
64 | return state.deleteIn(['automations', action.payload.automationId]);
65 | case types.DELETE_AUTOMATION_ERROR:
66 | return state.setIn(
67 | ['automations', action.payload.automation.id],
68 | new Automation(action.payload.automation)
69 | ).set('error', action.payload.error.message);
70 | case sessionTypes.LOGOUT:
71 | return initialState;
72 | default:
73 | return state;
74 | }
75 | };
76 |
77 | export default reducer;
78 |
--------------------------------------------------------------------------------
/src/state/ducks/automations-list/selectors.js:
--------------------------------------------------------------------------------
1 | import Automation from './models/automation-record.js';
2 |
3 | const getAutomations = (automationsList, toJs = true) => {
4 | const automations = automationsList.get('automations');
5 |
6 | return toJs ? automations.toList().toJS() : automations;
7 | },
8 | getAutomationById = (automationsList, automationId, toJs = true) => {
9 | const automation = automationsList.getIn(['automations', automationId]);
10 |
11 | if (!automation) {
12 | return;
13 | }
14 |
15 | return toJs ? automation.toJS() : automation;
16 | },
17 | getEmptyAutomation = () => new Automation(),
18 | hasInitialFetchCompleted = (automationsList) => {
19 | return automationsList.get('fetched');
20 | },
21 | getAutomationsError = (automationsList) => {
22 | const error = automationsList.get('error');
23 |
24 | if (error) {
25 | return error;
26 | }
27 | };
28 |
29 | export {
30 | getAutomations,
31 | getAutomationById,
32 | getEmptyAutomation,
33 | hasInitialFetchCompleted,
34 | getAutomationsError
35 | };
36 |
--------------------------------------------------------------------------------
/src/state/ducks/automations-list/types.js:
--------------------------------------------------------------------------------
1 | export const FETCH_AUTOMATIONS = 'open-automation/automations-list/FETCH_AUTOMATIONS';
2 | export const FETCH_AUTOMATIONS_SUCCESS = 'open-automation/automations-list/FETCH_AUTOMATIONS_SUCCESS';
3 | export const FETCH_AUTOMATIONS_ERROR = 'open-automation/automations-list/FETCH_AUTOMATIONS_ERROR';
4 | export const SORT_AUTOMATIONS = 'open-automation/automations-list/SORT_AUTOMATIONS';
5 | export const SORT_AUTOMATIONS_ERROR = 'open-automation/automations-list/SORT_AUTOMATIONS_ERROR';
6 | export const ADD_AUTOMATION = 'open-automation/automations-list/ADD_AUTOMATION';
7 | export const ADD_AUTOMATION_SUCCESS = 'open-automation/automations-list/ADD_AUTOMATION_SUCCESS';
8 | export const ADD_AUTOMATION_ERROR = 'open-automation/automations-list/ADD_AUTOMATION_ERROR';
9 | export const SAVE_AUTOMATION = 'open-automation/automations-list/SAVE_AUTOMATION';
10 | export const SAVE_AUTOMATION_ERROR = 'open-automation/automations-list/SAVE_AUTOMATION_ERROR';
11 | export const DELETE_AUTOMATION = 'open-automation/automations-list/DELETE_AUTOMATION';
12 | export const DELETE_AUTOMATION_ERROR = 'open-automation/automations-list/DELETE_AUTOMATION_ERROR';
13 |
--------------------------------------------------------------------------------
/src/state/ducks/config/actions.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 |
3 | export const initialize = (config) => ({
4 | type: types.INITIALIZE,
5 | payload: {config}
6 | });
7 |
--------------------------------------------------------------------------------
/src/state/ducks/config/index.js:
--------------------------------------------------------------------------------
1 | import reducer from './reducers';
2 |
3 | import * as selectors from './selectors';
4 | import * as operations from './operations';
5 | import * as types from './types';
6 |
7 | export {
8 | selectors,
9 | operations,
10 | types
11 | };
12 |
13 | export default reducer;
14 |
--------------------------------------------------------------------------------
/src/state/ducks/config/operations.js:
--------------------------------------------------------------------------------
1 | import * as actions from './actions';
2 |
3 | const initialize = (config) => (dispatch) => {
4 | dispatch(actions.initialize(config));
5 | };
6 |
7 | export {
8 | initialize
9 | };
10 |
--------------------------------------------------------------------------------
/src/state/ducks/config/reducers.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 |
3 | const initialState = null,
4 | reducer = (state = initialState, action) => {
5 | switch (action.type) {
6 | case types.INITIALIZE:
7 | return {
8 | ...action.payload.config,
9 | stream_port: action.payload.config.stream_port || window.location.port
10 | };
11 | default:
12 | return state;
13 | }
14 | };
15 |
16 | export default reducer;
17 |
--------------------------------------------------------------------------------
/src/state/ducks/config/selectors.js:
--------------------------------------------------------------------------------
1 | const getAppName = (config) => config.app_name,
2 | getLogoPath = (config) => config.logo_path;
3 |
4 | export {
5 | getAppName,
6 | getLogoPath
7 | };
8 |
--------------------------------------------------------------------------------
/src/state/ducks/config/types.js:
--------------------------------------------------------------------------------
1 | export const INITIALIZE = 'open-automation/config/INITIALIZE';
2 |
--------------------------------------------------------------------------------
/src/state/ducks/devices-list/actions.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 |
3 | export const fetchDevices = () => ({
4 | type: types.FETCH_DEVICES
5 | });
6 |
7 | export const fetchDevicesSuccess = (devices) => ({
8 | type: types.FETCH_DEVICES_SUCCESS,
9 | payload: {devices}
10 | });
11 |
12 | export const fetchDevicesError = (error) => ({
13 | type: types.FETCH_DEVICES_ERROR,
14 | payload: {error},
15 | error: true
16 | });
17 |
18 | export const setSettings = (deviceId, settings) => ({
19 | type: types.SET_SETTINGS,
20 | payload: {deviceId, settings}
21 | });
22 |
23 | export const setSettingsError = (deviceId, originalSettings, error) => ({
24 | type: types.SET_SETTINGS_ERROR,
25 | payload: {deviceId, originalSettings, error},
26 | error: true
27 | });
28 |
29 | export const setDeviceRoom = (deviceId, roomId) => ({
30 | type: types.SET_DEVICE_ROOM,
31 | payload: {deviceId, roomId}
32 | });
33 |
34 | export const setDeviceRoomError = (deviceId, originalRoomId, error) => ({
35 | type: types.SET_DEVICE_ROOM_ERROR,
36 | payload: {deviceId, originalRoomId, error},
37 | error: true
38 | });
39 |
40 | export const deleteDevice = (deviceId) => ({
41 | type: types.DELETE_DEVICE,
42 | payload: {deviceId}
43 | });
44 |
45 | export const deleteDeviceError = (device, error) => ({
46 | type: types.DELETE_DEVICE_ERROR,
47 | payload: {device, error},
48 | error: true
49 | });
50 |
--------------------------------------------------------------------------------
/src/state/ducks/devices-list/index.js:
--------------------------------------------------------------------------------
1 | import reducer from './reducers';
2 |
3 | import * as selectors from './selectors';
4 | import * as operations from './operations';
5 | import * as types from './types';
6 |
7 | export {
8 | selectors,
9 | operations,
10 | types
11 | };
12 |
13 | export default reducer;
14 |
--------------------------------------------------------------------------------
/src/state/ducks/devices-list/models/device-record.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 |
3 | class DeviceRecord extends Immutable.Record({
4 | id: null,
5 | account_id: null,
6 | gateway_id: null,
7 | services: Immutable.OrderedMap({}),
8 | settings_definitions: Immutable.OrderedMap({}),
9 | settings: Immutable.Map({}),
10 | automator_supported: false,
11 | room_id: null,
12 | info: Immutable.Map({}),
13 | state: Immutable.Map({}),
14 | error: null
15 | }) {
16 | constructor (values) {
17 | super({
18 | ...values,
19 | services: Immutable.OrderedMap(values.services.map(({id, type}) => [
20 | id,
21 | {id, type}
22 | ])),
23 | settings_definitions: Immutable.OrderedMap(values.settings_definitions),
24 | settings: Immutable.Map(values.settings),
25 | state: Immutable.Map(values.state)
26 | });
27 | }
28 |
29 | set (key, value) {
30 | let _value;
31 |
32 | switch (key) {
33 | case 'settings':
34 | case 'state':
35 | _value = Immutable.Map(value);
36 | break;
37 | case 'settings_definitions':
38 | _value = Immutable.OrderedMap(value);
39 | break;
40 | default:
41 | _value = value;
42 | break;
43 | }
44 |
45 | return super.set(key, _value);
46 | }
47 | }
48 |
49 | export default DeviceRecord;
50 |
--------------------------------------------------------------------------------
/src/state/ducks/devices-list/operations.js:
--------------------------------------------------------------------------------
1 | import * as actions from './actions';
2 | import Api from '../../../api.js';
3 |
4 | const listenForDeviceChanges = () => (dispatch) => {
5 | Api.on('devices', (data) => dispatch(actions.fetchDevicesSuccess(data.devices)));
6 | },
7 | fetchDevices = () => (dispatch) => {
8 | dispatch(actions.fetchDevices());
9 |
10 | Api.getDevices().then((data) => {
11 | dispatch(actions.fetchDevicesSuccess(data.devices));
12 | }).catch((error) => {
13 | dispatch(actions.fetchDevicesError(error));
14 | });
15 | },
16 | setDeviceSettings = (deviceId, settings, originalSettings) => (dispatch) => {
17 | dispatch(actions.setSettings(deviceId, settings));
18 |
19 | Api.setDeviceSettings(deviceId, settings).catch((error) => {
20 | dispatch(actions.setSettingsError(deviceId, originalSettings, error));
21 | });
22 | },
23 | setDeviceRoom = (deviceId, roomId, originalRoomId) => (dispatch) => {
24 | dispatch(actions.setDeviceRoom(deviceId, roomId));
25 |
26 | Api.setDeviceRoom(deviceId, roomId).catch((error) => {
27 | dispatch(actions.setDeviceRoomError(deviceId, originalRoomId, error));
28 | });
29 | },
30 | deleteDevice = (deviceId) => (dispatch) => {
31 | dispatch(actions.deleteDevice(deviceId));
32 |
33 | Api.deleteDevice(deviceId).catch((error) => {
34 | dispatch(actions.deleteDeviceError(deviceId, error));
35 | });
36 | };
37 |
38 | export {
39 | listenForDeviceChanges,
40 | fetchDevices,
41 | setDeviceSettings,
42 | setDeviceRoom,
43 | deleteDevice
44 | };
45 |
--------------------------------------------------------------------------------
/src/state/ducks/devices-list/reducers.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import {immutableOrderedMapFromArray} from '../../../utilities.js';
3 | import Device from './models/device-record.js';
4 | import * as types from './types';
5 | import * as sessionTypes from '../session/types';
6 |
7 | const initialState = Immutable.Map({
8 | devices: Immutable.OrderedMap(),
9 | loading: false,
10 | fetched: false, // Whether first fetch has completed.
11 | error: false
12 | }),
13 | reducer = (state = initialState, action) => {
14 | switch (action.type) {
15 | case types.FETCH_DEVICES:
16 | return state.set('loading', true);
17 | case types.FETCH_DEVICES_SUCCESS:
18 | return state.merge({
19 | loading: false,
20 | fetched: true,
21 | error: false,
22 | devices: immutableOrderedMapFromArray(action.payload.devices, (device) => new Device(device))
23 | });
24 | case types.FETCH_DEVICES_ERROR:
25 | return state.merge({
26 | loading: false,
27 | error: action.payload.error.message
28 | });
29 | case types.SET_SETTINGS:
30 | return state.mergeIn(
31 | ['devices', action.payload.deviceId],
32 | {
33 | settings: action.payload.settings,
34 | error: null
35 | }
36 | );
37 | case types.SET_SETTINGS_ERROR:
38 | return state.mergeIn(
39 | ['devices', action.payload.deviceId],
40 | {
41 | settings: action.payload.originalSettings,
42 | error: action.payload.error.message
43 | }
44 | );
45 | case types.SET_DEVICE_ROOM:
46 | return state.mergeIn(
47 | ['devices', action.payload.deviceId],
48 | {
49 | room_id: action.payload.roomId,
50 | error: null
51 | }
52 | );
53 | case types.SET_DEVICE_ROOM_ERROR:
54 | return state.mergeIn(
55 | ['devices', action.payload.deviceId],
56 | {
57 | room_id: action.payload.originalRoomId,
58 | error: action.payload.error.message
59 | }
60 | );
61 | case types.DELETE_DEVICE:
62 | return state.deleteIn(['devices', action.payload.deviceId]);
63 | case types.DELETE_DEVICE_ERROR:
64 | return state.setIn(
65 | ['devices', action.payload.device.id],
66 | new Device(action.payload.device)
67 | ).set('error', action.payload.error.message);
68 | case sessionTypes.LOGOUT:
69 | return initialState;
70 | default:
71 | return state;
72 | }
73 | };
74 |
75 | export default reducer;
76 |
--------------------------------------------------------------------------------
/src/state/ducks/devices-list/selectors.js:
--------------------------------------------------------------------------------
1 | const getDevices = (devicesList, toJs = true) => {
2 | const devices = devicesList.get('devices');
3 |
4 | return toJs ? devicesToJs(devices) : devices;
5 | },
6 | getDeviceById = (devicesList, deviceId, toJs = true) => {
7 | const device = devicesList.getIn(['devices', deviceId]);
8 |
9 | if (!device) {
10 | return;
11 | }
12 |
13 | return toJs ? device.set('services', device.services.toList()).toJS() : device;
14 | },
15 | getDevicesWithAutomatorSupport = (devicesList, toJs = true) => {
16 | const devices = devicesList.get('devices').filter((device) => device.automator_supported);
17 |
18 | return toJs ? devicesToJs(devices) : devices;
19 | },
20 | hasInitialFetchCompleted = (devicesList) => {
21 | return devicesList.get('fetched');
22 | },
23 | devicesToJs = (devices) => {
24 | return devices.map((device) => {
25 | return device.set('services', device.services.toList());
26 | }).toList().toJS();
27 | };
28 |
29 | export {
30 | getDevices,
31 | getDeviceById,
32 | getDevicesWithAutomatorSupport,
33 | hasInitialFetchCompleted
34 | };
35 |
--------------------------------------------------------------------------------
/src/state/ducks/devices-list/types.js:
--------------------------------------------------------------------------------
1 | export const FETCH_DEVICES = 'open-automation/devices-list/FETCH_DEVICES';
2 | export const FETCH_DEVICES_SUCCESS = 'open-automation/devices-list/FETCH_DEVICES_SUCCESS';
3 | export const FETCH_DEVICES_ERROR = 'open-automation/devices-list/FETCH_DEVICES_ERROR';
4 | export const SET_SETTINGS = 'open-automation/devices-list/SET_SETTINGS';
5 | export const SET_SETTINGS_ERROR = 'open-automation/devices-list/SET_SETTINGS_ERROR';
6 | export const SET_DEVICE_ROOM = 'open-automation/devices-list/SET_DEVICE_ROOM';
7 | export const SET_DEVICE_ROOM_ERROR = 'open-automation/devices-list/SET_DEVICE_ROOM_ERROR';
8 | export const DELETE_DEVICE = 'open-automation/devices-list/DELETE_DEVICE';
9 | export const DELETE_DEVICE_ERROR = 'open-automation/devices-list/DELETE_DEVICE_ERROR';
10 |
--------------------------------------------------------------------------------
/src/state/ducks/index.js:
--------------------------------------------------------------------------------
1 | export {default as devicesList} from './devices-list';
2 | export {default as servicesList} from './services-list';
3 | export {default as roomsList} from './rooms-list';
4 | export {default as automationsList} from './automations-list';
5 | export {default as session} from './session';
6 | export {default as navigation} from './navigation';
7 | export {default as config} from './config';
8 |
--------------------------------------------------------------------------------
/src/state/ducks/navigation/actions.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 |
3 | export const loadContext = (path) => ({
4 | type: types.LOAD_CONTEXT,
5 | payload: {path}
6 | }),
7 | unloadContext = (path) => ({
8 | type: types.UNLOAD_CONTEXT,
9 | payload: {path}
10 | }),
11 | loadScreen = (context, path, depth, currentFullPath, title, shouldShowTitle) => ({
12 | type: types.LOAD_SCREEN,
13 | payload: {context, path, depth, currentFullPath, title, shouldShowTitle}
14 | }),
15 | unloadScreen = (context, path) => ({
16 | type: types.UNLOAD_SCREEN,
17 | payload: {context, path}
18 | });
19 |
--------------------------------------------------------------------------------
/src/state/ducks/navigation/index.js:
--------------------------------------------------------------------------------
1 | import reducer from './reducers';
2 |
3 | import * as selectors from './selectors';
4 | import * as operations from './operations';
5 | import * as types from './types';
6 |
7 | export {
8 | selectors,
9 | operations,
10 | types
11 | };
12 |
13 | export default reducer;
14 |
--------------------------------------------------------------------------------
/src/state/ducks/navigation/operations.js:
--------------------------------------------------------------------------------
1 | import * as actions from './actions';
2 |
3 | const loadContext = (path) => (dispatch) => {
4 | dispatch(actions.loadContext(path));
5 | },
6 | unloadContext = (path) => (dispatch) => {
7 | dispatch(actions.unloadContext(path));
8 | },
9 | loadScreen = (context, path, depth, currentFullPath, title, shouldShowTitle) => (dispatch) => {
10 | dispatch(actions.loadScreen(context, path, depth, currentFullPath, title, shouldShowTitle));
11 | },
12 | unloadScreen = (context, path) => (dispatch) => {
13 | dispatch(actions.unloadScreen(context, path));
14 | };
15 |
16 | export {
17 | loadContext,
18 | unloadContext,
19 | loadScreen,
20 | unloadScreen
21 | };
22 |
--------------------------------------------------------------------------------
/src/state/ducks/navigation/types.js:
--------------------------------------------------------------------------------
1 | export const LOAD_CONTEXT = 'open-automation/navigation/LOAD_CONTEXT';
2 | export const UNLOAD_CONTEXT = 'open-automation/navigation/UNLOAD_CONTEXT';
3 | export const LOAD_SCREEN = 'open-automation/navigation/LOAD_SCREEN';
4 | export const UNLOAD_SCREEN = 'open-automation/navigation/UNLOAD_SCREEN';
5 |
--------------------------------------------------------------------------------
/src/state/ducks/rooms-list/actions.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 |
3 | export const fetchRooms = () => ({
4 | type: types.FETCH_ROOMS
5 | });
6 |
7 | export const fetchRoomsSuccess = (rooms) => ({
8 | type: types.FETCH_ROOMS_SUCCESS,
9 | payload: {rooms}
10 | });
11 |
12 | export const fetchRoomsError = (error) => ({
13 | type: types.FETCH_ROOMS_ERROR,
14 | payload: {error},
15 | error: true
16 | });
17 |
18 | export const sortRooms = (order) => ({
19 | type: types.SORT_ROOMS,
20 | payload: {order}
21 | });
22 |
23 | export const sortRoomsError = (roomId, originalName, error) => ({
24 | type: types.SORT_ROOMS_ERROR,
25 | payload: {error},
26 | error: true
27 | });
28 |
29 | export const addRoom = (tempRoomId, room) => ({
30 | type: types.ADD_ROOM,
31 | payload: {tempRoomId, room}
32 | });
33 |
34 | export const addRoomSuccess = (tempRoomId, room) => ({
35 | type: types.ADD_ROOM_SUCCESS,
36 | payload: {tempRoomId, room}
37 | });
38 |
39 | export const addRoomError = (tempRoomId, error) => ({
40 | type: types.ADD_ROOM_ERROR,
41 | payload: {tempRoomId, error},
42 | error: true
43 | });
44 |
45 | export const setRoomName = (roomId, name) => ({
46 | type: types.SET_ROOM_NAME,
47 | payload: {roomId, name}
48 | });
49 |
50 | export const setRoomNameError = (roomId, originalName, error) => ({
51 | type: types.SET_ROOM_NAME_ERROR,
52 | payload: {roomId, originalName, error},
53 | error: true
54 | });
55 |
56 | export const deleteRoom = (roomId) => ({
57 | type: types.DELETE_ROOM,
58 | payload: {roomId}
59 | });
60 |
61 | export const deleteRoomError = (room, error) => ({
62 | type: types.DELETE_ROOM_ERROR,
63 | payload: {room, error},
64 | error: true
65 | });
66 |
--------------------------------------------------------------------------------
/src/state/ducks/rooms-list/index.js:
--------------------------------------------------------------------------------
1 | import reducer from './reducers';
2 |
3 | import * as selectors from './selectors';
4 | import * as operations from './operations';
5 | import * as types from './types';
6 |
7 | export {
8 | selectors,
9 | operations,
10 | types
11 | };
12 |
13 | export default reducer;
14 |
--------------------------------------------------------------------------------
/src/state/ducks/rooms-list/models/room-record.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 |
3 | class RoomRecord extends Immutable.Record({
4 | id: null,
5 | name: null,
6 | isUnsaved: false
7 | }) {}
8 |
9 | export default RoomRecord;
10 |
--------------------------------------------------------------------------------
/src/state/ducks/rooms-list/operations.js:
--------------------------------------------------------------------------------
1 | import * as actions from './actions';
2 | import Api from '../../../api.js';
3 | const {v4: uuidV4} = require('uuid');
4 |
5 | const listenForRoomChanges = () => (dispatch) => {
6 | Api.on('rooms', (data) => dispatch(actions.fetchRoomsSuccess(data.rooms)));
7 | },
8 | fetchRooms = () => (dispatch) => {
9 | dispatch(actions.fetchRooms());
10 |
11 | Api.getRooms().then((data) => {
12 | dispatch(actions.fetchRoomsSuccess(data.rooms));
13 | }).catch((error) => {
14 | dispatch(actions.fetchRoomsError(error));
15 | });
16 | },
17 | sortRooms = (order) => (dispatch) => {
18 | dispatch(actions.sortRooms(order));
19 |
20 | Api.sortRooms(order).catch((error) => {
21 | dispatch(actions.sortRoomsError(error));
22 | });
23 | },
24 | addRoom = (name) => (dispatch) => {
25 | const tempId = uuidV4();
26 |
27 | dispatch(actions.addRoom(tempId, {name}));
28 |
29 | Api.addRoom(name).then((data) => {
30 | dispatch(actions.addRoomSuccess(tempId, data.room));
31 | }).catch((error) => {
32 | dispatch(actions.addRoomError(tempId, error));
33 | });
34 | },
35 | setRoomName = (roomId, name, originalName) => (dispatch) => {
36 | dispatch(actions.setRoomName(roomId, name));
37 |
38 | Api.nameRoom(roomId, name).catch((error) => {
39 | dispatch(actions.setRoomNameError(roomId, originalName, error));
40 | });
41 | },
42 | deleteRoom = (room) => (dispatch) => {
43 | dispatch(actions.deleteRoom(room.id));
44 |
45 | Api.deleteRoom(room.id).catch((error) => {
46 | dispatch(actions.deleteRoomError(room, error));
47 | });
48 | };
49 |
50 | export {
51 | listenForRoomChanges,
52 | fetchRooms,
53 | sortRooms,
54 | addRoom,
55 | setRoomName,
56 | deleteRoom
57 | };
58 |
--------------------------------------------------------------------------------
/src/state/ducks/rooms-list/selectors.js:
--------------------------------------------------------------------------------
1 | const getRooms = (roomsList, toJs = true) => {
2 | const rooms = roomsList.get('rooms');
3 |
4 | return toJs ? rooms.toList().toJS() : rooms;
5 | },
6 | getRoomById = (roomsList, roomId, toJs = true) => {
7 | const room = roomsList.getIn(['rooms', roomId]);
8 |
9 | if (!room) {
10 | return;
11 | }
12 |
13 | return toJs ? room.toJS() : room;
14 | },
15 | hasInitialFetchCompleted = (roomsList) => {
16 | return roomsList.get('fetched');
17 | },
18 | getRoomsError = (roomsList) => {
19 | const error = roomsList.get('error');
20 |
21 | if (error) {
22 | return error;
23 | }
24 | };
25 |
26 | export {
27 | getRooms,
28 | getRoomById,
29 | hasInitialFetchCompleted,
30 | getRoomsError
31 | };
32 |
--------------------------------------------------------------------------------
/src/state/ducks/rooms-list/types.js:
--------------------------------------------------------------------------------
1 | export const FETCH_ROOMS = 'open-automation/rooms-list/FETCH_ROOMS';
2 | export const FETCH_ROOMS_SUCCESS = 'open-automation/rooms-list/FETCH_ROOMS_SUCCESS';
3 | export const FETCH_ROOMS_ERROR = 'open-automation/rooms-list/FETCH_ROOMS_ERROR';
4 | export const SORT_ROOMS = 'open-automation/rooms-list/SORT_ROOMS';
5 | export const SORT_ROOMS_ERROR = 'open-automation/rooms-list/SORT_ROOMS_ERROR';
6 | export const ADD_ROOM = 'open-automation/rooms-list/ADD_ROOM';
7 | export const ADD_ROOM_SUCCESS = 'open-automation/rooms-list/ADD_ROOM_SUCCESS';
8 | export const ADD_ROOM_ERROR = 'open-automation/rooms-list/ADD_ROOM_ERROR';
9 | export const SET_ROOM_NAME = 'open-automation/rooms-list/SET_ROOM_NAME';
10 | export const SET_ROOM_NAME_ERROR = 'open-automation/rooms-list/SET_ROOM_NAME_ERROR';
11 | export const DELETE_ROOM = 'open-automation/rooms-list/DELETE_ROOM';
12 | export const DELETE_ROOM_ERROR = 'open-automation/rooms-list/DELETE_ROOM_ERROR';
13 |
--------------------------------------------------------------------------------
/src/state/ducks/services-list/index.js:
--------------------------------------------------------------------------------
1 | import reducer from './reducers';
2 |
3 | import * as selectors from './selectors';
4 | import * as operations from './operations';
5 | import * as types from './types';
6 |
7 | export {
8 | selectors,
9 | operations,
10 | types
11 | };
12 |
13 | export default reducer;
14 |
--------------------------------------------------------------------------------
/src/state/ducks/services-list/models/camera-recording-record.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 |
3 | const CameraRecordingRecord = Immutable.Record({
4 | id: null,
5 | camera_id: null,
6 | date: null,
7 | duration: 0,
8 | width: 640,
9 | height: 480,
10 | audio_streaming_token: null,
11 | file: null,
12 | streaming_token: null
13 | });
14 |
15 | export default CameraRecordingRecord;
16 |
--------------------------------------------------------------------------------
/src/state/ducks/services-list/models/camera.js:
--------------------------------------------------------------------------------
1 | import ServiceRecord from './service-record.js';
2 |
3 | // The Camera model defines the fields specific to a camera service and their defualt values.
4 | class Camera extends ServiceRecord({
5 | recordingsList: null,
6 | streaming_token: null,
7 | settings: {
8 | resolution_w: 640,
9 | resolution_h: 480,
10 | network_path: '',
11 | rotation: 0
12 | },
13 | state: {
14 | motion_detected_date: null
15 | },
16 | preview_image: null,
17 | preview_image_fetch_date: null
18 | }) {}
19 |
20 | export default Camera;
21 |
--------------------------------------------------------------------------------
/src/state/ducks/services-list/models/gateway.js:
--------------------------------------------------------------------------------
1 | import ServiceRecord from './service-record.js';
2 |
3 | // The Gateway model defines the fields specific to a gateway service and their defualt values.
4 | class Gateway extends ServiceRecord() {}
5 |
6 | export default Gateway;
7 |
--------------------------------------------------------------------------------
/src/state/ducks/services-list/models/service.js:
--------------------------------------------------------------------------------
1 | import ServiceRecord from './service-record.js';
2 | import Gateway from './gateway.js';
3 | import Camera from './camera.js';
4 |
5 | const Service = ServiceRecord({}), // Generic service
6 | // Service factory
7 | createService = (service) => {
8 | switch (service.type) {
9 | case 'camera':
10 | return new Camera(service);
11 | case 'network-camera':
12 | return new Camera(service);
13 | case 'gateway':
14 | return new Gateway(service);
15 | default:
16 | return new Service(service);
17 | }
18 | };
19 |
20 | export default createService;
21 |
--------------------------------------------------------------------------------
/src/state/ducks/services-list/recordings.worker.js:
--------------------------------------------------------------------------------
1 | onmessage = (message) => {
2 | if (!Array.isArray(message.data.recordings)) {
3 | return;
4 | }
5 |
6 | const recordingsDateIndex = new Map(),
7 | datesOfRecordings = new Map();
8 |
9 | message.data.recordings.forEach((recording) => {
10 | const date = new Date(recording.date),
11 | month = date.getFullYear() + '-' + (date.getMonth() + 1),
12 | day = date.getDate(),
13 | dateKey = month + '-' + day,
14 | recordingsForDate = recordingsDateIndex.get(dateKey),
15 | datesForMonth = datesOfRecordings.get(month);
16 |
17 | if (recordingsForDate) {
18 | recordingsForDate.add(recording.id);
19 | } else {
20 | recordingsDateIndex.set(dateKey, new Set([recording.id]));
21 | }
22 |
23 | if (datesForMonth) {
24 | datesForMonth.add(day);
25 | } else {
26 | datesOfRecordings.set(month, new Set([day]));
27 | }
28 | });
29 |
30 | postMessage({
31 | dateIndex: recordingsDateIndex,
32 | dates: datesOfRecordings
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/src/state/ducks/services-list/types.js:
--------------------------------------------------------------------------------
1 | export const DO_SERVICE_ACTION = 'open-automation/services-list/DO_SERVICE_ACTION';
2 | export const DO_SERVICE_ACTION_ERROR = 'open-automation/services-list/DO_SERVICE_ACTION_ERROR';
3 | export const SET_SETTINGS = 'open-automation/services-list/SET_SETTINGS';
4 | export const SET_SETTINGS_ERROR = 'open-automation/services-list/SET_SETTINGS_ERROR';
5 | export const FETCH_SERVICE_LOG = 'open-automation/services-list/FETCH_SERVICE_LOG';
6 | export const FETCH_SERVICE_LOG_SUCCESS = 'open-automation/services-list/FETCH_SERVICE_LOG_SUCCESS';
7 | export const FETCH_SERVICE_LOG_ERROR = 'open-automation/services-list/FETCH_SERVICE_LOG_ERROR';
8 | export const FETCH_SERVICE_STATE = 'open-automation/services-list/FETCH_SERVICE_LOG';
9 | export const FETCH_SERVICE_STATE_SUCCESS = 'open-automation/services-list/FETCH_SERVICE_LOG_SUCCESS';
10 | export const FETCH_SERVICE_STATE_ERROR = 'open-automation/services-list/FETCH_SERVICE_LOG_ERROR';
11 | export const FETCH_CAMERA_RECORDINGS = 'open-automation/services-list/FETCH_CAMERA_RECORDINGS';
12 | export const FETCH_CAMERA_RECORDINGS_SUCCESS = 'open-automation/services-list/FETCH_CAMERA_RECORDINGS_SUCCESS';
13 | export const FETCH_CAMERA_RECORDINGS_ERROR = 'open-automation/services-list/FETCH_CAMERA_RECORDINGS_ERROR';
14 | export const STREAM_CAMERA_LIVE = 'open-automation/services-list/STREAM_CAMERA_LIVE';
15 | export const STREAM_AUDIO_LIVE = 'open-automation/services-list/STREAM_AUDIO_LIVE';
16 | export const STREAM_CAMERA_RECORDING = 'open-automation/services-list/STREAM_CAMERA_RECORDING';
17 | export const STREAM_CAMERA_AUDIO_RECORDING = 'open-automation/services-list/STREAM_CAMERA_AUDIO_RECORDING';
18 |
--------------------------------------------------------------------------------
/src/state/ducks/session/actions.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 |
3 | export const initialize = (isAuthenticated) => ({
4 | type: types.INITIALIZE,
5 | payload: {isAuthenticated}
6 | });
7 |
8 | export const login = () => ({
9 | type: types.LOGIN
10 | });
11 |
12 | export const loginSuccess = (user) => ({
13 | type: types.LOGIN_SUCCESS,
14 | payload: {user}
15 | });
16 |
17 | export const loginError = (error) => ({
18 | type: types.LOGIN_ERROR,
19 | payload: {error},
20 | error: true
21 | });
22 |
23 | export const logout = () => ({
24 | type: types.LOGOUT
25 | });
26 |
27 | export const logoutSuccess = () => ({
28 | type: types.LOGOUT_SUCCESS
29 | });
30 |
31 | export const logoutError = (error) => ({
32 | type: types.LOGOUT_ERROR,
33 | payload: {error},
34 | error: true
35 | });
36 |
37 | export const register = () => ({
38 | type: types.REGISTER
39 | });
40 |
41 | export const registerError = (error) => ({
42 | type: types.REGISTER_ERROR,
43 | payload: {error},
44 | error: true
45 | });
46 |
47 | export const setArmed = () => ({
48 | type: types.SET_ARMED,
49 | payload: {mode: -1}
50 | });
51 |
52 | export const setArmedSuccess = (mode) => ({
53 | type: types.SET_ARMED_SUCCESS,
54 | payload: {mode},
55 | error: true
56 | });
57 |
58 | export const setArmedError = (mode, error) => ({
59 | type: types.SET_ARMED_ERROR,
60 | payload: {mode, error},
61 | error: true
62 | });
63 |
64 | export const changePassword = () => ({
65 | type: types.CHANGE_PASSWORD
66 | });
67 |
68 | export const changePasswordSuccess = () => ({
69 | type: types.CHANGE_PASSWORD_SUCCESS
70 | });
71 |
72 | export const changePasswordError = (error) => ({
73 | type: types.CHANGE_PASSWORD_ERROR,
74 | payload: {error},
75 | error: true
76 | });
77 |
78 |
--------------------------------------------------------------------------------
/src/state/ducks/session/index.js:
--------------------------------------------------------------------------------
1 | import reducer from './reducers';
2 |
3 | import * as selectors from './selectors';
4 | import * as operations from './operations';
5 | import * as types from './types';
6 |
7 | export {
8 | selectors,
9 | operations,
10 | types
11 | };
12 |
13 | export default reducer;
14 |
--------------------------------------------------------------------------------
/src/state/ducks/session/operations.js:
--------------------------------------------------------------------------------
1 | import * as actions from './actions';
2 | import Api from '../../../api.js';
3 |
4 | const initialize = () => (dispatch) => {
5 | const isLoggedIn = Boolean(localStorage.getItem('is_logged_in'));
6 |
7 | dispatch(actions.initialize(isLoggedIn));
8 |
9 | if (isLoggedIn) {
10 | // Try to refresh the access token to make sure it's still valid.
11 | Api.getAccessToken().then((user) => {
12 | // User's access token is valid. Connect to API.
13 | Api.connect().then(() => {
14 | dispatch(loginSuccess(user));
15 | });
16 | }).catch(() => {
17 | // User's access token is invalid. User is not logged in.
18 | dispatch(logout());
19 | });
20 | }
21 |
22 | // When an API authentication error happens, log out.
23 | Api.on('authentication', (data) => {
24 | if (data.error) {
25 | dispatch(logout());
26 | }
27 | });
28 | },
29 | login = (username, password) => (dispatch) => {
30 | dispatch(actions.login());
31 |
32 | Api.login(username, password).then((user) => {
33 | dispatch(loginSuccess(user));
34 | }).catch((error) => {
35 | dispatch(actions.loginError(error));
36 | });
37 | },
38 | loginSuccess = (user) => (dispatch) => {
39 | localStorage.setItem('is_logged_in', true);
40 |
41 | dispatch(actions.loginSuccess(user));
42 | },
43 | logout = () => (dispatch) => {
44 | dispatch(actions.logout());
45 |
46 | localStorage.removeItem('is_logged_in');
47 |
48 | Api.logout().then(() => {
49 | dispatch(actions.logoutSuccess());
50 | }).catch((error) => {
51 | dispatch(actions.logoutError(error));
52 | });
53 | },
54 | register = (username, password) => (dispatch) => {
55 | dispatch(actions.register());
56 |
57 | Api.createAccount({username, password}).then(() => {
58 | dispatch(login(username, password));
59 | }).catch((error) => {
60 | dispatch(actions.registerError(error));
61 | });
62 | },
63 | changePassword = (username, currentPassword, newPassword) => (dispatch) => {
64 | dispatch(actions.changePassword());
65 |
66 | Api.changePassword({username, currentPassword, newPassword})
67 | .then(() => {
68 | dispatch(actions.changePasswordSuccess());
69 | })
70 | .catch((error) => {
71 | dispatch(actions.changePasswordError(error));
72 | });
73 | },
74 | setArmed = (mode) => (dispatch) => {
75 | dispatch(actions.setArmed());
76 |
77 | Api.setArmed(mode).then((data) => {
78 | dispatch(actions.setArmedSuccess(data.mode));
79 | }).catch((error, data) => {
80 | dispatch(actions.setArmedError(data.mode, error));
81 | });
82 | };
83 |
84 | export {
85 | initialize,
86 | login,
87 | logout,
88 | register,
89 | changePassword,
90 | setArmed
91 | };
92 |
--------------------------------------------------------------------------------
/src/state/ducks/session/selectors.js:
--------------------------------------------------------------------------------
1 | const isAuthenticated = (session) => Boolean(session.user),
2 | isLoading = (session) => session.loading,
3 | getUsername = (session) => session.user && session.user.username,
4 | getArmed = (session) => session.armed;
5 |
6 | export {
7 | isAuthenticated,
8 | isLoading,
9 | getUsername,
10 | getArmed
11 | };
12 |
--------------------------------------------------------------------------------
/src/state/ducks/session/types.js:
--------------------------------------------------------------------------------
1 | export const INITIALIZE = 'open-automation/session/INITIALIZE';
2 | export const LOGIN = 'open-automation/session/LOGIN';
3 | export const LOGIN_SUCCESS = 'open-automation/session/LOGIN_SUCCESS';
4 | export const LOGIN_ERROR = 'open-automation/session/LOGIN_ERROR';
5 | export const LOGOUT = 'open-automation/session/LOGOUT';
6 | export const LOGOUT_SUCCESS = 'open-automation/session/LOGOUT_SUCCESS';
7 | export const LOGOUT_ERROR = 'open-automation/session/LOGOUT_ERROR';
8 | export const REGISTER = 'open-automation/session/REGISTER';
9 | export const REGISTER_ERROR = 'open-automation/session/REGISTER_ERROR';
10 | export const SET_ARMED = 'open-automation/session/SET_ARMED';
11 | export const SET_ARMED_SUCCESS = 'open-automation/session/SET_ARMED_SUCCESS';
12 | export const SET_ARMED_ERROR = 'open-automation/session/SET_ARMED_ERROR';
13 | export const CHANGE_PASSWORD = 'CHANGE_PASSWORD';
14 | export const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS';
15 | export const CHANGE_PASSWORD_ERROR = 'CHANGE_PASSWORD_ERROR';
16 |
--------------------------------------------------------------------------------
/src/state/store.js:
--------------------------------------------------------------------------------
1 | import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
2 | import {connectRouter, routerMiddleware} from 'connected-react-router';
3 | import thunk from 'redux-thunk';
4 | import * as reducers from './ducks';
5 |
6 | function generateRootReducer (reducersToCombine, history) {
7 | return combineReducers({
8 | ...reducersToCombine,
9 | router: connectRouter(history)
10 | });
11 | }
12 |
13 | export default function configureStore (history, initialState) {
14 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // Middleware wrapper for Redux DevTools browser extension
15 |
16 | // Enable hot-reloading reducers.
17 | if (module.hot) {
18 | module.hot.accept('./ducks', () => {
19 | const newReducers = require('./ducks/index.js'); // eslint-disable-line global-require
20 |
21 | store.replaceReducer(generateRootReducer(newReducers, history));
22 | });
23 | }
24 |
25 | const store = createStore(
26 | generateRootReducer(reducers, history),
27 | initialState,
28 | composeEnhancers(applyMiddleware(
29 | thunk,
30 | routerMiddleware(history)
31 | ))
32 | );
33 |
34 | return store;
35 | }
36 |
--------------------------------------------------------------------------------
/src/utilities.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 |
3 | export const isEmpty = (value) => {
4 | if (typeof value === 'number') {
5 | if (Object.is(value, NaN)) {
6 | return true;
7 | }
8 |
9 | return false;
10 | }
11 |
12 | if (typeof value === 'boolean') {
13 | return false;
14 | }
15 |
16 | if (typeof value === 'undefined' || value === null) {
17 | return true;
18 | }
19 |
20 | // Arrays and strings.
21 | if (typeof value.length !== 'undefined' && value.length === 0) {
22 | return true;
23 | }
24 | },
25 | areValuesTheSame = (value1, value2) => {
26 | return JSON.stringify(value1) === JSON.stringify(value2);
27 | },
28 | noOp = () => { /* no-op */ },
29 | formatUsd = Intl
30 | ? new Intl.NumberFormat('en-US', {
31 | style: 'currency',
32 | currency: 'USD',
33 | minimumFractionDigits: 0
34 | }).format
35 | : (number) => number, // Fallback if Internationalization API isn't available.
36 | getUniqueId = (() => {
37 | let count = 0;
38 |
39 | return () => 'id' + (count += 1);
40 | })(),
41 | immutableMapFromArray = (array = [], mapper, mapClass = Immutable.Map) => {
42 | return mapClass(array.map((item) => [
43 | item.id,
44 | typeof mapper === 'function' ? mapper(item) : item
45 | ]));
46 | },
47 | immutableOrderedMapFromArray = (array, mapper) => {
48 | return immutableMapFromArray(array, mapper, Immutable.OrderedMap);
49 | };
50 |
--------------------------------------------------------------------------------
/src/views/components/AccessControlCard.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-flow: column nowrap;
4 | justify-content: space-around;
5 | align-items: center;
6 | height: 100%;
7 | min-height: 128px;
8 | }
9 |
10 | .sensorValue {
11 | margin: 0px 8px 8px 8px;
12 | font-size: 13px;
13 | color: #c8c8c8;
14 | }
15 |
16 | .sensorValues {
17 | display: flex;
18 | flex-flow: row nowrap;
19 | align-items: center;
20 | justify-content: center;
21 | flex: 1 0;
22 | }
23 |
24 | .sensorTitle {
25 | display: flex;
26 | flex-flow: column nowrap;
27 | align-items: center;
28 | justify-content: center;
29 | flex: 1 0;
30 | /* margin: 24px 0; */
31 | /* padding: 0 8px; */
32 | text-align: center;
33 | /* display: block; */
34 | font-size: 24px;
35 | color: #969696;
36 | }
37 |
38 | .sensorPanelA {
39 | display: flex;
40 | flex-flow: column nowrap;
41 | align-items: center;
42 | justify-content: center;
43 | flex: 1 0;
44 | padding: 12px 12px 12px 12px;
45 | /* margin: 24px 0; */
46 | /* padding: 0 16px; */
47 | text-align: center;
48 | /* align-items: center; */
49 | border-bottom: 1px solid #7e7e7e;
50 | }
51 |
52 | .sensorPanelB {
53 | display: flex;
54 | flex-flow: column nowrap;
55 | align-items: center;
56 | justify-content: center;
57 | flex: 1 0;
58 | padding: 12px 12px 12px 12px;
59 | /* margin: 24px 0; */
60 | /* padding: 0 16px; */
61 | text-align: center;
62 | /* align-items: center; */
63 | }
64 |
--------------------------------------------------------------------------------
/src/views/components/AccessControlCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import {compose} from 'redux';
5 | import {withRouter} from 'react-router-dom';
6 | import {doServiceAction} from '../../state/ducks/services-list/operations.js';
7 | import ServiceCardBase from './ServiceCardBase.js';
8 | import Button from './Button.js';
9 | import styles from './AccessControlCard.css';
10 |
11 | export class AccessControlCard extends React.Component {
12 | constructor (props) {
13 | super(props);
14 |
15 | this.state = {
16 | is_changing: false
17 | };
18 | }
19 |
20 | pulse () {
21 | if (!this.props.service.state.get('connected')) return;
22 |
23 | this.props.doAction(this.props.service.id, {
24 | property: 'pulseLock',
25 | value: true
26 | });
27 | }
28 |
29 | render() {
30 | const isConnected = this.props.service.state.get('connected');
31 |
32 | return (
33 | Access Log}
39 | onCardClick={() => {}}
40 | {...this.props}>
41 |
42 |
43 |
46 |
47 |
48 |
49 | );
50 | }
51 | }
52 |
53 | AccessControlCard.propTypes = {
54 | match: PropTypes.object,
55 | service: PropTypes.object,
56 | doAction: PropTypes.func
57 | };
58 |
59 | const mergeProps = (stateProps, {dispatch}, ownProps) => ({
60 | ...ownProps,
61 | ...stateProps,
62 | doAction: (serviceId, action) => dispatch(doServiceAction(serviceId, action))
63 | });
64 |
65 | export default compose(
66 | connect(null, null, mergeProps),
67 | withRouter
68 | )(AccessControlCard);
69 |
--------------------------------------------------------------------------------
/src/views/components/Actions.css:
--------------------------------------------------------------------------------
1 | .actions {
2 | display: flex;
3 | flex-flow: column nowrap;
4 | align-items: flex-end;
5 | margin: 8px 0;
6 | }
7 |
--------------------------------------------------------------------------------
/src/views/components/Actions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styles from './Actions.css';
4 |
5 | export const Actions = (props) => {
6 | return (
7 |
8 | {props.children}
9 |
10 | );
11 | };
12 |
13 | Actions.propTypes = {
14 | children: PropTypes.node
15 | };
16 |
17 | export default Actions;
18 |
--------------------------------------------------------------------------------
/src/views/components/AlarmCardGlobal.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-flow: column nowrap;
4 | justify-content: space-around;
5 | align-items: center;
6 | height: 100%;
7 | min-height: 128px;
8 | padding: 24px 16px;
9 | }
10 |
--------------------------------------------------------------------------------
/src/views/components/AlarmCardGlobal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import {doServiceAction} from '../../state/ducks/services-list/operations.js';
5 | import ServiceCardBase from './ServiceCardBase.js';
6 | import Switch from './Switch.js';
7 | import styles from './AlarmCardGlobal.css';
8 |
9 | export class GlobalAlarmCard extends React.Component {
10 | constructor (props) {
11 | super(props);
12 |
13 | this.toggleMode = this.toggleMode.bind(this);
14 | }
15 |
16 | toggleMode () {
17 | this.props.doAction(this.props.service.id, {
18 | property: 'mode',
19 | value: this.props.service.state.get('mode') > 0 ? 0 : 1
20 | });
21 | }
22 |
23 | render () {
24 | return (
25 | 0
28 | ? 'Armed'
29 | : 'Disarmed'}
30 | isConnected={true}
31 | onCardClick={this.toggleMode}
32 | {...this.props}>
33 |
34 | 0}
36 | showLabels={true}
37 | onLabel="Armed"
38 | offLabel="Disarmed" />
39 |
40 |
41 | );
42 | }
43 | }
44 |
45 | GlobalAlarmCard.propTypes = {
46 | service: PropTypes.object,
47 | doAction: PropTypes.func
48 | };
49 |
50 | const mergeProps = (stateProps, {dispatch}, ownProps) => ({
51 | ...ownProps,
52 | ...stateProps,
53 | doAction: (serviceId, action) => dispatch(doServiceAction(serviceId, action))
54 | });
55 |
56 | export default connect(null, null, mergeProps)(GlobalAlarmCard);
57 |
--------------------------------------------------------------------------------
/src/views/components/AppToolbar.css:
--------------------------------------------------------------------------------
1 | @value toolbarBackground from "../styles/colors.css";
2 | @value toolbarHeight: 56px;
3 |
4 | .toolbar {
5 | display: flex;
6 | flex-flow: column nowrap;
7 | flex: 1 1 auto;
8 | height: toolbarHeight;
9 | padding: 0 8px;
10 | background-color: toolbarBackground;
11 | }
12 |
13 | .logo {
14 | display: flex;
15 | flex-flow: column nowrap;
16 | justify-content: center;
17 | align-items: flex-start;
18 | max-width: 150px;
19 | height: 100%;
20 | padding: 5px 0;
21 | }
22 |
23 | .armedButton {
24 | display: inline-flex;
25 | padding: 0 8px;
26 | color: #888888;
27 | cursor: pointer;
28 | }
29 | .armedButton.isArmed {
30 | color: #eac13d;
31 | }
32 |
33 | .userButton {
34 | display: inline-flex;
35 | padding: 0 8px;
36 | color: inherit;
37 | cursor: pointer;
38 | }
39 |
40 | .user {
41 | display: flex;
42 | flex-flow: row nowrap;
43 | align-items: center;
44 | padding: 0 16px 16px 16px;
45 | border-bottom: 1px solid #7e7e7e;
46 | }
47 | .username {
48 | flex: 1 0 auto;
49 | font-size: 0.875em;
50 | }
51 |
52 | .security {
53 | padding: 16px;
54 | }
55 | .securityTitle {
56 | margin: 8px 0;
57 | font-size: 0.875em;
58 | }
59 |
--------------------------------------------------------------------------------
/src/views/components/ArmMenu.css:
--------------------------------------------------------------------------------
1 | .list {
2 | display: flex;
3 | flex-flow: row nowrap;
4 | justify-content: space-between;
5 | max-width: 300px;
6 | margin: 24px 0 16px 0;
7 | }
8 |
9 | .option {
10 | color: #bebebe;
11 | }
12 | .option.active {
13 | color: #ffffff;
14 | }
15 |
--------------------------------------------------------------------------------
/src/views/components/ArmMenu.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from '../components/Button.js';
4 | import ShieldIcon from '../icons/ShieldIcon.js';
5 | import ShieldCrossedIcon from '../icons/ShieldCrossedIcon.js';
6 | import styles from './ArmMenu.css';
7 |
8 | const DISARMED = 0,
9 | ARMED_STAY = 1,
10 | ARMED_AWAY = 2;
11 |
12 | export const ArmMenu = (props) => {
13 | const isArmedAway = props.mode === ARMED_AWAY,
14 | isArmedStay = props.mode === ARMED_STAY,
15 | isDisarmed = props.mode === DISARMED;
16 |
17 | return (
18 |
19 | -
20 | }
23 | onClick={() => props.setArmed(ARMED_AWAY)}>
24 | {isArmedAway || props.labelsAllOn ? 'Armed Away' : 'Arm Away'}
25 |
26 |
27 | -
28 | }
31 | onClick={() => props.setArmed(ARMED_STAY)}>
32 | {isArmedStay || props.labelsAllOn ? 'Armed Stay' : 'Arm Stay'}
33 |
34 |
35 | -
36 | }
39 | onClick={() => props.setArmed(DISARMED)}>
40 | {isDisarmed || props.labelsAllOn ? 'Disarmed' : 'Disarm'}
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | ArmMenu.propTypes = {
48 | mode: PropTypes.number,
49 | labelsAllOn: PropTypes.bool,
50 | setArmed: PropTypes.func.isRequired
51 | };
52 |
53 | export default ArmMenu;
54 |
--------------------------------------------------------------------------------
/src/views/components/AudioPlayer.css:
--------------------------------------------------------------------------------
1 | @value toolbarHeight from "./AppToolbar.css";
2 | @value tabBarHeight from "../layouts/App.css";
3 | @value liveRed: #bd2100;
4 |
5 | .canvas {
6 | composes: fillContext from "../styles/helpers.css";
7 | display: none;
8 | width: 100%;
9 | }
10 |
--------------------------------------------------------------------------------
/src/views/components/AudioStream.css:
--------------------------------------------------------------------------------
1 | /* A canvas for rendering streamed video. */
2 |
3 | .canvas {
4 | display: block;
5 | max-width: 100%;
6 | max-height: 100%;
7 | }
8 |
--------------------------------------------------------------------------------
/src/views/components/AutomationConditionArmedScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {withRoute} from './Route.js';
4 | import NavigationScreen from './NavigationScreen.js';
5 | import SettingsScreenContainer from './SettingsScreenContainer.js';
6 | import ArmMenu from './ArmMenu.js';
7 | import Button from './Button.js';
8 |
9 | export class AutomationConditionArmedScreen extends React.Component {
10 | constructor (props) {
11 | super(props);
12 |
13 | this.handleModeClick = this.handleModeClick.bind(this);
14 | this.handleDeleteClick = this.handleDeleteClick.bind(this);
15 | }
16 |
17 | handleModeClick (mode) {
18 | this.props.saveCondition(
19 | {
20 | type: 'armed',
21 | mode
22 | },
23 | Number.parseInt(this.props.match.params.conditionIndex)
24 | );
25 | }
26 |
27 | handleDeleteClick () {
28 | this.props.deleteCondition(Number.parseInt(this.props.match.params.conditionIndex));
29 | }
30 |
31 | render () {
32 | const condition = this.props.conditions && this.props.conditions.get(Number.parseInt(this.props.match.params.conditionIndex)),
33 | armedMode = condition ? condition.mode : null;
34 |
35 | return (
36 | Delete}
40 | toolbarBackAction={{label: 'Back'}}>
41 |
42 | Choose the armed status that must be active for this automation to run.
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | AutomationConditionArmedScreen.propTypes = {
51 | conditions: PropTypes.object,
52 | conditionIndex: PropTypes.number,
53 | isNew: PropTypes.bool,
54 | saveCondition: PropTypes.func.isRequired,
55 | deleteCondition: PropTypes.func,
56 | match: PropTypes.object.isRequired
57 | };
58 |
59 | export default withRoute({params: '/:conditionIndex?'})(AutomationConditionArmedScreen);
60 |
--------------------------------------------------------------------------------
/src/views/components/AutomationEditAction copy.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import {withRouter} from 'react-router-dom';
5 | import AutomationChooseServiceActionScreen from './AutomationChooseServiceActionScreen.js';
6 | import AutomationNotificationScreen from './AutomationNotificationScreen.js';
7 | import ChooseDeviceScreen from './ChooseDeviceScreen.js';
8 | import Button from './Button.js';
9 | import {getDevicesWithAutomatorSupport} from '../../state/ducks/devices-list/selectors.js';
10 |
11 | export const AutomationEditAction = (props) => {
12 | return (
13 |
14 | {props.isNew && Choose Device Action.}
24 | devices={props.devices}
25 | blankstateBody={'There are no devices that can trigger automations.'} />}
26 |
33 |
34 |
Send a !! Notification
35 |
36 |
37 |
43 |
44 |
45 | );
46 | };
47 |
48 | AutomationEditAction.propTypes = {
49 | isNew: PropTypes.bool,
50 | actions: PropTypes.object,
51 | devices: PropTypes.array.isRequired,
52 | saveAction: PropTypes.func.isRequired,
53 | deleteAction: PropTypes.func,
54 | match: PropTypes.object.isRequired
55 | };
56 |
57 | const mapStateToProps = ({devicesList}) => ({
58 | devices: getDevicesWithAutomatorSupport(devicesList)
59 | });
60 |
61 | export default withRouter(connect(mapStateToProps)(AutomationEditAction));
62 |
--------------------------------------------------------------------------------
/src/views/components/AutomationEditCondition.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {withRouter} from 'react-router-dom';
4 | import NavigationScreen from './NavigationScreen.js';
5 | import SettingsScreenContainer from './SettingsScreenContainer.js';
6 | import AutomationConditionArmedScreen from './AutomationConditionArmedScreen.js';
7 | import Button from './Button.js';
8 |
9 | export const AutomationEditCondition = (props) => {
10 | return (
11 |
12 | {props.isNew &&
15 |
16 | Choose which type of condition to add.
17 |
18 |
19 | }
20 |
26 |
27 | );
28 | };
29 |
30 | AutomationEditCondition.propTypes = {
31 | isNew: PropTypes.bool,
32 | conditions: PropTypes.object,
33 | saveCondition: PropTypes.func.isRequired,
34 | deleteCondition: PropTypes.func,
35 | match: PropTypes.object.isRequired
36 | };
37 |
38 | export default withRouter(AutomationEditCondition);
39 |
--------------------------------------------------------------------------------
/src/views/components/AutomationEditTrigger.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import {withRouter} from 'react-router-dom';
5 | import ChooseDeviceScreen from './ChooseDeviceScreen.js';
6 | import AutomationChooseServiceTriggerScreen from './AutomationChooseServiceTriggerScreen.js';
7 | import {getDevicesWithAutomatorSupport} from '../../state/ducks/devices-list/selectors.js';
8 |
9 | export const AutomationEditTrigger = (props) => {
10 | return (
11 |
12 | {props.isNew && Choose which device should trigger this automation.}
16 | devices={props.devices}
17 | blankstateBody={'There are no devices that can trigger automations.'} />}
18 |
24 |
25 | );
26 | };
27 |
28 | AutomationEditTrigger.propTypes = {
29 | isNew: PropTypes.bool,
30 | triggers: PropTypes.object,
31 | devices: PropTypes.array.isRequired,
32 | saveTrigger: PropTypes.func.isRequired,
33 | deleteTrigger: PropTypes.func,
34 | match: PropTypes.object.isRequired
35 | };
36 |
37 | const mapStateToProps = ({devicesList}) => ({
38 | devices: getDevicesWithAutomatorSupport(devicesList)
39 | });
40 |
41 | export default withRouter(connect(mapStateToProps)(AutomationEditTrigger));
42 |
--------------------------------------------------------------------------------
/src/views/components/AutomationRoute.js:
--------------------------------------------------------------------------------
1 | // AutomationRoute.js
2 |
3 | import React from 'react';
4 | import PropTypes from 'prop-types';
5 | import { Switch } from 'react-router-dom';
6 | import AutomationNotificationScreen from './AutomationNotificationScreen.js';
7 | import { Route } from './Route.js';
8 |
9 | export const AutomationRoute = (props) => {
10 | const {match: {url}} = props;
11 |
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | AutomationRoute.propTypes = {
21 | match: PropTypes.object.isRequired
22 | };
23 |
--------------------------------------------------------------------------------
/src/views/components/AutomationsScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import {compose} from 'redux';
5 | import {Switch, Redirect} from 'react-router-dom';
6 | import {Route, withRoute} from './Route.js';
7 | import NavigationScreen from './NavigationScreen.js';
8 | import AutomationEditScreen from './AutomationEditScreen.js';
9 | import List from './List.js';
10 | import Button from './Button.js';
11 | import BlankState from './BlankState.js';
12 | import AddIcon from '../icons/AddIcon.js';
13 | import ShieldIcon from '../icons/ShieldIcon.js';
14 | import {getAutomations, hasInitialFetchCompleted} from '../../state/ducks/automations-list/selectors.js';
15 |
16 | export const AutomationsScreen = (props) => {
17 | return (
18 | }>
23 | {props.error && {props.error}
}
24 | {props.isLoading
25 | ? Loading
26 | :
27 | (
28 |
29 | {!props.automations.length &&
30 |
33 | }
34 |
35 | {props.automations.sort((automation) => automation.type === 'security' ? -1 : 1).map((automation) => ({
36 | key: automation.id,
37 | icon: automation.type === 'security' ? : ,
38 | label: automation.name || 'Automation',
39 | link: props.match.url + '/' + automation.id
40 | }))}
41 |
42 |
43 | )} />
44 |
45 |
46 | } />
47 | }
48 |
49 | );
50 | };
51 |
52 | AutomationsScreen.propTypes = {
53 | automations: PropTypes.array.isRequired,
54 | error: PropTypes.string,
55 | isLoading: PropTypes.bool,
56 | match: PropTypes.object.isRequired
57 | };
58 |
59 | const mapStateToProps = ({automationsList}) => ({
60 | automations: getAutomations(automationsList, false)
61 | .filter((automation) => automation.user_editable)
62 | .reverse()
63 | .toList()
64 | .toJS(),
65 | isLoading: !hasInitialFetchCompleted(automationsList)
66 | });
67 |
68 | export default compose(
69 | withRoute(),
70 | connect(mapStateToProps)
71 | )(AutomationsScreen);
72 |
--------------------------------------------------------------------------------
/src/views/components/BlankState.css:
--------------------------------------------------------------------------------
1 | .blankState {
2 | display: flex;
3 | flex-flow: column nowrap;
4 | justify-content: center;
5 | align-items: center;
6 | width: 100%;
7 | height: 100%;
8 | }
9 |
10 | .heading {
11 | font-size: 20px;
12 | }
13 |
14 | .body {}
15 |
--------------------------------------------------------------------------------
/src/views/components/BlankState.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styles from './BlankState.css';
4 |
5 | export const BlankState = (props) => (
6 |
7 | {props.heading}
8 | {props.body}
9 |
10 | );
11 |
12 | BlankState.propTypes = {
13 | heading: PropTypes.string,
14 | body: PropTypes.string
15 | };
16 |
17 | export default BlankState;
18 |
--------------------------------------------------------------------------------
/src/views/components/Button.css:
--------------------------------------------------------------------------------
1 | @value primary from "../styles/colors.css";
2 |
3 | .button {
4 | display: inline-flex;
5 | flex-flow: row nowrap;
6 | justify-content: center;
7 | align-items: center;
8 | position: relative;
9 | height: 36px;
10 | min-width: 64px;
11 | padding: 0 8px;
12 | vertical-align: middle;
13 | border-radius: 4px;
14 | overflow: hidden;
15 | font-size: 1rem;
16 | color: primary;
17 | text-decoration: none;
18 | cursor: pointer;
19 | }
20 | .button::before {
21 | content: "";
22 | display: block;
23 | position: absolute;
24 | width: 100%;
25 | height: 100%;
26 | background-color: currentColor;
27 | opacity: 0;
28 | transition: opacity 0.15s;
29 | }
30 | .button:hover::before {
31 | opacity: 0.04;
32 | }
33 | .button:disabled {
34 | background-color: transparent;
35 | color: rgba(255, 255, 255, 0.37);
36 | cursor: default;
37 | pointer-events: none;
38 | }
39 |
40 | .outlinedButton {
41 | composes: button;
42 | padding: 0 16px;
43 | box-shadow: inset 0 0 0 1px primary;
44 | }
45 | .outlinedButton:disabled {
46 | box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.37);
47 | }
48 |
49 | .filledButton {
50 | composes: button;
51 | padding: 0 16px;
52 | background-color: primary;
53 | color: #333333;
54 | }
55 | .filledButton::before {
56 | background-color: #000000;
57 | }
58 | .filledButton:hover::before {
59 | opacity: 0.08;
60 | }
61 | .filledButton:disabled {
62 | background-color: rgba(255, 255, 255, 0.12);
63 | color: rgba(255, 255, 255, 0.45);
64 | }
65 |
66 | .tabButton {
67 | composes: button;
68 | display: flex;
69 | flex-flow: column nowrap;
70 | height: auto;
71 | color: inherit;
72 | }
73 | .tabButton:focus {
74 | outline: none;
75 | color: primary;
76 | }
77 |
78 | .label {
79 | display: inline-flex;
80 | }
81 | .tabButton .label {
82 | margin: 5px 0;
83 | font-size: 12px;
84 | }
85 |
86 | .icon {
87 | display: flex;
88 | }
89 | .tabButton .icon {
90 | margin: 5px 0 3px 0;
91 | }
92 |
93 | .submit {
94 | display: none;
95 | }
96 |
--------------------------------------------------------------------------------
/src/views/components/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {Link} from 'react-router-dom';
4 | import styles from './Button.css';
5 |
6 | export class Button extends React.Component {
7 | constructor (props) {
8 | super(props);
9 |
10 | this.submitInput = React.createRef();
11 | }
12 |
13 | render () {
14 | const children = (
15 |
16 | {this.props.icon && {this.props.icon}}
17 | {this.props.children}
18 |
19 | );
20 |
21 | let className;
22 |
23 | switch (this.props.type) {
24 | case 'tab':
25 | className = 'tabButton';
26 | break;
27 | case 'filled':
28 | className = 'filledButton';
29 | break;
30 | case 'outlined':
31 | className = 'outlinedButton';
32 | break;
33 | case 'link':
34 | default:
35 | className = 'button';
36 | break;
37 | }
38 |
39 | if (this.props.to && !this.props.disabled) {
40 | return {children};
41 | }
42 |
43 | return [
44 | ,
55 | this.props.submitForm &&
56 | ];
57 | }
58 | }
59 |
60 | Button.propTypes = {
61 | to: PropTypes.string,
62 | type: PropTypes.string,
63 | submitForm: PropTypes.bool,
64 | onClick: PropTypes.func,
65 | icon: PropTypes.node,
66 | children: PropTypes.node.isRequired,
67 | disabled: PropTypes.bool
68 | };
69 |
70 | export default Button;
71 |
--------------------------------------------------------------------------------
/src/views/components/ButtonCard.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-flow: column nowrap;
4 | justify-content: space-around;
5 | align-items: center;
6 | height: 100%;
7 | min-height: 128px;
8 | padding: 24px 16px;
9 | }
10 |
11 | .sliderWrapper {
12 | align-self: stretch;
13 | }
14 |
--------------------------------------------------------------------------------
/src/views/components/DashboardScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import {compose} from 'redux';
5 | import {withRoute} from './Route.js';
6 | import NavigationScreen from './NavigationScreen.js';
7 | import ServiceCardGrid from './ServiceCardGrid.js';
8 | import BlankState from './BlankState.js';
9 | import {getServices} from '../../state/ducks/services-list/selectors.js';
10 |
11 | export const DashboardScreen = (props) => {
12 | return (
13 |
14 | {!props.services.size &&
15 |
18 | }
19 | service.settings.get('show_on_dashboard') === true)} />
20 |
21 | );
22 | };
23 |
24 | DashboardScreen.propTypes = {
25 | services: PropTypes.object.isRequired,
26 | match: PropTypes.object.isRequired
27 | };
28 |
29 | const mapStateToProps = ({servicesList}) => ({
30 | services: getServices(servicesList, false)
31 | });
32 |
33 | export default compose(
34 | withRoute(),
35 | connect(mapStateToProps)
36 | )(DashboardScreen);
37 |
--------------------------------------------------------------------------------
/src/views/components/DatePicker.css:
--------------------------------------------------------------------------------
1 | @value primary from "../styles/colors.css";
2 |
3 | .datePicker {
4 | display: flex;
5 | flex-flow: row nowrap;
6 | justify-content: center;
7 | position: relative;
8 | width: 100%;
9 | }
10 |
11 | .calendar {
12 | composes: fillContext from "../styles/helpers.css";
13 | display: flex;
14 | flex-flow: column nowrap;
15 | padding-bottom: 3%;
16 | text-align: center;
17 | }
18 | .cell {
19 | display: flex;
20 | flex-flow: column nowrap;
21 | justify-content: center;
22 | align-items: center;
23 | flex: 0 0 calc(100% / 7);
24 | }
25 |
26 | .monthHeader {
27 | display: flex;
28 | flex-flow: row nowrap;
29 | justify-content: space-between;
30 | align-items: center;
31 | flex: 1 0 40px;
32 | }
33 | .monthButton {
34 | composes: cell;
35 | width: auto;
36 | }
37 | .previousMonthButton { composes: monthButton; }
38 | .nextMonthButton { composes: monthButton; }
39 |
40 | .weekHeader {
41 | display: flex;
42 | flex-flow: row nowrap;
43 | flex: 1 1 24px;
44 | font-size: 12px;
45 | color: #aaaaaa;
46 | }
47 | .dayHeading {
48 | composes: cell;
49 | }
50 |
51 | .week {
52 | display: flex;
53 | flex-flow: row nowrap;
54 | flex: 1 1 24px;
55 | }
56 | .firstWeek {
57 | composes: week;
58 | justify-content: flex-end;
59 | }
60 |
61 | .day {
62 | composes: cell;
63 | position: relative;
64 | z-index: 1;
65 | }
66 | .dayLink {
67 | display: flex;
68 | flex-flow: column nowrap;
69 | justify-content: center;
70 | align-items: center;
71 | width: 100%;
72 | height: 100%;
73 | color: inherit;
74 | font-size: 14px;
75 | text-decoration: none;
76 | cursor: default;
77 | }
78 | .enabledDay {
79 | color: primary;
80 | cursor: pointer;
81 | }
82 | .selectedDayLink {
83 | composes: dayLink;
84 | color: #ffffff;
85 | }
86 | .selectedDayLink::before {
87 | content: "";
88 | display: block;
89 | width: 32px;
90 | height: 32px;
91 | position: absolute;
92 | z-index: -1;
93 | background-color: primary;
94 | border-radius: 100%;
95 | }
96 | @media (min-width: 400px) and (min-height: 400px) {
97 | .selectedDayLink::before {
98 | width: 40px;
99 | height: 40px;
100 | }
101 | }
102 |
103 | .aspectRatio {
104 | composes: aspectRatio43 from "../styles/helpers.css";
105 | }
106 |
--------------------------------------------------------------------------------
/src/views/components/DeviceDetailsScreen.css:
--------------------------------------------------------------------------------
1 | .settingsHeading {
2 | margin: 32px 16px 24px 16px;
3 | font-size: 14px;
4 | line-height: 24px;
5 | }
6 |
7 | .infoHeading {
8 | margin: 32px 16px 24px 16px;
9 | font-size: 14px;
10 | line-height: 24px;
11 | }
12 |
--------------------------------------------------------------------------------
/src/views/components/DeviceLogScreen.css:
--------------------------------------------------------------------------------
1 | .table {
2 | width: 100%;
3 | margin-bottom: 16px;
4 | font-size: 14px;
5 | }
6 |
7 | .row {
8 | height: 48px;
9 | border-bottom: 1px solid #888888;
10 | }
11 | .headerRow {
12 | composes: row;
13 | font-size: 12px;
14 | font-weight: 500;
15 | color: #aaaaaa;
16 | text-align: left;
17 | }
18 |
19 | .cell {
20 | padding: 0 16px;
21 | }
22 | .headerCell {
23 | composes: cell;
24 | }
25 |
--------------------------------------------------------------------------------
/src/views/components/DeviceRoomField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import Form from './Form.js';
5 | import {getDeviceById} from '../../state/ducks/devices-list/selectors.js';
6 | import {setDeviceRoom} from '../../state/ducks/devices-list/operations.js';
7 | import {getRooms, hasInitialFetchCompleted} from '../../state/ducks/rooms-list/selectors.js';
8 |
9 | export class DeviceRoomField extends React.Component {
10 | constructor (props) {
11 | super(props);
12 |
13 | this.handleRoomChange = this.handleRoomChange.bind(this);
14 | }
15 |
16 | handleRoomChange ({room}) {
17 | this.props.saveRoom(this.props.device.id, room, this.props.device.room_id);
18 | }
19 |
20 | getRoomValue () {
21 | if (this.props.areRoomsLoading || !this.props.rooms.find((room) => room.id === this.props.device.room_id)) {
22 | return '';
23 | }
24 |
25 | return this.props.device.room_id;
26 | }
27 |
28 | render () {
29 | return (
30 |
80 | );
81 | }
82 | }
83 |
84 | LoginForm.propTypes = {
85 | login: PropTypes.func
86 | };
87 |
88 | const mapDispatchToProps = (dispatch) => ({
89 | login: (username, password) => {
90 | dispatch(session.operations.login(username, password));
91 | }
92 | });
93 |
94 | export default connect(null, mapDispatchToProps)(LoginForm);
95 |
--------------------------------------------------------------------------------
/src/views/components/Logout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {Redirect} from 'react-router-dom';
4 | import {connect} from 'react-redux';
5 | import {logout} from '../../state/ducks/session/operations.js';
6 |
7 | export class Logout extends React.Component {
8 | constructor (props) {
9 | super(props);
10 | props.logout();
11 | }
12 |
13 | render () {
14 | return ;
15 | }
16 | }
17 |
18 | Logout.propTypes = {
19 | logout: PropTypes.func
20 | };
21 |
22 | const mapDispatchToProps = (dispatch) => ({
23 | logout: () => dispatch(logout())
24 | });
25 |
26 | export default connect(null, mapDispatchToProps)(Logout);
27 |
--------------------------------------------------------------------------------
/src/views/components/MetaList.css:
--------------------------------------------------------------------------------
1 | .list {
2 | display: grid;
3 | width: 100%;
4 | margin: 16px 0 0 0;
5 | }
6 | .list-horizontal {
7 | composes: list;
8 | grid-template-columns: repeat(auto-fit, minmax(calc(100% / 3), 1fr));
9 | }
10 | .list-vertical {
11 | composes: list;
12 | grid-template-columns: max-content;
13 | grid-auto-flow: row;
14 | padding: 0 16px;
15 | }
16 |
17 | .item {
18 | display: flex;
19 | flex-flow: column nowrap;
20 | margin: 0 0 16px 0;
21 | }
22 | .item-long {
23 | composes: item;
24 | grid-column: 1 / -1;
25 | }
26 | .list-horizontal > .item {
27 | padding: 0 16px;
28 | }
29 | .list-vertical > .item {
30 | grid-column: 1;
31 | }
32 |
33 | .label {
34 | font-size: 13px;
35 | line-height: 18px;
36 | color: #aaaaaa;
37 | }
38 | .label-top {
39 | composes: label;
40 | }
41 | .label-left {
42 | composes: label;
43 | margin: 0 0 16px 0;
44 | }
45 |
46 | .value {
47 | composes: selectable from "../styles/helpers.css";
48 | margin: 0;
49 | padding-left: 24px;
50 | font-size: 14px;
51 | line-height: 18px;
52 | }
53 | .value-right {
54 | composes: value;
55 | padding-left: 8px;
56 | text-align: right;
57 | }
58 | .label-left + .value {
59 | grid-column: 2;
60 | margin: 0 0 16px 0;
61 | }
62 |
--------------------------------------------------------------------------------
/src/views/components/MetaList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styles from './MetaList.css';
4 |
5 | export const MetaList = (props) => {
6 | const areLabelsLeft = props.alignLabels === 'left',
7 | layout = areLabelsLeft ? 'vertical' : props.layout,
8 | ItemElement = !areLabelsLeft ? 'div' : React.Fragment,
9 | getItemElementProps = (item) => {
10 | return ItemElement !== React.Fragment
11 | ? {className: item.long ? styles['item-long'] : styles.item}
12 | : {};
13 | };
14 |
15 | return (
16 |
17 | {props.children.map((item, index) => (
18 |
19 | - {item.label}
20 | - {item.value}
21 |
22 | ))}
23 |
24 | );
25 | };
26 |
27 | MetaList.defaultProps = {
28 | children: [],
29 | layout: 'horizontal',
30 | alignLabels: 'top'
31 | };
32 |
33 | MetaList.propTypes = {
34 | children: PropTypes.arrayOf(PropTypes.shape({
35 | label: PropTypes.node,
36 | value: PropTypes.node
37 | })),
38 | layout: PropTypes.oneOf(['horizontal', 'vertical']),
39 | alignLabels: PropTypes.oneOf(['top', 'left']),
40 | alignValuesRight: PropTypes.bool
41 | };
42 |
43 | export default MetaList;
44 |
--------------------------------------------------------------------------------
/src/views/components/ModalShelf.css:
--------------------------------------------------------------------------------
1 | @value background from "../styles/colors.css";
2 |
3 | .modal {
4 | width: 300px;
5 | background-color: background;
6 | box-shadow: 0 0 24px rgba(0, 0, 0, 0.32);
7 | position: absolute;
8 | top: 0;
9 | right: 0;
10 | bottom: 0;
11 | z-index: 1001;
12 | }
13 |
14 | .header {
15 | display: flex;
16 | flex-flow: row nowrap;
17 | justify-content: flex-end;
18 | align-items: center;
19 | height: 56px;
20 | padding: 0 16px;
21 | }
22 |
23 | .scrim {
24 | background-color: rgba(0, 0, 0, 0.5);
25 | position: absolute;
26 | top: 0;
27 | right: 0;
28 | bottom: 0;
29 | left: 0;
30 | z-index: 1000;
31 | }
32 |
--------------------------------------------------------------------------------
/src/views/components/ModalShelf.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import PropTypes from 'prop-types';
4 | import Button from './Button.js';
5 | import styles from './ModalShelf.css';
6 |
7 | export const ModalShelf = (props) => ReactDOM.createPortal(
8 |
9 |
10 |
11 |
12 |
13 | {props.children}
14 |
15 | , document.body);
16 |
17 | ModalShelf.propTypes = {
18 | children: PropTypes.node,
19 | hide: PropTypes.func
20 | };
21 |
22 | export default ModalShelf;
23 |
--------------------------------------------------------------------------------
/src/views/components/NavigationScreen.css:
--------------------------------------------------------------------------------
1 | .screen {
2 | width: 100%;
3 | height: 100%;
4 | position: absolute;
5 | top: 0;
6 | left: 0;
7 | overflow-x: hidden;
8 | overflow-y: auto;
9 | z-index: 1;
10 | background-color: #555555;
11 | }
12 |
--------------------------------------------------------------------------------
/src/views/components/PercentageField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import SliderControl from './SliderControl.js';
4 |
5 | export const PercentageField = (props) => {
6 | return (
7 |
8 |
9 | {
13 | if (typeof props.onChange !== 'function') {
14 | return;
15 | }
16 |
17 | props.onChange({
18 | type: 'change',
19 | target: {
20 | name: props.name,
21 | value: Math.round(value) / 100
22 | }
23 | });
24 | }}
25 | disabled={props.disabled} />
26 |
27 | );
28 | };
29 |
30 | PercentageField.propTypes = {
31 | name: PropTypes.string.isRequired,
32 | label: PropTypes.string,
33 | value: PropTypes.number,
34 | disabled: PropTypes.bool,
35 | onChange: PropTypes.func
36 | };
37 |
38 | export default PercentageField;
39 |
--------------------------------------------------------------------------------
/src/views/components/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {Redirect} from 'react-router-dom';
4 | import Route from './Route.js';
5 | import {connect} from 'react-redux';
6 | import {isAuthenticated} from '../../state/ducks/session/selectors.js';
7 |
8 | /**
9 | * Higher order component for Route. Implements a private route that will only
10 | * be rendered if the user is logged in.
11 | */
12 |
13 | export class PrivateRoute extends React.Component {
14 | render () {
15 | const {isLoggedIn, component, children, ...rest} = this.props;
16 |
17 | return (
18 | {
19 | if (!isLoggedIn) {
20 | return (
21 |
25 | );
26 | }
27 |
28 | return ;
29 | }} />
30 | );
31 | }
32 | }
33 |
34 | PrivateRoute.propTypes = {
35 | ...Route.propTypes,
36 | isLoggedIn: PropTypes.bool
37 | };
38 |
39 | const mapStateToProps = (state) => ({
40 | isLoggedIn: isAuthenticated(state.session)
41 | });
42 |
43 | export default connect(
44 | mapStateToProps,
45 | null,
46 | null,
47 |
48 | /* Need to set pure to false so connect doesn't implement
49 | shouldComponentUpdate. Otherwise this component will only update on
50 | state/props change. That's desired for most components, but not this
51 | since react-router uses react context to communicate route changes. */
52 | {pure: false}
53 | )(PrivateRoute);
54 |
--------------------------------------------------------------------------------
/src/views/components/PureComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | /**
5 | * A generic wrapper component that implements React PureComponent.
6 | */
7 |
8 | export class PureComponent extends React.PureComponent {
9 | render () {
10 | return this.props.children;
11 | }
12 | }
13 |
14 | PureComponent.propTypes = {
15 | children: PropTypes.node
16 | };
17 |
18 | export default PureComponent;
19 |
--------------------------------------------------------------------------------
/src/views/components/RangeControl.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {Slider} from '@mui/material';
4 |
5 | export class RangeControl extends React.Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | value: props.value || [0, 100],
11 | is_changing: false
12 | };
13 | }
14 |
15 | handleInput(event, newValue) {
16 | this.setState({value: newValue});
17 |
18 | if (typeof this.props.onInput === 'function') {
19 | this.props.onInput(newValue);
20 | }
21 | }
22 |
23 | handleChange(event, newValue) {
24 | this.setState({
25 | value: newValue,
26 | is_changing: false
27 | });
28 |
29 | if (typeof this.props.onChange === 'function') {
30 | this.props.onChange(newValue);
31 | }
32 | }
33 |
34 | render() {
35 | return (
36 |
46 | );
47 | }
48 | }
49 |
50 | RangeControl.propTypes = {
51 | value: PropTypes.array,
52 | onChange: PropTypes.func,
53 | onInput: PropTypes.func,
54 | disabled: PropTypes.bool,
55 | minRange: PropTypes.number,
56 | maxRange: PropTypes.number
57 | };
58 |
59 | RangeControl.defaultProps = {
60 | value: [0, 100]
61 | };
62 |
63 | export default RangeControl;
64 |
--------------------------------------------------------------------------------
/src/views/components/Route.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {Route as ReactRouterRoute, matchPath, withRouter} from 'react-router-dom';
4 |
5 | /**
6 | * Higher order component for Route. Adds the base url without optional
7 | * parameters to the match object.
8 | */
9 |
10 | export const Route = (props) => {
11 | const route = (
12 | (
13 |
22 | )} />
23 | );
24 |
25 | return route;
26 | };
27 |
28 | Route.propTypes = {...Route.propTypes};
29 |
30 | // Gets the matched URL and removes any optional segments at the end.
31 | function getUrlWithoutParams (url = '', path = '', props, includeRequired) {
32 | const pattern = new RegExp(
33 | includeRequired
34 | ? /(\/:[^/?*]+[?*])*$/
35 | : /(\/:[^/?*]+)*$/,
36 | props.sensitive
37 | ? ''
38 | : 'i'
39 | ), // Match all optional parameters at the end of the path.
40 | modifiedPath = path.replace(pattern, ''),
41 | modifiedMatch = matchPath(url, {...props, path: modifiedPath});
42 |
43 | return modifiedMatch ? modifiedMatch.url : url;
44 | }
45 |
46 | export const withRoute = (routeProps = {}) => (WrappedComponent) => {
47 | const RouteHOC = (props) => {
48 | const _props = {...props};
49 |
50 | // Prevent Route component from using computedMatch from a Switch
51 | // component so we can add the route parameters.
52 | delete _props.computedMatch;
53 |
54 | return (
55 | (
56 |
57 | )} />
58 | );
59 | };
60 |
61 | RouteHOC.propTypes = {
62 | path: PropTypes.string,
63 | match: PropTypes.object
64 | };
65 |
66 | return withRouter(RouteHOC);
67 | };
68 |
69 | export default withRouter(Route);
70 |
--------------------------------------------------------------------------------
/src/views/components/ScaleCard.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-flow: column nowrap;
4 | justify-content: space-around;
5 | align-items: center;
6 | height: 100%;
7 | min-height: 128px;
8 | }
9 |
10 | .sensorValue {
11 | margin: 0px 8px 8px 8px;
12 | font-size: 13px;
13 | color: #c8c8c8;
14 | }
15 |
16 | .sensorValues {
17 | display: flex;
18 | flex-flow: row nowrap;
19 | align-items: center;
20 | justify-content: center;
21 | flex: 1 0;
22 | }
23 |
24 | .sensorTitle {
25 | display: flex;
26 | flex-flow: column nowrap;
27 | align-items: center;
28 | justify-content: center;
29 | flex: 1 0;
30 | /* margin: 24px 0; */
31 | /* padding: 0 8px; */
32 | text-align: center;
33 | /* display: block; */
34 | font-size: 54px;
35 | color: #969696;
36 | }
37 |
38 | .sensorPanelA {
39 | display: flex;
40 | flex-flow: column nowrap;
41 | align-items: center;
42 | justify-content: center;
43 | flex: 1 0;
44 | padding: 12px 12px 12px 12px;
45 | /* margin: 24px 0; */
46 | /* padding: 0 16px; */
47 | text-align: center;
48 | /* align-items: center; */
49 | border-bottom: 1px solid #7e7e7e;
50 | }
51 |
52 | .sensorPanelB {
53 | display: flex;
54 | flex-flow: column nowrap;
55 | align-items: center;
56 | justify-content: center;
57 | flex: 1 0;
58 | padding: 12px 12px 12px 12px;
59 | /* margin: 24px 0; */
60 | /* padding: 0 16px; */
61 | text-align: center;
62 | /* align-items: center; */
63 | }
64 |
--------------------------------------------------------------------------------
/src/views/components/SelectField.css:
--------------------------------------------------------------------------------
1 | .select,
2 | .input {
3 | width: 100%;
4 | height: 100%;
5 | position: absolute;
6 | top: 0;
7 | left: 0;
8 | }
9 | .isDisabled {
10 | cursor: not-allowed;
11 | }
12 |
13 | .input {
14 | opacity: 0;
15 | z-index: 1;
16 | cursor: pointer;
17 | color: initial;
18 | }
19 | .isDisabled > .input {
20 | cursor: inherit;
21 | }
22 |
23 | .icon {
24 | display: flex;
25 | position: absolute;
26 | top: calc(50% - 5px);
27 | right: 17px;
28 | }
29 |
--------------------------------------------------------------------------------
/src/views/components/SelectField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import TextField from './TextField.js';
4 | import MenuIndicatorIcon from '../icons/MenuIndicatorIcon.js';
5 | import {getUniqueId} from '../../utilities.js';
6 | import styles from './SelectField.css';
7 |
8 | export class SelectField extends React.Component {
9 | constructor (props) {
10 | super(props);
11 |
12 | this.inputId = getUniqueId();
13 | }
14 |
15 | render () {
16 | const {label, name, options, ...inputProps} = this.props,
17 | currentOption = options.find((option) => option.value === inputProps.value);
18 |
19 | return (
20 |
26 |
27 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | SelectField.propTypes = {
51 | label: PropTypes.string,
52 | name: PropTypes.string,
53 | value: PropTypes.oneOfType([
54 | PropTypes.string,
55 | PropTypes.number
56 | ]),
57 | disabled: PropTypes.bool,
58 | options: PropTypes.array
59 | };
60 |
61 | export default SelectField;
62 |
--------------------------------------------------------------------------------
/src/views/components/ServiceCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import CameraCard from './CameraCard.js';
4 | import DimmerCard from './DimmerCard.js';
5 | import GrowPodCard from './GrowPodCard.js';
6 | import AccessControlCard from './AccessControlCard.js';
7 | import LightCard from './LightCard.js';
8 | import ScaleCard from './ScaleCard.js';
9 | import ButtonCard from './ButtonCard.js';
10 | import GlobalAlarmCard from './AlarmCardGlobal.js';
11 | import GameMachineCard from './GameMachineCard.js';
12 | import LockCard from './LockCard.js';
13 | import ThermostatCard from './ThermostatCard.js';
14 | import MediaCard from './MediaCard.js';
15 |
16 | export const ServiceCard = (props) => {
17 | const Card = ServiceCard.cardComponents[props.service.type];
18 |
19 | return Card && ;
20 | };
21 |
22 | ServiceCard.willCardRender = ({type}) => Boolean(ServiceCard.cardComponents[type]);
23 |
24 | ServiceCard.propTypes = {
25 | service: PropTypes.object
26 | };
27 |
28 | ServiceCard.cardComponents = {
29 | 'camera': CameraCard,
30 | 'network-camera': CameraCard,
31 | 'dimmer': DimmerCard,
32 | 'grow-pod': GrowPodCard,
33 | 'access-control': AccessControlCard,
34 | 'light': LightCard,
35 | 'scale': ScaleCard,
36 | 'global-alarm': GlobalAlarmCard,
37 | 'button': ButtonCard,
38 | 'game-machine': GameMachineCard,
39 | 'lock': LockCard,
40 | 'thermostat': ThermostatCard,
41 | 'media': MediaCard
42 | };
43 |
44 | export default ServiceCard;
45 |
--------------------------------------------------------------------------------
/src/views/components/ServiceCardBase.css:
--------------------------------------------------------------------------------
1 | @value topBarHeight: 72px;
2 | @value bottomBarHeight: 48px;
3 |
4 | .card {
5 | display: flex;
6 | flex-flow: column nowrap;
7 | width: 100%;
8 | flex: 1 1 auto;
9 | background-color: #444444;
10 | border: 1px solid #888888;
11 | border-radius: 4px;
12 | overflow: hidden;
13 | cursor: pointer;
14 | }
15 |
16 | .topBar {
17 | display: flex;
18 | flex-flow: row nowrap;
19 | height: topBarHeight;
20 | padding: 0 16px;
21 | position: relative;
22 | z-index: 10;
23 | opacity: 1;
24 | background-color: rgba(56, 56, 56, 0.9);
25 | font-size: 14px;
26 | transition: opacity 0.2s;
27 | }
28 | .topBarHidden {
29 | composes: topBar;
30 | opacity: 0;
31 | pointer-events: none;
32 | }
33 |
34 | .bottomBar {
35 | composes: topBar;
36 | height: bottomBarHeight;
37 | padding: 0 8px;
38 | }
39 | .bottomBarHidden {
40 | composes: bottomBar;
41 | composes: topBarHidden;
42 | }
43 |
44 | .topBarNone {
45 | composes: topBar;
46 | display: none;
47 | pointer-events: none;
48 | }
49 |
50 | .bottomBarNone {
51 | composes: bottomBar;
52 | composes: topBarHidden;
53 | display: none;
54 | }
55 |
56 | .content {
57 | flex: 1 1 auto;
58 | position: relative;
59 | }
60 |
61 | .contentBehindToolbars {
62 | composes: content;
63 | margin: calc(0px - topBarHeight) 0 calc(0px - bottomBarHeight) 0;
64 | }
65 |
--------------------------------------------------------------------------------
/src/views/components/ServiceCardBase.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {withRouter} from 'react-router-dom';
4 | import Toolbar from './Toolbar.js';
5 | import Button from './Button.js';
6 | import ServiceHeader from './ServiceHeader.js';
7 | import styles from './ServiceCardBase.css';
8 |
9 | export const ServiceCardBase = (props) => {
10 | const detailsPath = `${props.match.url}/service/${props.service.id}`;
11 |
12 | return (
13 | props.history.push(detailsPath))}>
14 |
15 |
20 |
21 |
22 | {props.children}
23 |
24 |
25 | event.stopPropagation()}>{props.secondaryAction}
}
27 | rightChildren={ event.stopPropagation()}>
28 |
29 |
} />
30 |
31 |
32 | );
33 | };
34 |
35 | ServiceCardBase.propTypes = {
36 | service: PropTypes.object,
37 | name: PropTypes.string,
38 | status: PropTypes.string,
39 | isConnected: PropTypes.bool,
40 | children: PropTypes.node,
41 | toolbarsOverlayContent: PropTypes.bool,
42 | secondaryAction: PropTypes.node,
43 | hideToolbars: PropTypes.bool,
44 | removePadding: PropTypes.bool,
45 | onCardClick: PropTypes.func,
46 | match: PropTypes.object,
47 | history: PropTypes.object
48 | };
49 |
50 | export default withRouter(ServiceCardBase);
51 |
--------------------------------------------------------------------------------
/src/views/components/ServiceCardGrid.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {Switch, Redirect, withRouter} from 'react-router-dom';
4 | import Route from './Route.js';
5 | import ServiceCard from './ServiceCard.js';
6 | import ServiceDetailsScreen from './ServiceDetailsScreen.js';
7 | import ServiceLogScreen from './ServiceLogScreen.js';
8 | import DeviceLogScreen from './DeviceLogScreen.js';
9 | import CameraRecordingsScreen from './CameraRecordingsScreen.js';
10 | import Grid from '../layouts/Grid.js';
11 | import GridColumn from '../layouts/GridColumn.js';
12 |
13 | export const ServiceCardGrid = (props) => {
14 | const serviceCards = props.services
15 | .filter(ServiceCard.willCardRender)
16 | .toArray()
17 | .map(([, service]) => );
18 |
19 | return (
20 |
21 | (
22 |
23 | {serviceCards.map((card, index) => (
24 | {card}
25 | ))}
26 |
27 | )} />
28 |
29 |
30 |
31 |
32 | } />
33 |
34 | );
35 | };
36 |
37 | ServiceCardGrid.propTypes = {
38 | services: PropTypes.object.isRequired,
39 | match: PropTypes.object
40 | };
41 |
42 | export default withRouter(ServiceCardGrid);
43 |
--------------------------------------------------------------------------------
/src/views/components/ServiceDetails.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {Switch, Redirect, withRouter} from 'react-router-dom';
4 | import {Route} from './Route.js';
5 | import Button from './Button.js';
6 | import SettingsScreenContainer from './SettingsScreenContainer.js';
7 | import ServiceSettingsScreen from './ServiceSettingsScreen.js';
8 | import styles from './ServiceDetails.css';
9 | import MetaList from './MetaList.js';
10 |
11 | export class ServiceDetails extends React.Component {
12 | getDevice (property) {
13 | return (
14 | this.props.service.state.get('device' + property) ? this.props.service.state.get('device' + property) : '...'
15 | );
16 | }
17 |
18 | render () {
19 | return (
20 |
21 | (
22 |
23 | {this.props.service.error && The device settings could not be updated because of an error.
}
24 |
25 | {this.props.shouldShowSettingsButton && }
26 |
27 | {this.props.children}
28 |
29 | {[].filter((item) => Boolean(item.value))}
30 |
31 |
32 | )} />
33 |
34 | } />
35 |
36 | );
37 | }
38 | }
39 |
40 | ServiceDetails.settingsPath = '/service-settings';
41 |
42 | ServiceDetails.propTypes = {
43 | service: PropTypes.object.isRequired,
44 | children: PropTypes.node,
45 | shouldShowSettingsButton: PropTypes.bool,
46 | shouldShowRoomField: PropTypes.bool,
47 | match: PropTypes.object
48 | };
49 |
50 | export default withRouter(ServiceDetails);
51 |
--------------------------------------------------------------------------------
/src/views/components/ServiceDetailsScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {Redirect} from 'react-router-dom';
4 | import {withRoute} from './Route.js';
5 | import NavigationScreen from './NavigationScreen.js';
6 | import ServiceDetails from './ServiceDetails.js';
7 | import ThermostatServiceDetails from './ThermostatServiceDetails.js';
8 | import MediaServiceDetails from './MediaServiceDetails.js';
9 | import LightServiceDetails from './LightServiceDetails.js';
10 | import GrowServiceDetails from './GrowServiceDetails.js';
11 | import AccessControlServiceDetails from './AccessControlServiceDetails.js';
12 | import Button from './Button.js';
13 | import {connect} from 'react-redux';
14 | import {compose} from 'redux';
15 | import {getServiceById} from '../../state/ducks/services-list/selectors.js';
16 |
17 | export const ServiceDetailsScreen = (props) => {
18 | const service = props.service;
19 |
20 | if (!service) {
21 | return ;
22 | }
23 |
24 | return (
25 | Settings}>
29 |
30 | {props.service.get('type') === 'thermostat' ? : ''}
31 | {props.service.get('type') === 'media' ? : ''}
32 | {props.service.get('type') === 'light' ? : ''}
33 | {props.service.get('type') === 'grow-pod' ? : ''}
34 | {props.service.get('type') === 'access-control' ? : ''}
35 |
36 |
39 |
40 | );
41 | };
42 |
43 | ServiceDetailsScreen.propTypes = {
44 | service: PropTypes.object,
45 | shouldShowRoomField: PropTypes.bool,
46 | match: PropTypes.object.isRequired
47 | };
48 |
49 | const mapStateToProps = ({servicesList}, {match}) => {
50 | const service = getServiceById(servicesList, match.params.serviceId, false);
51 |
52 | if (!service) {
53 | return {};
54 | }
55 |
56 | return {service};
57 | };
58 |
59 | export default compose(
60 | withRoute({params: '/:serviceId'}),
61 | connect(mapStateToProps)
62 | )(ServiceDetailsScreen);
63 |
--------------------------------------------------------------------------------
/src/views/components/ServiceHeader.css:
--------------------------------------------------------------------------------
1 | @value disconnectedRed: #d30808;
2 |
3 | .header {
4 | display: flex;
5 | flex-flow: row nowrap;
6 | height: 72px;
7 | font-size: 14px;
8 | }
9 | .icon {
10 | display: flex;
11 | flex-flow: row nowrap;
12 | justify-content: center;
13 | align-items: center;
14 | width: 56px;
15 | padding: 16px 16px 16px 0;
16 | position: relative;
17 | }
18 | .disconnectedIcon {
19 | composes: icon;
20 | }
21 | .disconnectedIcon::after {
22 | content: "!";
23 | display: table-cell;
24 | vertical-align: middle;
25 | box-sizing: border-box;
26 | width: 18px;
27 | height: 18px;
28 | position: absolute;
29 | right: 16px;
30 | bottom: 16px;
31 | border-radius: 100%;
32 | background-color: disconnectedRed;
33 | color: #ffffff;
34 | font-size: 15px;
35 | line-height: 20px;
36 | font-weight: bold;
37 | text-align: center;
38 | }
39 | .nameWrapper {
40 | display: flex;
41 | flex-flow: column nowrap;
42 | justify-content: center;
43 | }
44 | .name {
45 | display: flex;
46 | flex-flow: column nowrap;
47 | justify-content: center;
48 | min-height: 20px;
49 | }
50 | .status {
51 | display: flex;
52 | flex-flow: column nowrap;
53 | justify-content: center;
54 | min-height: 20px;
55 | color: #909090;
56 | }
57 | .disconnectedStatus {
58 | composes: status;
59 | color: disconnectedRed;
60 | }
61 |
--------------------------------------------------------------------------------
/src/views/components/ServiceHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import ServiceIcon from '../icons/ServiceIcon.js';
4 | import styles from './ServiceHeader.css';
5 |
6 | export const ServiceHeader = (props) => {
7 | const status = props.isConnected
8 | ? props.status
9 | : 'Not Responding';
10 |
11 | return (
12 |
13 | {ServiceIcon.willRenderIcon(props.service) &&
14 |
15 |
16 |
}
17 |
18 |
19 | {props.name || props.service.settings.get('name') || props.service.strings.get('friendly_type')}
20 |
21 | {status &&
22 |
23 | {status}
24 | }
25 |
26 |
27 | );
28 | };
29 |
30 | ServiceHeader.propTypes = {
31 | service: PropTypes.object,
32 | name: PropTypes.string,
33 | status: PropTypes.string,
34 | isConnected: PropTypes.bool
35 | };
36 |
37 | export default ServiceHeader;
38 |
--------------------------------------------------------------------------------
/src/views/components/ServiceLogScreen.css:
--------------------------------------------------------------------------------
1 | .table {
2 | width: 100%;
3 | margin-bottom: 16px;
4 | font-size: 14px;
5 | }
6 |
7 | .row {
8 | height: 48px;
9 | border-bottom: 1px solid #888888;
10 | }
11 | .headerRow {
12 | composes: row;
13 | font-size: 12px;
14 | font-weight: 500;
15 | color: #aaaaaa;
16 | text-align: left;
17 | }
18 |
19 | .cell {
20 | padding: 0 16px;
21 | }
22 | .headerCell {
23 | composes: cell;
24 | }
25 |
--------------------------------------------------------------------------------
/src/views/components/ServiceSettingsScreen.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | flex-flow: row nowrap;
4 | align-items: center;
5 | margin: 8px 16px;
6 | }
7 |
8 | .iconContainer {
9 | margin: 8px 0 16px 0;
10 | padding: 16px 16px 16px 0;
11 | }
12 |
13 | .nameContainer {
14 | flex: 1 0 auto;
15 | margin: -8px -16px;
16 | }
17 |
18 | .settingsHeading {
19 | margin: 32px 16px 24px 16px;
20 | font-size: 14px;
21 | line-height: 24px;
22 | }
23 |
--------------------------------------------------------------------------------
/src/views/components/SettingValue.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import moment from 'moment';
4 |
5 | export const SettingValue = (props) => {
6 | let formatted;
7 |
8 | switch (props.type) {
9 | case 'percentage':
10 | formatted = Math.round(props.children * 100) + '%';
11 | break;
12 | case 'time-of-day':
13 | formatted = moment.utc(props.children).format('h:mm A');
14 | break;
15 | default:
16 | formatted = props.children;
17 | break;
18 | }
19 |
20 | return {formatted};
21 | };
22 |
23 | SettingValue.propTypes = {
24 | type: PropTypes.string,
25 | children: PropTypes.any
26 | };
27 |
28 | export default SettingValue;
29 |
--------------------------------------------------------------------------------
/src/views/components/SettingsScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {Switch, Redirect} from 'react-router-dom';
4 | import {Route, withRoute} from './Route.js';
5 | import NavigationScreen from './NavigationScreen.js';
6 | import List from './List.js';
7 | import DevicesListScreen from './DevicesListScreen.js';
8 | import RoomsSettingsScreen from './RoomsSettingsScreen.js';
9 | import DoorIcon from '../icons/DoorIcon.js';
10 |
11 | export class SettingsScreen extends React.Component {
12 | render () {
13 | return (
14 |
15 |
16 | (
17 |
18 | {[
19 | {
20 | label: 'Devices',
21 | link: this.props.match.url + '/devices',
22 | icon: // TODO: Get rid of this hack.
23 | },
24 | {
25 | label: 'Rooms',
26 | link: this.props.match.url + '/rooms',
27 | icon:
28 | }
29 | ]}
30 |
31 | )} />
32 |
33 |
34 | } />
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | SettingsScreen.propTypes = {
42 | match: PropTypes.object.isRequired
43 | };
44 |
45 | export default withRoute()(SettingsScreen);
46 |
--------------------------------------------------------------------------------
/src/views/components/SettingsScreenContainer.css:
--------------------------------------------------------------------------------
1 | .container {
2 | max-width: 500px;
3 | }
4 | .containerWithPadding {
5 | composes: container;
6 | padding: 8px 16px;
7 | }
8 |
--------------------------------------------------------------------------------
/src/views/components/SettingsScreenContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styles from './SettingsScreenContainer.css';
4 |
5 | export const SettingsScreenContainer = ({section, children, withPadding}) => React.createElement(
6 | section ? 'section' : 'div',
7 | {
8 | className: withPadding ? styles.containerWithPadding : styles.container,
9 | children
10 | }
11 | );
12 |
13 | SettingsScreenContainer.propTypes = {
14 | section: PropTypes.bool,
15 | children: PropTypes.node,
16 | withPadding: PropTypes.bool
17 | };
18 |
19 | export default SettingsScreenContainer;
20 |
--------------------------------------------------------------------------------
/src/views/components/SliderControl.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Slider from 'rc-slider';
4 | // import 'rc-slider/assets/index.css';
5 |
6 | export class SliderControl extends React.Component {
7 | constructor (props) {
8 | super(props);
9 | const value = props.value || 0;
10 |
11 | this.state = {
12 | value,
13 | is_changing: false
14 | };
15 | }
16 |
17 | onBeforeChange () {
18 | if (this.state.is_changing) {
19 | return;
20 | }
21 |
22 | this.setState({
23 | value: this.props.value,
24 | is_changing: true
25 | });
26 | }
27 |
28 | handleInput (value) {
29 | this.setState({value});
30 |
31 | if (typeof this.props.onInput === 'function') {
32 | this.props.onInput(value);
33 | }
34 | }
35 |
36 | handleChange (value) {
37 | this.setState({
38 | value,
39 | is_changing: false
40 | });
41 |
42 | if (typeof this.props.onChange === 'function') {
43 | this.props.onChange(value);
44 | }
45 | }
46 |
47 | render() {
48 | const SliderComponent = this.props.tooltip
49 | ? Slider.createSliderWithTooltip(Slider)
50 | : Slider,
51 | currentValue = this.state.is_changing
52 | ? this.state.value
53 | : this.props.value;
54 |
55 | return (
56 |
64 | );
65 | }
66 | }
67 |
68 | SliderControl.propTypes = {
69 | value: PropTypes.number,
70 | max: PropTypes.number,
71 | min: PropTypes.number,
72 | tooltip: PropTypes.bool,
73 | onChange: PropTypes.func,
74 | onInput: PropTypes.func,
75 | disabled: PropTypes.bool
76 | };
77 |
78 | SliderControl.defaultProps = {
79 | value: 0
80 | };
81 |
82 | export default SliderControl;
83 |
--------------------------------------------------------------------------------
/src/views/components/Switch.css:
--------------------------------------------------------------------------------
1 | @value primary from "../styles/colors.css";
2 |
3 | .container {
4 | display: flex;
5 | flex-flow: row nowrap;
6 | align-items: center;
7 | }
8 |
9 | .offLabel,
10 | .onLabel {
11 | font-size: 13px;
12 | font-weight: bold;
13 | text-transform: uppercase;
14 | }
15 | .offLabel {
16 | margin-right: 8px;
17 | }
18 | .onLabel {
19 | margin-left: 8px;
20 | }
21 |
22 | .switch {
23 | display: block;
24 | width: 32px;
25 | height: 14px;
26 | margin: 3px 0;
27 |
28 | position: relative;
29 | }
30 | .isDisabled {
31 | opacity: 0.5;
32 | cursor: not-allowed;
33 | }
34 |
35 | /* Track */
36 | .switch::before {
37 | content: "";
38 | display: block;
39 | width: 100%;
40 | height: 100%;
41 |
42 | position: absolute;
43 |
44 | border-radius: 14px;
45 | background-color: #bebebe;
46 |
47 | transition: all 0.09s;
48 | }
49 | .isOn::before {
50 | background-color: primary;
51 | opacity: 0.54;
52 | }
53 |
54 | /* Thumb */
55 | .switch::after {
56 | content: "";
57 | display: block;
58 | width: 20px;
59 | height: 20px;
60 |
61 | position: absolute;
62 | top: -3px;
63 | left: -3px;
64 |
65 | border-radius: 100%;
66 | background-color: #ffffff;
67 | box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
68 |
69 | transition: all 0.09s;
70 | }
71 | .isOn::after {
72 | transform: translateX(20px);
73 | background-color: primary;
74 | }
75 |
76 | .input {
77 | width: 38px;
78 | height: 20px;
79 | position: absolute;
80 | top: -3px;
81 | left: -3px;
82 | opacity: 0;
83 | z-index: 1;
84 | cursor: pointer;
85 | }
86 | .isDisabled .input {
87 | cursor: inherit;
88 | }
89 |
--------------------------------------------------------------------------------
/src/views/components/Switch.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styles from './Switch.css';
4 |
5 | export const Switch = (props) => {
6 | const {isOn, showLabels, offLabel, onLabel, ...inputProps} = {...props};
7 |
8 | return (
9 |
10 | {showLabels && {offLabel}}
11 |
12 |
17 |
18 | {showLabels && {onLabel}}
19 |
20 | );
21 | };
22 |
23 | Switch.propTypes = {
24 | isOn: PropTypes.bool,
25 | showLabels: PropTypes.bool,
26 | offLabel: PropTypes.string,
27 | onLabel: PropTypes.string,
28 | disabled: PropTypes.bool
29 | };
30 |
31 | Switch.defaultProps = {
32 | isOn: false,
33 | offLabel: 'Off',
34 | onLabel: 'On'
35 | };
36 |
37 | export default Switch;
38 |
--------------------------------------------------------------------------------
/src/views/components/SwitchField.css:
--------------------------------------------------------------------------------
1 | .container {
2 | margin: 8px 0 0 0;
3 | }
4 |
5 | .field {
6 | display: flex;
7 | flex-flow: row nowrap;
8 | justify-content: space-between;
9 | align-items: center;
10 | position: relative;
11 | width: 100%;
12 | height: 54px;
13 | padding: 14px 0;
14 | }
15 | .isFocused {}
16 | .isDisabled {
17 | cursor: not-allowed;
18 | }
19 |
20 | .label {}
21 | .isDisabled > .label {
22 | opacity: 0.5;
23 | }
24 |
--------------------------------------------------------------------------------
/src/views/components/SwitchField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Switch from './Switch.js';
4 | import {getUniqueId} from '../../utilities.js';
5 | import styles from './SwitchField.css';
6 |
7 | export class SwitchField extends React.Component {
8 | constructor (props) {
9 | super(props);
10 |
11 | this.inputId = getUniqueId();
12 | this.state = {is_focused: false};
13 |
14 | this.handleFocus = this.handleFocus.bind(this);
15 | this.handleBlur = this.handleBlur.bind(this);
16 | }
17 |
18 | handleFocus (event) {
19 | this.setState({is_focused: true});
20 |
21 | if (typeof this.props.onFocus === 'function') {
22 | this.props.onFocus(event);
23 | }
24 | }
25 |
26 | handleBlur (event) {
27 | this.setState({is_focused: false});
28 |
29 | if (typeof this.props.onBlur === 'function') {
30 | this.props.onBlur(event);
31 | }
32 | }
33 |
34 | render () {
35 | return (
36 |
37 |
38 |
39 |
47 |
48 |
49 | );
50 | }
51 | }
52 |
53 | SwitchField.propTypes = {
54 | label: PropTypes.string,
55 | name: PropTypes.string,
56 | isOn: PropTypes.bool,
57 | disabled: PropTypes.bool,
58 | onChange: PropTypes.func,
59 | onFocus: PropTypes.func,
60 | onBlur: PropTypes.func
61 | };
62 |
63 | export default SwitchField;
64 |
--------------------------------------------------------------------------------
/src/views/components/TabBar.css:
--------------------------------------------------------------------------------
1 | /* Main app tab bar. */
2 |
3 | @value tabBarBackground from "../styles/colors.css";
4 |
5 | .bar {
6 | display: flex;
7 | flex-flow: row nowrap;
8 | justify-content: center;
9 | flex: 1 1 auto;
10 | background-color: tabBarBackground;
11 | }
12 |
13 | .tabs {
14 | display: flex;
15 | }
16 |
17 | .tab {
18 | display: flex;
19 | margin: 0 8px;
20 | color: #bebebe;
21 | }
22 | .tab.active {
23 | color: #ffffff;
24 | }
25 |
--------------------------------------------------------------------------------
/src/views/components/TabBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from './Button.js';
4 | import styles from './TabBar.css';
5 |
6 | export const TabBar = (props) => (
7 |
8 |
9 | {props.buttons.map((button, index) => (
10 | -
11 |
12 |
13 | ))}
14 |
15 |
16 | );
17 |
18 | TabBar.propTypes = {
19 | buttons: PropTypes.arrayOf(PropTypes.shape({
20 | label: PropTypes.string,
21 | icon: PropTypes.node,
22 | to: PropTypes.string,
23 | isActive: PropTypes.bool
24 | }))
25 | };
26 |
27 | TabBar.defaultProps = {
28 | buttons: []
29 | };
30 |
31 | export default TabBar;
32 |
--------------------------------------------------------------------------------
/src/views/components/TextField.css:
--------------------------------------------------------------------------------
1 | @value error from "../styles/colors.css";
2 |
3 | .container {
4 | margin: 8px 0 0 0;
5 | animation: finishedRendering 1ms;
6 | }
7 | /* A utility animation that enables the component to detect when the styling
8 | has been rendered to the DOM. */
9 | @keyframes finishedRendering {
10 | from { transform: none; }
11 | }
12 |
13 | .field {
14 | display: flex;
15 | position: relative;
16 | border-radius: 4px;
17 | cursor: text;
18 |
19 | /* Show a box shadow as a temporary outline until the notched outline is initiated. */
20 | box-shadow: inset 0 0 0 1px #7e7e7e;
21 | }
22 | .hasNotchedOutline {
23 | box-shadow: none;
24 | }
25 | .isDisabled,
26 | .isDisabled:hover {
27 | opacity: 0.5;
28 | cursor: not-allowed;
29 | }
30 |
31 | .outlineWrapper {
32 | pointer-events: none;
33 | position: absolute;
34 | width: 100%;
35 | height: 100%;
36 | }
37 | .outline {
38 | fill: none;
39 | stroke-width: 2px;
40 | stroke: #7e7e7e;
41 | transition: stroke 0.15s, stroke-width 0.15s;
42 | }
43 | .field:hover .outline {
44 | stroke: #a2a2a2;
45 | }
46 | .isFocused .outline,
47 | .isFocused:hover .outline {
48 | stroke-width: 4px;
49 | stroke: #a2a2a2;
50 | }
51 | .hasError .outline,
52 | .hasError:hover .outline {
53 | stroke-width: 4px;
54 | stroke: error;
55 | }
56 | .isDisabled .outline,
57 | .isDisabled:hover .outline {
58 | stroke: #7e7e7e;
59 | }
60 |
61 | .label,
62 | .mask {
63 | position: absolute;
64 | left: 14px;
65 | top: 18px;
66 | pointer-events: none;
67 | transform-origin: 0 0;
68 | transition: all 0.15s;
69 | }
70 | .hasError > .label {
71 | color: error;
72 | }
73 | .isFocused > .label,
74 | .isPopulated > .label {
75 | top: -5px;
76 | transform: scale(0.75); /* Scale 16px font size down to 12px. */
77 | }
78 |
79 | .mask {
80 | opacity: 0.6;
81 | }
82 |
83 | .input {
84 | width: 100%;
85 | height: 54px;
86 | padding: 14px;
87 | background: none;
88 | border: none;
89 | outline: none;
90 |
91 | color: inherit;
92 | font-size: 1rem;
93 | line-height: 1.75rem;
94 |
95 | cursor: inherit !important;
96 | }
97 | .textarea {
98 | composes: input;
99 | height: 108px;
100 | resize: none;
101 | }
102 |
103 | .bottom {
104 | height: 16px;
105 | margin: 0 14px;
106 | padding-top: 3px;
107 | }
108 |
109 | .errorMessage {
110 | color: error;
111 | font-size: 12px;
112 | line-height: 16px;
113 | }
114 |
--------------------------------------------------------------------------------
/src/views/components/ThermostatCard.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-flow: column nowrap;
4 | justify-content: space-around;
5 | align-items: center;
6 | height: 100%;
7 | min-height: 128px;
8 | }
9 |
10 | .sensorValue {
11 | font-size: 20px;
12 | color: #c8c8c8;
13 | }
14 |
15 | .coolValue {
16 | font-size: 20px;
17 | color: #0000c8;
18 | }
19 |
20 | .heatValue {
21 | font-size: 20px;
22 | color: #c80000;
23 | }
24 |
25 | .sensorValues {
26 | display: flex;
27 | flex-flow: row nowrap;
28 | align-items: center;
29 | justify-content: center;
30 | flex: 1 0;
31 | margin: 0px 0px 10px 0px;
32 | }
33 |
34 | .sensorTitle {
35 | display: flex;
36 | flex-flow: column nowrap;
37 | align-items: center;
38 | justify-content: center;
39 | flex: 1 0;
40 | /* margin: 24px 0; */
41 | /* padding: 0 8px; */
42 | text-align: center;
43 | /* display: block; */
44 | font-size: 68px;
45 | color: #969696;
46 | }
47 |
48 | .sensorPanelA {
49 | display: flex;
50 | flex-flow: column nowrap;
51 | align-items: center;
52 | justify-content: center;
53 | flex: 1 0;
54 | padding: 12px 12px 12px 12px;
55 | /* margin: 24px 0; */
56 | /* padding: 0 16px; */
57 | text-align: center;
58 | /* align-items: center; */
59 | border-bottom: 1px solid #7e7e7e;
60 | }
61 |
62 | .sensorPanelB {
63 | display: flex;
64 | flex-flow: column nowrap;
65 | align-items: center;
66 | justify-content: center;
67 | flex: 1 0;
68 | padding: 12px 12px 12px 12px;
69 | /* margin: 24px 0; */
70 | /* padding: 0 16px; */
71 | text-align: center;
72 | /* align-items: center; */
73 | }
74 |
75 | .hidden {
76 | display: none;
77 | }
78 |
--------------------------------------------------------------------------------
/src/views/components/TimeField.css:
--------------------------------------------------------------------------------
1 | .container {}
2 |
3 | .field {
4 | display: flex;
5 | flex-flow: row nowrap;
6 | }
7 |
8 | .hour,
9 | .minute,
10 | .meridiem {
11 | flex: 1 0 auto;
12 | }
13 | .hour,
14 | .minute {
15 | margin-right: 10px;
16 | }
17 |
--------------------------------------------------------------------------------
/src/views/components/Toolbar.css:
--------------------------------------------------------------------------------
1 | /* A generic toolbar with left, middle, and right sections. */
2 |
3 | .toolbar {
4 | display: flex;
5 | flex-flow: row nowrap;
6 | justify-content: flex-start;
7 | flex: 1 1 auto;
8 | max-height: 100%;
9 | white-space: nowrap;
10 | }
11 |
12 |
13 | /* Components */
14 |
15 | .left {
16 | display: flex;
17 | flex-flow: row nowrap;
18 | justify-content: flex-start;
19 | align-items: center;
20 |
21 | flex: 0 1 50%;
22 | position: relative;
23 | }
24 | .right {
25 | display: flex;
26 | flex-flow: row nowrap;
27 | justify-content: flex-end;
28 | align-items: center;
29 |
30 | flex: 0 1 50%;
31 | position: relative;
32 | }
33 | .middle {
34 | display: flex;
35 | flex-flow: row nowrap;
36 | justify-content: center;
37 | align-items: center;
38 |
39 | flex: 0 0 auto;
40 | margin-left: auto;
41 | margin-right: auto;
42 | position: relative;
43 | }
44 |
--------------------------------------------------------------------------------
/src/views/components/Toolbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styles from './Toolbar.css';
4 |
5 | const Toolbar = (props) => (
6 |
7 |
{props.leftChildren}
8 | {props.middleChildren &&
9 |
{props.middleChildren}
}
10 | {(props.rightChildren || props.middleChildren) &&
11 |
{props.rightChildren}
}
12 |
13 | );
14 |
15 | Toolbar.propTypes = {
16 | leftChildren: PropTypes.node,
17 | middleChildren: PropTypes.node,
18 | rightChildren: PropTypes.node
19 | };
20 |
21 | export default Toolbar;
22 |
--------------------------------------------------------------------------------
/src/views/components/VideoStream.css:
--------------------------------------------------------------------------------
1 | /* A canvas for rendering streamed video. */
2 |
3 | .canvas {
4 | display: block;
5 | max-width: 100%;
6 | max-height: 100%;
7 | }
8 |
--------------------------------------------------------------------------------
/src/views/icons/AddIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 |
4 | export const AddIcon = (props) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default AddIcon;
11 |
--------------------------------------------------------------------------------
/src/views/icons/AutomationsIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 |
4 | export const AutomationsIcon = (props) => (
5 |
6 |
7 |
8 |
9 | );
10 |
11 | export default AutomationsIcon;
12 |
--------------------------------------------------------------------------------
/src/views/icons/CameraIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 |
4 | export const CameraIcon = (props) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default CameraIcon;
11 |
--------------------------------------------------------------------------------
/src/views/icons/DashboardIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 |
4 | export const DashboardIcon = (props) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default DashboardIcon;
11 |
--------------------------------------------------------------------------------
/src/views/icons/DoorIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 |
4 | export const DashboardIcon = (props) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default DashboardIcon;
11 |
--------------------------------------------------------------------------------
/src/views/icons/DownloadIcon.css:
--------------------------------------------------------------------------------
1 | .shadowedFill {
2 | fill: rgba(255, 255, 255, 0.9);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/icons/DownloadIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 | import './DownloadIcon.css';
4 |
5 | export const StopButtonIcon = () => (
6 |
7 |
8 |
9 | );
10 |
11 | export default StopButtonIcon;
12 |
--------------------------------------------------------------------------------
/src/views/icons/ExpandIcon.css:
--------------------------------------------------------------------------------
1 | .arrow {
2 | transition: all 0.15s;
3 | }
4 |
5 | .topArrow {
6 | composes: arrow;
7 | transform-origin: 15px 7.75px;
8 | }
9 | .isHovered .topArrow {
10 | transform: translate(1px, -1px);
11 | }
12 | .isExpanded .topArrow {
13 | transform: rotate(-180deg) translate(-2.5px, 2.5px);
14 | }
15 | .isExpanded.isHovered .topArrow {
16 | transform: rotate(-180deg) translate(-1.5px, 1.5px);
17 | }
18 |
19 | .bottomArrow {
20 | composes: arrow;
21 | transform-origin: 7.75px 15px;
22 | }
23 | .isHovered .bottomArrow {
24 | transform: translate(-1px, 1px);
25 | }
26 | .isExpanded .bottomArrow {
27 | transform: rotate(180deg) translate(2.5px, -2.5px);
28 | }
29 | .isExpanded.isHovered .bottomArrow {
30 | transform: rotate(180deg) translate(1.5px, -1.5px);
31 | }
32 |
--------------------------------------------------------------------------------
/src/views/icons/ExpandIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import IconBase from './IconBase.js';
4 | import styles from './ExpandIcon.css';
5 |
6 | export class ExpandIcon extends React.Component {
7 | constructor (props) {
8 | super(props);
9 |
10 | this.state = {isHovered: false};
11 |
12 | this.handleMouseOver = this.handleMouseOver.bind(this);
13 | this.handleMouseOut = this.handleMouseOut.bind(this);
14 | }
15 |
16 | handleMouseOver () {
17 | this.setState({isHovered: true});
18 | }
19 |
20 | handleMouseOut () {
21 | this.setState({isHovered: false});
22 | }
23 |
24 | render () {
25 | const {isExpanded, ...iconBaseProps} = this.props;
26 |
27 | return (
28 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 | }
41 |
42 | ExpandIcon.propTypes = {
43 | isExpanded: PropTypes.bool
44 | };
45 |
46 | export default ExpandIcon;
47 |
--------------------------------------------------------------------------------
/src/views/icons/GameControllerIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 |
4 | export const GameControllerIcon = (props) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default GameControllerIcon;
11 |
--------------------------------------------------------------------------------
/src/views/icons/IconBase.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: inline-flex;
3 | }
4 |
5 | .icon {
6 | fill: currentColor;
7 | }
8 |
--------------------------------------------------------------------------------
/src/views/icons/IconBase.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styles from './IconBase.css';
4 |
5 | export const IconBase = ({size, onClick, onMouseOver, onMouseOut, children, ...props}) => (
6 |
11 |
20 |
21 | );
22 |
23 | IconBase.propTypes = {
24 | size: PropTypes.number.isRequired,
25 | onClick: PropTypes.func,
26 | onMouseOver: PropTypes.func,
27 | onMouseOut: PropTypes.func,
28 | children: PropTypes.node
29 | };
30 |
31 | IconBase.defaultProps = {
32 | size: 40
33 | };
34 |
35 | export default IconBase;
36 |
--------------------------------------------------------------------------------
/src/views/icons/LeftCarrotIcon.css:
--------------------------------------------------------------------------------
1 | .shadowedFill {
2 | fill: rgba(255, 255, 255, 0.9);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/icons/LeftCarrotIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 | import './StopButtonIcon.css';
4 |
5 | export const StopButtonIcon = () => (
6 |
7 |
8 |
9 | );
10 |
11 | export default StopButtonIcon;
12 |
--------------------------------------------------------------------------------
/src/views/icons/MenuIndicatorIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 |
4 | export const MenuIndicatorIcon = (props) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default MenuIndicatorIcon;
11 |
--------------------------------------------------------------------------------
/src/views/icons/MuteButtonIcon.css:
--------------------------------------------------------------------------------
1 | .shadowedFill {
2 | fill: rgba(255, 255, 255, 0.9);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/icons/MuteButtonIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 | import './StopButtonIcon.css';
4 |
5 | export const StopButtonIcon = () => (
6 |
7 |
8 |
9 |
10 |
11 | );
12 |
13 | export default StopButtonIcon;
14 |
--------------------------------------------------------------------------------
/src/views/icons/PlayButtonIcon.css:
--------------------------------------------------------------------------------
1 | .shadowedFill {
2 | fill: rgba(255, 255, 255, 0.9);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/icons/PlayButtonIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import IconBase from './IconBase.js';
4 | import styles from './PlayButtonIcon.css';
5 |
6 | export const PlayButtonIcon = ({shadowed, ...props}) => (
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {shadowed &&
24 | }
25 |
26 |
27 |
28 | );
29 |
30 | PlayButtonIcon.propTypes = {
31 | shadowed: PropTypes.bool
32 | };
33 |
34 | export default PlayButtonIcon;
35 |
--------------------------------------------------------------------------------
/src/views/icons/PlayMediaButtonIcon.css:
--------------------------------------------------------------------------------
1 | .shadowedFill {
2 | fill: rgba(255, 255, 255, 0.9);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/icons/PlayMediaButtonIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 | import './StopButtonIcon.css';
4 |
5 | export const StopButtonIcon = () => (
6 |
7 |
8 |
9 | );
10 |
11 | export default StopButtonIcon;
12 |
--------------------------------------------------------------------------------
/src/views/icons/RightCarrotIcon.css:
--------------------------------------------------------------------------------
1 | .shadowedFill {
2 | fill: rgba(255, 255, 255, 0.9);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/icons/RightCarrotIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 | import './StopButtonIcon.css';
4 |
5 | export const StopButtonIcon = () => (
6 |
7 |
8 |
9 | );
10 |
11 | export default StopButtonIcon;
12 |
--------------------------------------------------------------------------------
/src/views/icons/ServiceIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import CameraIcon from '../icons/CameraIcon.js';
4 | import GameControllerIcon from '../icons/GameControllerIcon.js';
5 |
6 | export const ServiceIcon = (props) => {
7 | const Icon = ServiceIcon.iconComponents[props.service.type];
8 |
9 | return Icon
10 | ?
11 | : props.shouldRenderBlank && ;
12 | };
13 |
14 | ServiceIcon.willRenderIcon = ({type}) => Boolean(ServiceIcon.iconComponents[type]);
15 |
16 | ServiceIcon.propTypes = {
17 | service: PropTypes.object,
18 | size: PropTypes.number,
19 | shouldRenderBlank: PropTypes.bool
20 | };
21 |
22 | ServiceIcon.iconComponents = {
23 | 'camera': CameraIcon,
24 | 'network-camera': CameraIcon,
25 | 'game-machine': GameControllerIcon
26 | };
27 |
28 | export default ServiceIcon;
29 |
--------------------------------------------------------------------------------
/src/views/icons/SettingsIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 |
4 | export const SettingsIcon = (props) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default SettingsIcon;
11 |
--------------------------------------------------------------------------------
/src/views/icons/ShieldCrossedIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 |
4 | export const ShieldCrossedIcon = (props) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default ShieldCrossedIcon;
11 |
--------------------------------------------------------------------------------
/src/views/icons/ShieldIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import IconBase from './IconBase.js';
4 |
5 | export const ShieldIcon = ({shieldChecked, ...props}) => (
6 |
7 | {shieldChecked
8 | ?
9 | :
10 | }
11 |
12 | );
13 |
14 | ShieldIcon.propTypes = {
15 | shieldChecked: PropTypes.bool
16 | };
17 |
18 | export default ShieldIcon;
19 |
--------------------------------------------------------------------------------
/src/views/icons/StopButtonIcon.css:
--------------------------------------------------------------------------------
1 | .shadowedFill {
2 | fill: rgba(255, 255, 255, 0.9);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/icons/StopButtonIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import IconBase from './IconBase.js';
4 | import styles from './StopButtonIcon.css';
5 |
6 | export const StopButtonIcon = ({shadowed, ...props}) => (
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {shadowed &&
25 | }
26 |
27 |
28 |
29 | );
30 |
31 | StopButtonIcon.propTypes = {
32 | shadowed: PropTypes.bool
33 | };
34 |
35 | export default StopButtonIcon;
36 |
--------------------------------------------------------------------------------
/src/views/icons/UnMuteButtonIcon.css:
--------------------------------------------------------------------------------
1 | .shadowedFill {
2 | fill: rgba(255, 255, 255, 0.9);
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/icons/UnMuteButtonIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 | import './StopButtonIcon.css';
4 |
5 | export const UnMuteButtonIcon = () => (
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 |
14 | export default UnMuteButtonIcon;
15 |
--------------------------------------------------------------------------------
/src/views/icons/UserIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 |
4 | export const UserIcon = (props) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default UserIcon;
11 |
--------------------------------------------------------------------------------
/src/views/icons/downCarrotIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconBase from './IconBase.js';
3 |
4 | export const downCarrotIcon = (props) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default downCarrotIcon;
11 |
--------------------------------------------------------------------------------
/src/views/layouts/App.css:
--------------------------------------------------------------------------------
1 | @value tabBarHeight: 56px;
2 |
3 | .app {
4 | composes: fillContext from "../styles/helpers.css";
5 | display: flex;
6 | flex-flow: column nowrap;
7 | }
8 |
9 | .toolbar {
10 | display: flex;
11 | flex-flow: column nowrap;
12 | flex: 0 0 auto;
13 | }
14 |
15 | .content {
16 | flex: 1 1 auto;
17 | overflow-x: hidden;
18 | overflow-y: auto;
19 | height: 100%;
20 | position: relative;
21 | -webkit-overflow-scrolling: touch;
22 | }
23 |
24 | .tabBar {
25 | display: flex;
26 | flex-flow: column nowrap;
27 | flex: 0 0 tabBarHeight;
28 | }
29 |
--------------------------------------------------------------------------------
/src/views/layouts/Grid.css:
--------------------------------------------------------------------------------
1 | .grid {
2 | display: flex;
3 | flex-flow: row wrap;
4 | align-items: stretch;
5 |
6 | margin: 16px 0 16px 16px;
7 | }
8 | @media (min-width: 720px) {
9 | .grid {
10 | margin-top: 24px;
11 | margin-left: 24px;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/views/layouts/Grid.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styles from './Grid.css';
4 |
5 | export const Grid = (props) => (
6 |
7 | {props.children}
8 |
9 | );
10 |
11 | Grid.propTypes = {
12 | children: PropTypes.node
13 | };
14 |
15 | export default Grid;
16 |
--------------------------------------------------------------------------------
/src/views/layouts/GridColumn.css:
--------------------------------------------------------------------------------
1 | .column {
2 | display: flex;
3 | flex-flow: column nowrap;
4 | padding: 0 16px 16px 0;
5 | }
6 | @media (min-width: 720px) {
7 | .column {
8 | padding-right: 24px;
9 | padding-bottom: 24px;
10 | }
11 | }
12 |
13 | .column1, .column2, .column3, .column4, .column5, .column6, .column7, .column8, .column9, .column10, .column11, .column12 {
14 | composes: column;
15 | }
16 |
17 | .column1 { width: calc(1% / 0.04); }
18 | .column2 { width: calc(2% / 0.04); }
19 | .column3 { width: calc(3% / 0.04); }
20 | .column4,
21 | .column5,
22 | .column6,
23 | .column7,
24 | .column8,
25 | .column9,
26 | .column10,
27 | .column11,
28 | .column12 { width: calc(4% / 0.04); }
29 |
30 | @media (min-width: 600px) {
31 | .column1 { width: calc(1% / 0.08); }
32 | .column2 { width: calc(2% / 0.08); }
33 | .column3 { width: calc(3% / 0.08); }
34 | .column4 { width: calc(4% / 0.08); }
35 | .column5 { width: calc(5% / 0.08); }
36 | .column6 { width: calc(6% / 0.08); }
37 | .column7 { width: calc(7% / 0.08); }
38 | .column8,
39 | .column9,
40 | .column10,
41 | .column11,
42 | .column12 { width: calc(8% / 0.08); }
43 | }
44 |
45 | @media (min-width: 1024px) {
46 | .column1 { width: calc(1% / 0.12); }
47 | .column2 { width: calc(2% / 0.12); }
48 | .column3 { width: calc(3% / 0.12); }
49 | .column4 { width: calc(4% / 0.12); }
50 | .column5 { width: calc(5% / 0.12); }
51 | .column6 { width: calc(6% / 0.12); }
52 | .column7 { width: calc(7% / 0.12); }
53 | .column8 { width: calc(8% / 0.12); }
54 | .column9 { width: calc(9% / 0.12); }
55 | .column10 { width: calc(10% / 0.12); }
56 | .column11 { width: calc(11% / 0.12); }
57 | .column12 { width: calc(12% / 0.12); }
58 | }
59 |
--------------------------------------------------------------------------------
/src/views/layouts/GridColumn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styles from './GridColumn.css';
4 |
5 | export const GridColumn = (props) => {
6 | const numberOfColumns = props.columns || '',
7 | columnClass = numberOfColumns ? styles[`column${numberOfColumns}`] : '',
8 | combinedClasses = `${styles.column} ${columnClass}`;
9 |
10 | return (
11 |
12 | {props.children}
13 |
14 | );
15 | };
16 |
17 | GridColumn.propTypes = {
18 | children: PropTypes.node,
19 | columns: PropTypes.number
20 | };
21 |
22 | export default GridColumn;
23 |
--------------------------------------------------------------------------------
/src/views/layouts/LoginScreen.css:
--------------------------------------------------------------------------------
1 | @value toolbarBackground from "../styles/colors.css";
2 | @value error from "../styles/colors.css";
3 |
4 | .screen {
5 | display: flex;
6 | flex-flow: column nowrap;
7 | justify-content: center;
8 | align-items: center;
9 | width: 100%;
10 | min-height: 100%;
11 | padding: 16px;
12 | }
13 | .container {
14 | display: flex;
15 | flex-flow: column nowrap;
16 | position: relative;
17 | overflow: hidden;
18 | width: 100%;
19 | max-width: 400px;
20 | padding: 20px 30px;
21 | background-color: toolbarBackground;
22 | border-radius: 4px;
23 | }
24 | .branding {
25 | margin: 8px 0;
26 | }
27 | .errorMessage {
28 | font-size: 12px;
29 | color: error;
30 | }
31 | .loading {
32 | composes: fillContext from "../styles/helpers.css";
33 | display: flex;
34 | flex-flow: column nowrap;
35 | justify-content: center;
36 | align-items: center;
37 | background-color: toolbarBackground;
38 | opacity: 0.8;
39 | }
40 | .footer {
41 | margin: 16px 0;
42 | font-size: 14px;
43 | }
44 |
--------------------------------------------------------------------------------
/src/views/styles/base.css:
--------------------------------------------------------------------------------
1 | @import "./reset.css";
2 | @value background from "../styles/colors.css";
3 | @value text from "../styles/colors.css";
4 |
5 | html {
6 | width: 100%;
7 | height: 100%;
8 | overflow: hidden;
9 | background-color: background;
10 | font-family: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
11 | color: text;
12 | cursor: default;
13 | user-select: none;
14 | }
15 |
16 | body {
17 | width: 100%;
18 | height: 100%;
19 | overflow: hidden;
20 | }
21 |
22 | a {
23 | color: #dcf7ff;
24 | text-decoration: underline;
25 | }
26 |
--------------------------------------------------------------------------------
/src/views/styles/colors.css:
--------------------------------------------------------------------------------
1 | @value background: #555555;
2 | @value toolbarBackground: #333333;
3 | @value tabBarBackground: #222222;
4 | @value text: #dddddd;
5 | @value primary: #5cd4f9;
6 | @value error: #ff4436;
7 |
--------------------------------------------------------------------------------
/src/views/styles/helpers.css:
--------------------------------------------------------------------------------
1 | .fillContext {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | }
8 |
9 | .aspectRatio43 {
10 | display: block;
11 | width: 100%;
12 |
13 | padding-top: calc((3 / 4) * 100%);
14 | }
15 |
16 | .selectable {
17 | user-select: text;
18 | cursor: auto;
19 | }
20 |
--------------------------------------------------------------------------------
/src/views/styles/reset.css:
--------------------------------------------------------------------------------
1 | * { box-sizing: border-box; }
2 |
3 | h1, h2, h3, h4, h5, h6 {
4 | margin: 0;
5 | font-size: 100%;
6 | font-weight: normal;
7 | }
8 |
9 | a {
10 | text-decoration: none;
11 | cursor: pointer;
12 | }
13 |
14 | ol,
15 | ul,
16 | menu,
17 | li {
18 | margin: 0;
19 | padding: 0;
20 | list-style: none;
21 | }
22 |
23 | button {
24 | padding: 0;
25 | background: transparent;
26 | border: 0;
27 | }
28 |
29 | iframe { border: 0; }
30 |
31 | img {
32 | display: inline-block;
33 | max-width: 100%;
34 | max-height: 100%;
35 | }
36 |
37 | figure { margin: 0; }
38 |
39 | label { cursor: inherit; }
40 |
--------------------------------------------------------------------------------
/testEmail.js:
--------------------------------------------------------------------------------
1 | const nodemailer = require('nodemailer');
2 |
3 | async function sendEmail() {
4 | try {
5 | // Create a transporter using SMTP with your Gmail and App Password
6 | const transporter = nodemailer.createTransport({
7 | service: 'gmail',
8 | auth: {
9 | user: 'physiphile@gmail.com', // Your Gmail address
10 | pass: 'kwxdxxcqeytkfufv' // Your generated App Password
11 | }
12 | });
13 |
14 | const message = {
15 | from: 'physiphile@gmail.com',
16 | to: '4058168685@mms.att.net', // Email-to-SMS gateway for AT&T
17 | subject: 'Test Email via Nodemailer and App Password',
18 | text: 'This is a test email sent using Gmail and App Password without OAuth2.'
19 | };
20 |
21 | // Send email
22 | const info = await transporter.sendMail(message);
23 | console.log('Text sent successfully!', info);
24 | } catch (error) {
25 | console.error('Error sending text:', error);
26 | }
27 | }
28 |
29 | sendEmail();
30 |
--------------------------------------------------------------------------------