├── .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 | ![Alt text](/images/dashboard.png?raw=true "Dashboard") 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 | ![Alt text](/images/server-graph.svg?raw=true "Server Graph") 46 | 47 | #### Client 48 | ![Alt text](/images/website-graph.svg?raw=true "Server Graph") 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 | 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 |
({ 35 | value: room.id, 36 | label: room.name 37 | })) 38 | }}} 39 | values={{room: this.getRoomValue()}} 40 | disabled={this.props.areRoomsLoading} 41 | onSaveableChange={this.handleRoomChange} /> 42 | ); 43 | } 44 | } 45 | 46 | DeviceRoomField.propTypes = { 47 | device: PropTypes.object.isRequired, 48 | rooms: PropTypes.array.isRequired, 49 | areRoomsLoading: PropTypes.bool, 50 | saveRoom: PropTypes.func.isRequired 51 | }; 52 | 53 | const mapStateToProps = ({devicesList, roomsList}, {deviceId}) => ({ 54 | device: getDeviceById(devicesList, deviceId), 55 | rooms: getRooms(roomsList), 56 | areRoomsLoading: !hasInitialFetchCompleted(roomsList) 57 | }), 58 | mapDispatchToProps = (dispatch) => ({ 59 | saveRoom: (deviceId, roomId, originalRoomId) => dispatch(setDeviceRoom(deviceId, roomId, originalRoomId)) 60 | }); 61 | 62 | export default connect(mapStateToProps, mapDispatchToProps)(DeviceRoomField); 63 | -------------------------------------------------------------------------------- /src/views/components/DevicesList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import List from './List.js'; 4 | import ServiceIcon from '../icons/ServiceIcon.js'; 5 | import {connect} from 'react-redux'; 6 | import {getDevices} from '../../state/ducks/devices-list/selectors.js'; 7 | import {getServiceById} from '../../state/ducks/services-list/selectors.js'; 8 | 9 | export class DevicesList extends React.Component { 10 | render () { 11 | return ( 12 | 13 | {this.props.devices.map((device) => { 14 | const firstService = device.services[0], 15 | additionalServicesCount = device.services.length - 1, 16 | additionalServicesNoun = 'feature' + (additionalServicesCount > 1 ? 's' : ''); 17 | 18 | return { 19 | key: device.id, 20 | icon: firstService && , 21 | label: device.settings.name || 'Device', 22 | secondaryText: firstService && (firstService.settings.get('name') || firstService.strings.get('friendly_type')) + 23 | (additionalServicesCount > 0 24 | ? ' and ' + additionalServicesCount + ' other ' + additionalServicesNoun 25 | : ''), 26 | link: this.props.deviceLinkBase ? this.props.deviceLinkBase + '/' + device.id : null, 27 | onClick: this.props.onDeviceClick ? () => this.props.onDeviceClick(device.id) : null 28 | }; 29 | })} 30 | 31 | ); 32 | } 33 | } 34 | 35 | DevicesList.propTypes = { 36 | devices: PropTypes.array.isRequired, 37 | deviceLinkBase: PropTypes.string, 38 | onDeviceClick: PropTypes.func 39 | }; 40 | 41 | const mapStateToProps = ({devicesList, servicesList}, ownProps) => { 42 | const devices = ownProps.devices || getDevices(devicesList); 43 | 44 | return { 45 | devices: devices.map((device) => ({ 46 | ...device, 47 | services: device.services.map(({id}) => getServiceById(servicesList, id, false)) 48 | })) 49 | }; 50 | }; 51 | 52 | export default connect(mapStateToProps)(DevicesList); 53 | -------------------------------------------------------------------------------- /src/views/components/DevicesListScreen.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 DevicesList from './DevicesList.js'; 7 | import DeviceDetailsScreen from './DeviceDetailsScreen.js'; 8 | import Button from './Button.js'; 9 | import AddIcon from '../icons/AddIcon.js'; 10 | import DeviceAddScreen from './DeviceAddScreen.js'; 11 | import {connect} from 'react-redux'; 12 | import {compose} from 'redux'; 13 | import {getDevices} from '../../state/ducks/devices-list/selectors.js'; 14 | import {getServiceById} from '../../state/ducks/services-list/selectors.js'; 15 | 16 | export class DevicesListScreen extends React.Component { 17 | render () { 18 | return ( 19 | 24 | 25 | }> 26 | 27 | } /> 28 | 29 | 30 | } /> 31 | 32 | 33 | ); 34 | } 35 | } 36 | 37 | DevicesListScreen.propTypes = { 38 | devices: PropTypes.array.isRequired, 39 | match: PropTypes.object.isRequired 40 | }; 41 | 42 | const mapStateToProps = ({devicesList, servicesList}) => ({ 43 | devices: getDevices(devicesList).map((device) => ({ 44 | ...device, 45 | services: device.services.map(({id}) => getServiceById(servicesList, id, false)) 46 | })) 47 | }); 48 | 49 | export default compose( 50 | withRoute(), 51 | connect(mapStateToProps) 52 | )(DevicesListScreen); 53 | -------------------------------------------------------------------------------- /src/views/components/DimmerCard.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/Form.css: -------------------------------------------------------------------------------- 1 | .form { 2 | margin: 8px 16px; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/components/GameMachineCard.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-flow: column nowrap; 4 | height: 100%; 5 | } 6 | 7 | .main { 8 | display: flex; 9 | flex-flow: column nowrap; 10 | align-items: center; 11 | justify-content: center; 12 | flex: 1 0; 13 | margin: 24px 0; 14 | padding: 0 16px; 15 | text-align: center; 16 | } 17 | 18 | .hopperTotal { 19 | display: block; 20 | font-size: 32px; 21 | color: #c8c8c8; 22 | } 23 | 24 | .hopperTotalDescription { 25 | margin-top: 8px; 26 | font-size: 13px; 27 | color: #969696; 28 | } 29 | .hopperTotalDescription::before { 30 | content: "…"; 31 | } 32 | 33 | .addCredit { 34 | display: flex; 35 | flex-flow: column nowrap; 36 | padding: 0 16px; 37 | } 38 | 39 | .addCreditTitle { 40 | margin-top: 8px; 41 | font-size: 13px; 42 | color: #969696; 43 | } 44 | 45 | .addCreditButtons { 46 | display: flex; 47 | flex-flow: row wrap; 48 | justify-content: center; 49 | } 50 | -------------------------------------------------------------------------------- /src/views/components/GrowPodCard.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/List.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-flow: column nowrap; 4 | width: 100%; 5 | } 6 | 7 | .title { 8 | height: 56px; 9 | padding: 16px; 10 | font-size: 14px; 11 | line-height: 24px; 12 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 13 | } 14 | 15 | .listWrapper { 16 | flex: 1 1 auto; 17 | overflow: auto; 18 | } 19 | 20 | .list { 21 | padding: 8px 16px; 22 | } 23 | -------------------------------------------------------------------------------- /src/views/components/ListItem.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | flex-flow: row nowrap; 4 | align-items: center; 5 | height: 48px; 6 | position: relative; 7 | z-index: 1; /* Needed for the row to stay visible while dragging. */ 8 | } 9 | .isDraggable { 10 | cursor: grab; 11 | } 12 | .isBeingDragged::after { 13 | content: ""; 14 | width: 100%; 15 | height: 100%; 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | pointer-events: auto; 20 | cursor: grabbing; 21 | } 22 | 23 | .rowContent { 24 | display: flex; 25 | flex-flow: row nowrap; 26 | justify-content: flex-start; 27 | align-items: center; 28 | flex: 1 0; 29 | width: 100%; 30 | height: 100%; 31 | color: inherit; 32 | text-decoration: none; 33 | } 34 | 35 | .rowContentInner { 36 | display: flex; 37 | flex-flow: row nowrap; 38 | justify-content: flex-start; 39 | flex: 1 1 auto; 40 | } 41 | 42 | .rowIcon { 43 | display: flex; 44 | margin-right: 16px; 45 | align-self: flex-start; 46 | } 47 | 48 | .rowText { 49 | display: flex; 50 | flex-flow: column nowrap; 51 | justify-content: center; 52 | flex: 1 1 auto; 53 | } 54 | .primaryText { 55 | display: flex; 56 | flex-flow: row nowrap; 57 | justify-content: space-between; 58 | align-items: baseline; 59 | } 60 | 61 | .metaText { 62 | font-size: 13px; 63 | } 64 | 65 | .secondaryText, 66 | .tertiaryText { 67 | font-size: 14px; 68 | color: #b6b6b6; 69 | } 70 | 71 | .rowActions { 72 | display: flex; 73 | } 74 | -------------------------------------------------------------------------------- /src/views/components/LockCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ServiceCardBase from './ServiceCardBase.js'; 4 | import Button from './Button.js'; 5 | import {connect} from 'react-redux'; 6 | import {lockLock, lockUnlock} from '../../state/ducks/services-list/operations.js'; 7 | 8 | export const LockCard = (props) => { 9 | const isLocked = props.service.state.get('locked'), 10 | toggleLock = () => { 11 | if (isLocked) { 12 | props.unlock(props.service.id); 13 | } else { 14 | props.lock(props.service.id); 15 | } 16 | }; 17 | 18 | return ( 19 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | LockCard.propTypes = { 31 | service: PropTypes.object, 32 | lock: PropTypes.func, 33 | unlock: PropTypes.func 34 | }; 35 | 36 | const mapDispatchToProps = (dispatch) => { 37 | return { 38 | lock: (serviceId) => dispatch(lockLock(serviceId)), 39 | unlock: (serviceId) => dispatch(lockUnlock(serviceId)) 40 | }; 41 | }; 42 | 43 | export default connect(null, mapDispatchToProps)(LockCard); 44 | -------------------------------------------------------------------------------- /src/views/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import TextField from './TextField.js'; 4 | import Actions from './Actions.js'; 5 | import Button from './Button.js'; 6 | import {default as FormValidator, required} from '../form-validation.js'; 7 | import {connect} from 'react-redux'; 8 | import * as session from '../../state/ducks/session'; 9 | 10 | export class LoginForm extends React.Component { 11 | constructor (props) { 12 | super(props); 13 | 14 | this.state = { 15 | email: '', 16 | password: '', 17 | validation_errors: {} 18 | }; 19 | 20 | this.validator = new FormValidator(this.state) 21 | .field('email', 'Email', required) 22 | .field('password', 'Password', required); 23 | 24 | this.handleFieldChange = this.handleFieldChange.bind(this); 25 | this.handleSubmit = this.handleSubmit.bind(this); 26 | } 27 | 28 | componentDidUpdate () { 29 | this.validator.setState(this.state); 30 | } 31 | 32 | handleFieldChange (event) { 33 | const newValue = event.target.value; 34 | 35 | this.setState({ 36 | [event.target.name]: newValue, 37 | validation_errors: this.validator.validateField(event.target.name, newValue, event.type) 38 | }); 39 | } 40 | 41 | handleSubmit (event) { 42 | event.preventDefault(); 43 | 44 | const errors = this.validator.validateForm(); 45 | 46 | if (this.validator.hasErrors()) { 47 | this.setState({validation_errors: errors}); 48 | 49 | return; 50 | } 51 | 52 | this.props.login(this.state.email, this.state.password); 53 | } 54 | 55 | render () { 56 | return ( 57 | 58 | 67 | 76 | 77 | 78 | 79 | 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 | 18 | {children} 19 | 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 | --------------------------------------------------------------------------------