import React, { Component } from "react";
import { API } from "aws-amplify";
import {
Tabs,
Tab,
Button,
Alert,
ProgressBar,
Grid,
Row,
Col,
Table,
Label,
FormGroup,
FormControl,
ControlLabel,
ListGroup,
ListGroupItem,
HelpBlock,
Modal
} from 'react-bootstrap';
import { Card } from "components/Card/Card.jsx";
class DeviceDetail extends Component {
constructor(props) {
super(props);
// General
this.goDeviceRegistration = this.goDeviceRegistration.bind(this);
// Commands
this.handleCommandStatusChange = this.handleCommandStatusChange.bind(this);
this.handleCommandSubmit = this.handleCommandSubmit.bind(this);
this.handleCommandDetailShow = this.handleCommandDetailShow.bind(this);
this.handleCommandDetailClose = this.handleCommandDetailClose.bind(this);
this.handleCreateCommand = this.handleCreateCommand.bind(this);
this.handleCommandClose = this.handleCommandClose.bind(this);
this.handleTargetTemperatureChange = this.handleTargetTemperatureChange.bind(this);
// Logs
this.handleEventLogsSubmit = this.handleEventLogsSubmit.bind(this);
this.handleEventTypeChange = this.handleEventTypeChange.bind(this);
// Common
this.goBack = this.goBack.bind(this);
this.handleTabSelect = this.handleTabSelect.bind(this);
// Sets up initial state
this.state = {
// common
statusInitial: true,
page: 'general',
// general
loadingDevice: false,
loadingStatus: false,
device: false,
deviceStatus: false,
deviceError: false,
statusError: false,
title: '',
isMinimized: false,
// commands
actualTemperature: '',
targetTemperature: '',
updatedTargetTemperature: '',
targetTemperatureState: null,
setCommand: '',
setCommandValue: '',
powerStatus: '',
commandShow: false,
commandMessage: '',
showCommandHelpBlock: false,
commadError: false,
commandHasMore: true,
loadingCommand: false,
creatingCommand: false,
commandDetail: false,
commandDetailError: false,
commandDetailLoading: false,
commandLastevalkey: null,
commands: [],
commandDetailShow: false,
commandInitial: true,
commandStatus: '',
updatedCommandStatus: '',
// logs
eventLogsError: false,
loadingEventLogs: false,
eventLogsHasMore: true,
events: [],
eventLogsLastevalkey: null,
eventType: '',
updatedEventType: '',
eventDetailShow: false,
eventDetail: false,
eventDetailError: false,
eventDetailLoading: false,
eventLogsInitial: true,
};
}
componentDidMount() {
this.handleResize();
this.getDevice();
this.getDeviceStatus();
this.timer = setInterval(async () => {
await this.getDeviceStatus();
}, 60000); // Gets status every 1 minute
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
clearInterval(this.timer);
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
}
// Handles tab select
handleTabSelect(eventKey) {
if (eventKey === 'commands' && this.state.commandInitial) {
this.setState({ commandInitial: false });
this.getCommands();
} else if (eventKey === 'logs' && this.state.eventLogsInitial) {
this.setState({ eventLogsInitial: false });
this.getEventLogs();
}
this.setState({ page: eventKey });
}
// Handles scroll down to load more
handleScroll = (_event) => {
let page = this.state.page;
if (page === 'commands') {
const {commandError, loadingCommand, commandHasMore} = this.state;
if (commandError || loadingCommand || !commandHasMore) return;
// Checks that the page has scrolled to the bottom
if (this.props.isScrollBottom() && !this.state.commandInitial) {
this.getCommands();
}
} else if (page === 'logs') {
const {eventLogsError, loadingEventLogs, eventLogsHasMore} = this.state;
if (eventLogsError || loadingEventLogs || !eventLogsHasMore) return;
// Checks that the page has scrolled to the bottom
if (this.props.isScrollBottom() && !this.state.eventLogsInitial) {
this.getEventLogs();
}
}
};
// Handles window resize
handleResize = (_event) => {
if (window.innerWidth < 993) {
this.setState({ isMinimized: true, });
} else {
this.setState({ isMinimized: false, });
}
};
// Goes to the device registration instruction
goDeviceRegistration = (deviceId) => {
this.props.history.push({
pathname: '/devices/registration',
state: { deviceId: deviceId }
});
}
// Handles command status change
handleCommandStatusChange = (event) => {
this.setState({ updatedCommandStatus: event.target.value});
}
// Handles command submit
handleCommandSubmit = async (event) => {
event.preventDefault();
this.setState({
commands: [],
commandLastevalkey: null,
commandError: false,
commandStatus: this.state.updatedCommandStatus,
});
this.getCommands();
}
// Handles command detail
handleCommandDetailShow = async (deviceId, commandId) => {
this.setState({
commandDetailError: false,
commandDetail: false,
commandDetailLoading: true
});
let token = await this.props.getToken();
let apiName = 'smart-product-api';
let path = `devices/${deviceId}/commands/${commandId}`;
let params = {
headers: {
'Authorization': token,
},
response: true
};
API.get(apiName, path, params)
.then(response => {
this.setState({ commandDetail: response.data });
})
.catch(error => {
let message = error.response;
if (message === undefined) {
message = error.message;
} else {
message = error.response.data.message;
}
this.setState({ commandDetailError: message });
})
.finally(() => {
this.setState({ commandDetailLoading: false });
})
this.setState({ commandDetailShow: true });
}
// Handles command detail close
handleCommandDetailClose = () => {
this.setState({ commandDetailShow: false });
}
// Handles create command
handleCreateCommand = (mode) => {
let pass = true;
let message = '';
let setCommand = '';
let setCommandValue = '';
switch (mode) {
case 'HEAT': {
message = 'turn on the heat';
setCommand = 'set-mode';
setCommandValue = 'HEAT';
break;
}
case 'AC': {
message = 'turn on the AC';
setCommand = 'set-mode';
setCommandValue = 'AC';
break;
}
case 'OFF': {
message = 'turn off the device';
setCommand = 'set-mode';
setCommandValue = 'OFF';
break;
}
case 'TEMPERATURE': {
if (!this.targetTemperatureValidate()) {
pass = false;
message = 'Invalid target temperature. (50 <= temperature <= 110)';
} else if (this.state.targetTemperature === this.state.updatedTargetTemperature) {
pass = false;
message = 'Target temperature has not been changed.';
} else {
message = `set the temperature to ${this.state.updatedTargetTemperature}`;
setCommand = 'set-temp';
setCommandValue = this.state.updatedTargetTemperature;
}
break;
}
default: {
break;
}
}
if (pass) {
this.setState({
setCommand: setCommand,
setCommandValue: setCommandValue,
commandShow: true,
message: message,
}, () => {});
} else {
this.props.handleNotification(message, 'error', 'pe-7s-check', 5);
}
}
// Handles command pop up close
handleCommandClose = () => {
this.setState({ commandShow: false });
}
// Handles target temperature change
handleTargetTemperatureChange = (event) => {
this.setState({ updatedTargetTemperature: event.target.value }, () => {
this.targetTemperatureValidate();
});
}
// Handles event logs submit
handleEventLogsSubmit = async (event) => {
event.preventDefault();
this.setState({
events: [],
eventLogsLastevalkey: null,
eventLogsError: false,
eventType: this.state.updatedEventType,
});
this.getEventLogs();
}
// Handles input changes
handleEventTypeChange = (event) => {
this.setState({ updatedEventType: event.target.value});
}
// Handles event detail
handleEventDetailShow = async (deviceId, eventId) => {
this.setState({
eventDetailError: false,
eventDetail: false,
eventDetailLoading: true
});
let token = await this.props.getToken();
let apiName = 'smart-product-api';
let path = `devices/${deviceId}/events/${eventId}`;
let params = {
headers: {
'Authorization': token,
},
response: true
};
API.get(apiName, path, params)
.then(response => {
this.setState({ eventDetail: response.data });
if (!response.data.ack) {
this.updateEvent(deviceId, eventId);
}
})
.catch(error => {
let message = error.response;
if (message === undefined) {
message = error.message;
} else {
message = error.response.data.message;
}
this.setState({ eventDetailError: message });
})
.finally(() => {
this.setState({ eventDetailLoading: false });
})
this.setState({ eventDetailShow: true });
}
// Handles detail close
handleEventDetailClose = () => {
this.setState({ eventDetailShow: false });
}
// Gets device information
getDevice = async () => {
this.setState({ loadingDevice: true });
const { deviceId } = this.props.match.params;
let token = await this.props.getToken();
let apiName = 'smart-product-api';
let path = `devices/${deviceId}`;
let params = {
headers: {
'Authorization': token,
},
response: true,
};
API.get(apiName, path, params)
.then(response => {
let device = response.data;
this.setState({
device: device,
title: `${device.deviceName}`,
});
})
.catch(error => {
let message = error.response;
if (message === undefined) {
message = error.message;
} else {
message = error.response.data.message;
}
this.setState({ deviceError: message })
})
.finally(() => {
this.setState({ loadingDevice: false });
});
}
// Gets device status
getDeviceStatus = async () => {
if (this.state.statusInitial) {
this.setState({ loadingStatus: true });
}
const { deviceId } = this.props.match.params;
let token = await this.props.getToken();
let apiName = 'smart-product-api';
let path = `devices/${deviceId}/status`;
let params = {
headers: {
'Authorization': token,
},
response: true,
};
API.get(apiName, path, params)
.then(response => {
let deviceStatus = '';
if (
Object.keys(response.data).length === 0
|| response.data.state.reported === undefined
|| !response.data.connected
) {
deviceStatus = {
state: {},
connected: false,
}
this.setState({
actualTemperature: 'N/A',
targetTemperature: 'N/A',
powerStatus: 'Disconnected',
}, () => {});
} else {
deviceStatus = response.data;
let reported = response.data.state.reported;
if (this.state.statusInitial) {
this.setState({
actualTemperature: reported.actualTemperature,
targetTemperature: reported.targetTemperature,
updatedTargetTemperature: reported.targetTemperature,
powerStatus: reported.powerStatus,
});
} else {
this.setState({
actualTemperature: reported.actualTemperature,
});
}
}
this.setState({ deviceStatus: deviceStatus });
})
.catch(error => {
let message = error.response;
if (message !== undefined) {
if (message.data.error === 'MissingRegistration') {
// If getting the device status has an issue due to MissingRegistration, clear the timer.
clearInterval(this.timer);
} else {
this.props.handleNotification('Failed to get device status', 'error', 'pe-7s-check', 5);
}
}
this.setState({
actualTemperature: 'N/A',
targetTemperature: 'N/A',
powerStatus: 'FAIL',
});
})
.finally(() => {
if (this.state.statusInitial) {
this.setState({
loadingStatus: false,
statusInitial: false,
});
}
});
}
// Gets device commands
getCommands = async () => {
this.setState({ loadingCommand: true});
const { deviceId } = this.props.match.params;
let token = await this.props.getToken();
let apiName = 'smart-product-api';
let path = `devices/${deviceId}/commands`;
let params = {
headers: {
'Authorization': token,
},
response: true,
queryStringParameters: {
lastevalkey: JSON.stringify(this.state.commandLastevalkey),
commandStatus: this.state.commandStatus,
}
};
API.get(apiName, path, params)
.then(response => {
/**
* If response has no data and LastEvaluatedKey is null, there is no more data.
* Otherwise, show the data.
*/
if (response.data.Items.length === 0
&& (response.data.LastEvaluatedKey === null
|| response.data.LastEvaluatedKey === undefined)) {
this.setState({
commandHasMore: false,
loadingCommand: false
});
} else {
this.setState({
commandHasMore: response.data.LastEvaluatedKey !== undefined,
loadingCommand: false,
commandLastevalkey: response.data.LastEvaluatedKey,
commandStatus: response.data.commandStatus,
commands: [
...this.state.commands,
...response.data.Items
]
});
}
}).catch(error => {
let message = error.response;
if (message === undefined) {
message = error.message;
} else {
message = error.response.data.message;
}
this.setState({
commandError: message,
loadingCommand: false
});
});
};
// Validates target temperature
targetTemperatureValidate = () => {
let temperature = this.state.updatedTargetTemperature;
let pass = !isNaN(temperature) && temperature !== ''
&& temperature >= 50 && temperature <= 110;
if (!pass) {
this.setState({
showHelpBlock: true,
targetTemperatureState: 'error',
});
} else {
this.setState({
showHelpBlock: false,
targetTemperatureState: null,
});
}
return pass;
}
// Creates a command
createCommand = async () => {
if (!this.state.creatingCommand) {
this.setState({ creatingCommand: true });
const { deviceId } = this.props.match.params;
let {targetTemperature, setCommand, setCommandValue, powerStatus} = this.state;
if (setCommand === 'set-temp') {
targetTemperature = this.state.updatedTargetTemperature;
}
if (setCommand === 'set-mode') {
powerStatus = setCommandValue;
}
let body = {
deviceId: deviceId,
commandDetails: {
command: this.state.setCommand,
value: this.state.setCommandValue,
},
shadowDetails: {
powerStatus: powerStatus,
actualTemperature: this.state.actualTemperature,
targetTemperature: targetTemperature,
}
}
let token = await this.props.getToken();
let apiName = 'smart-product-api';
let path = `devices/${deviceId}/commands`;
let params = {
headers: {
'Authorization': token,
},
body: body,
response: true,
};
API.post(apiName, path, params)
.then(response => {
this.setState({
powerStatus: powerStatus,
targetTemperature: targetTemperature,
}, () => {});
this.props.handleNotification('Success to execute the command', 'success', 'pe-7s-check', 5);
})
.catch(error => {
this.props.handleNotification('Failed to execute the command', 'error', 'pe-7s-check', 5);
})
.finally(() => {
this.setState({
commandShow: false,
creatingCommand: false,
commands: [],
commandLastevalkey: null,
commandError: false,
});
this.getDeviceStatus();
this.getCommands();
});
} else {
this.props.handleNotification('Still in progress to execute the command', 'error', 'pe-7s-check', 5);
}
}
// Gets device event log
getEventLogs = async () => {
this.setState({ loadingEventLogs: true});
const { deviceId } = this.props.match.params;
let token = await this.props.getToken();
let apiName = 'smart-product-api';
let path = `devices/${deviceId}/events`;
let params = {
headers: {
'Authorization': token,
},
response: true,
queryStringParameters: {
lastevalkey: JSON.stringify(this.state.eventLogsLastevalkey),
eventType: this.state.eventType
}
};
API.get(apiName, path, params)
.then(response => {
/**
* If response has no data and LastEvaluatedKey is null, there is no more data.
* Otherwise, show the data.
*/
if (response.data.Items.length === 0
&& (response.data.LastEvaluatedKey === null
|| response.data.LastEvaluatedKey === undefined)) {
this.setState({
eventLogsHasMore: false,
loadingEventLogs: false
});
} else {
this.setState({
eventLogsHasMore: response.data.LastEvaluatedKey !== undefined,
loadingEventLogs: false,
eventLogsLastevalkey: response.data.LastEvaluatedKey,
eventType: response.data.eventType,
events: [
...this.state.events,
...response.data.Items
]
});
}
}).catch(error => {
let message = error.response;
if (message === undefined) {
message = error.message;
} else {
message = error.response.data.message;
}
this.setState({
eventLogsError: message,
loadingEventLogs: false
});
});
}
// Updates the event
updateEvent = async (deviceId, eventId) => {
let token = await this.props.getToken();
let apiName = 'smart-product-api';
let path = `devices/${deviceId}/events/${eventId}`;
let params = {
headers: {
'Authorization': token,
},
body: {
id: eventId,
ack: true,
suppress: true,
},
};
API.put(apiName, path, params)
.catch(error => {
let message = error.response;
if (message === undefined) {
message = error.message;
} else {
message = error.response.data.message;
}
this.setState({ eventDetailError: message });
});
}
// Goes back to the main landing page
goBack() {
this.props.history.push('/devices');
}
render() {
const { loadingDevice, loadingStatus, device, deviceStatus,
loadingCommand, commandDetailLoading, creatingCommand, commands, commandStatus, powerStatus, commandDetail,
loadingEventLogs, eventDetailLoading, events, eventType, eventDetail,
commandHasMore, eventLogsHasMore, showCommandHelpBlock,
deviceError, statusError, commandError, commandDetailError, eventLogsError, eventDetailError,
title, message, isMinimized, } = this.state;
const commandThArray = ['Command Detail', 'Command Status', 'Created At', 'Updated At'];
const disabledConditions = ['FAIL', 'Disconnected'];
const eventThArray = ['Event Message', 'Event Type', 'Created At'];
return(
{ !loadingDevice &&
Device Information
{title}
<<
{ device &&
this.handleTabSelect(k)} className={ isMinimized ? "mobile_tabs" : "" }>
{/*
General Tab
*/}
{ device && deviceStatus &&
Serial Number
{device.deviceId}
Created At
{device.createdAt}
Activated At
{device.activatedAt}
Updated At
{device.updatedAt}
Status
{device.status}
{ device.status === 'pending' &&
this.goDeviceRegistration(device.deviceId)}>Back to Registration
}
Details
{ device.details &&
Object.keys(device.details).map(key => {
return(
{key}
{device.details[key]}
)
})
}
{ (!device.details ||
Object.keys(device.details).length === 0) &&
Device Details
Not found
}
}
/>
}
{ device && deviceStatus &&
Connectivitiy
{deviceStatus.connected ? "Connected" : "Disconnected"}
State
{ Object.keys(deviceStatus.state).length !== 0 &&
Object.keys(deviceStatus.state.reported).map(key => {
return(
{key}
{deviceStatus.state.reported[key]}
)
})
}
{ Object.keys(deviceStatus.state).length === 0 &&
State
Not found
}
}
/>
}
{/*
Commands Tab
*/}
Issue Remote Command
Mode
Current Mode
this.handleCreateCommand('OFF')}
disabled={powerStatus === 'OFF' || disabledConditions.indexOf(powerStatus) > -1}
active={disabledConditions.indexOf(powerStatus) < 0}>OFF
this.handleCreateCommand('AC')}
disabled={powerStatus === 'AC' || disabledConditions.indexOf(powerStatus) > -1}
active={disabledConditions.indexOf(powerStatus) < 0}>AC
this.handleCreateCommand('HEAT')}
disabled={powerStatus === 'HEAT' || disabledConditions.indexOf(powerStatus) > -1}
active={disabledConditions.indexOf(powerStatus) < 0}>HEAT
Temperature (℉)
Actual Temperature
Target Temperature
-1}
onChange={this.handleTargetTemperatureChange} />
{ showCommandHelpBlock &&
Invalid value
}
this.handleCreateCommand('TEMPERATURE')}
disabled={disabledConditions.indexOf(powerStatus) > -1}
active={disabledConditions.indexOf(powerStatus) < 0}>Set Temperature
}
/>
Search by Command Status
All
Success
Failed
Pending
Search
}
/>
{ !isMinimized &&
{
commandThArray.map((prop, key) => {
return (
{prop}
);
})
}
{/* If there is no command, showing no command found */}
{ commands.length === 0 && !loadingCommand && !commandHasMore &&
No command found.
}
{ commands.map(command => {
return (
this.handleCommandDetailShow(command.deviceId, command.commandId)}>
{command.details.command} / {command.details.value}
{ command.status === 'success' &&
{command.status}
}
{ command.status === 'failed' &&
{command.status}
}
{ command.status === 'pending' &&
{command.status}
}
{command.createdAt}
{command.updatedAt}
);
})
}
}
/>
}
{ isMinimized &&
{/* If there is no command, showing no command found */}
{ commands.length === 0 && !loadingCommand && !commandHasMore &&
No command found.
}
{ commands.map(command => {
return (
this.handleCommandDetailShow(command.deviceId, command.commandId)}>
C: {this.props.handleDateSize(command.createdAt)}
{ command.status === 'success' &&
{command.status}
}
{ command.status === 'failed' &&
{command.status}
}
{ command.status === 'pending' &&
{command.status}
}
U: {this.props.handleDateSize(command.updatedAt)}
{command.details.command} / {command.details.value}
);
})
}
}
/>
}
{ loadingCommand &&
}
{ commandError &&
{this.state.commandError}
}
{/*
Event Log Tab
*/}
Search by Event Type
All
Error
Warning
Info
Diagnostic
Search
}
/>
{ !isMinimized &&
{
eventThArray.map((prop, key) => {
return (
{prop}
);
})
}
{/* If there is no event, showing no event found */}
{ events.length === 0 && !loadingEventLogs && !eventLogsHasMore &&
No event found.
}
{ events.map(event => {
return (
this.handleEventDetailShow(event.deviceId, event.id)}>
{event.message}
{event.type === 'info' &&
{event.type}
}
{event.type === 'warning' &&
{event.type}
}
{event.type === 'error' &&
{event.type}
}
{event.type === 'diagnostic' &&
{event.type}
}
{event.createdAt}
);
})
}
}
/>
}
{ isMinimized &&
{/* If there is no event, showing no event found */}
{ events.length === 0 && !loadingEventLogs && !eventLogsHasMore &&
No event found.
}
{ events.map(event => {
return (
this.handleEventDetailShow(event.deviceId, event.id)}>
{this.props.handleDateSize(event.createdAt)}
{ event.type === 'info' &&
{event.message}
}
{ event.type === 'warning' &&
{event.message}
}
{ event.type === 'error' &&
{event.message}
}
{ event.type === 'diagnostic' &&
{event.message}
}
);
})
}
}
/>
}
{ loadingEventLogs &&
}
{ eventLogsError &&
{this.state.eventLogsError}
}
}
}
{ (loadingDevice || loadingStatus) &&
}
{ deviceError &&
Device Error: {this.state.deviceError}
}
{ statusError &&
Status Error: {this.state.statusError}
}
{/*
Modal dialog
*/}
Command Detail
{ commandDetailLoading &&
}
{ commandDetailError &&
{this.state.commandDetailError}
}
{ commandDetail &&
Command ID
{commandDetail.commandId}
Device ID
{commandDetail.deviceId}
Command Status
{commandDetail.status}
Reason
{commandDetail.reason}
Created At
{commandDetail.createdAt}
Updated At
{commandDetail.updatedAt}
Details
{
Object.keys(commandDetail.details).map(key => {
return(
{key} {commandDetail.details[key]}
)
})
}
}
Close
Device Command
Are you sure to {message}?
Close
this.createCommand()}>Confirm
{ creatingCommand &&
}
Event Detail
{ eventDetailLoading &&
}
{ eventDetailError &&
{this.state.eventDetailError}
}
{ eventDetail &&
ID
{eventDetail.id}
Device ID
{eventDetail.deviceId}
Message
{eventDetail.message}
Details
{
Object.keys(eventDetail.details).map(key => {
return(
{key} {eventDetail.details[key]}
)
})
}
Type
{eventDetail.type}
Created At
{eventDetail.createdAt}
Sent At
{eventDetail.sentAt}
Updated At
{eventDetail.updatedAt}
Acknowledged
{eventDetail.ack ? "Read" : "Unread"}
}
Close
{ device &&
Top
}
)
}
}
export default DeviceDetail;