import React, { Component, Fragment } from 'react'; import { withStyles, Theme, createStyles } from '@material-ui/core/styles'; import parseMilliseconds from 'parse-ms'; import { Decoder } from '@msgpack/msgpack'; const decoder = new Decoder(); import { Table, TableBody, TableCell, TableHead, TableRow, TableContainer, withWidth, WithWidthProps, isWidthDown, Button, Tooltip, DialogTitle, DialogContent, DialogActions, Box, Dialog, Typography } from '@material-ui/core'; import RefreshIcon from '@material-ui/icons/Refresh'; import ListIcon from '@material-ui/icons/List'; import IconButton from '@material-ui/core/IconButton'; import EditIcon from '@material-ui/icons/Edit'; import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from '../authentication'; import { RestFormProps, FormButton, extractEventValue } from '../components'; import { EMSESPData, EMSESPDeviceData, Device, DeviceValue, DeviceValueUOM, DeviceValueUOM_s, Sensor } from './EMSESPtypes'; import ValueForm from './ValueForm'; import SensorForm from './SensorForm'; import { ENDPOINT_ROOT } from '../api'; export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + 'scanDevices'; export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + 'deviceData'; export const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + 'writeValue'; export const WRITE_SENSOR_ENDPOINT = ENDPOINT_ROOT + 'writeSensor'; const StyledTableCell = withStyles((theme: Theme) => createStyles({ head: { backgroundColor: theme.palette.common.black, color: theme.palette.common.white }, body: { fontSize: 14 } }) )(TableCell); const CustomTooltip = withStyles((theme: Theme) => ({ tooltip: { backgroundColor: theme.palette.secondary.main, color: 'white', boxShadow: theme.shadows[1], fontSize: 11, border: '1px solid #dadde9' } }))(Tooltip); function compareDevices(a: Device, b: Device) { if (a.type < b.type) { return -1; } if (a.type > b.type) { return 1; } return 0; } interface EMSESPDataFormState { confirmScanDevices: boolean; processing: boolean; deviceData?: EMSESPDeviceData; selectedDevice?: number; edit_devicevalue?: DeviceValue; edit_Sensor?: Sensor; } type EMSESPDataFormProps = RestFormProps & AuthenticatedContextProps & WithWidthProps; export const formatDuration = (duration_min: number) => { const { days, hours, minutes } = parseMilliseconds(duration_min * 60000); let formatted = ''; if (days) { formatted += pluralize(days, 'day'); } if (hours) { formatted += pluralize(hours, 'hour'); } if (minutes) { formatted += pluralize(minutes, 'minute'); } return formatted; }; const pluralize = (count: number, noun: string, suffix = 's') => ` ${count} ${noun}${count !== 1 ? suffix : ''} `; function formatValue(value: any, uom: number, digit: number) { switch (uom) { case DeviceValueUOM.HOURS: return value ? formatDuration(value * 60) : '0 hours'; case DeviceValueUOM.MINUTES: return value ? formatDuration(value) : '0 minutes'; case DeviceValueUOM.NONE: case DeviceValueUOM.LIST: return value; case DeviceValueUOM.NUM: return new Intl.NumberFormat().format(value); case DeviceValueUOM.BOOLEAN: return value ? 'on' : 'off'; case DeviceValueUOM.DEGREES: return ( new Intl.NumberFormat(undefined, { minimumFractionDigits: digit }).format(value) + ' ' + DeviceValueUOM_s[uom] ); default: return ( new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom] ); } } class EMSESPDataForm extends Component< EMSESPDataFormProps, EMSESPDataFormState > { state: EMSESPDataFormState = { confirmScanDevices: false, processing: false }; handleDeviceValueChange = (name: keyof DeviceValue) => ( event: React.ChangeEvent ) => { this.setState({ edit_devicevalue: { ...this.state.edit_devicevalue!, [name]: extractEventValue(event) } }); }; cancelEditingDeviceValue = () => { this.setState({ edit_devicevalue: undefined }); }; doneEditingDeviceValue = () => { const { edit_devicevalue, selectedDevice } = this.state; redirectingAuthorizedFetch(WRITE_VALUE_ENDPOINT, { method: 'POST', body: JSON.stringify({ id: selectedDevice, devicevalue: edit_devicevalue }), headers: { 'Content-Type': 'application/json' } }) .then((response) => { if (response.status === 200) { this.props.enqueueSnackbar('Write command sent to device', { variant: 'success' }); } else if (response.status === 204) { this.props.enqueueSnackbar('Write command failed', { variant: 'error' }); } else if (response.status === 403) { this.props.enqueueSnackbar('Write access denied', { variant: 'error' }); } else { throw Error('Unexpected response code: ' + response.status); } }) .catch((error) => { this.props.enqueueSnackbar(error.message || 'Problem writing value', { variant: 'error' }); }); if (edit_devicevalue) { this.setState({ edit_devicevalue: undefined }); } }; sendCommand = (dv: DeviceValue) => { this.setState({ edit_devicevalue: dv }); }; handleSensorChange = (name: keyof Sensor) => ( event: React.ChangeEvent ) => { this.setState({ edit_Sensor: { ...this.state.edit_Sensor!, [name]: extractEventValue(event) } }); }; cancelEditingSensor = () => { this.setState({ edit_Sensor: undefined }); }; doneEditingSensor = () => { const { edit_Sensor } = this.state; redirectingAuthorizedFetch(WRITE_SENSOR_ENDPOINT, { method: 'POST', body: JSON.stringify({ sensor: edit_Sensor }), headers: { 'Content-Type': 'application/json' } }) .then((response) => { if (response.status === 200) { this.props.enqueueSnackbar('Sensor updated', { variant: 'success' }); } else if (response.status === 204) { this.props.enqueueSnackbar('Sensor change failed', { variant: 'error' }); } else if (response.status === 403) { this.props.enqueueSnackbar('Write access denied', { variant: 'error' }); } else { throw Error('Unexpected response code: ' + response.status); } }) .catch((error) => { this.props.enqueueSnackbar(error.message || 'Problem writing value', { variant: 'error' }); }); if (edit_Sensor) { this.setState({ edit_Sensor: undefined }); } }; sendSensor = (sn: Sensor) => { this.setState({ edit_Sensor: sn }); }; noDevices = () => { return this.props.data.devices.length === 0; }; noSensors = () => { return this.props.data.sensors.length === 0; }; noDeviceData = () => { return (this.state.deviceData?.data || []).length === 0; }; renderDeviceItems() { const { width, data } = this.props; return ( EMS Devices

{!this.noDevices() && ( {data.devices.sort(compareDevices).map((device) => ( this.handleRowClick(device)} > {device.brand + ' ' + device.name}{' '} ))}
)} {this.noDevices() && ( No EMS devices found. Check the connections and for possible Tx errors. )}
); } renderSensorItems() { const { data } = this.props; const me = this.props.authenticatedContext.me; return (

Sensors {!this.noSensors() && ( Sensor # ID / Name Temperature {data.sensors.map((sensorData) => ( {me.admin && ( this.sendSensor(sensorData)} > )} {sensorData.no} {sensorData.id} {formatValue(sensorData.temp, DeviceValueUOM.DEGREES, 1)} ))}
)} {this.noSensors() && ( no external temperature sensors were detected )}
); } renderAnalog() { const { data } = this.props; return ( {data.analog > 0 && ( Sensortype Value Analog Input {formatValue(data.analog, DeviceValueUOM.MV, 0)}
)}
); } renderScanDevicesDialog() { return ( Confirm Scan Devices Are you sure you want to start a scan on the EMS bus for all new devices? ); } onScanDevices = () => { this.setState({ confirmScanDevices: true }); }; onScanDevicesRejected = () => { this.setState({ confirmScanDevices: false }); }; onScanDevicesConfirmed = () => { this.setState({ processing: true }); redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT) .then((response) => { if (response.status === 200) { this.props.enqueueSnackbar('Device scan is starting...', { variant: 'info' }); this.setState({ processing: false, confirmScanDevices: false }); } else { throw Error('Invalid status code: ' + response.status); } }) .catch((error) => { this.props.enqueueSnackbar(error.message || 'Problem with scan', { variant: 'error' }); this.setState({ processing: false, confirmScanDevices: false }); }); }; handleRowClick = (device: any) => { this.setState({ selectedDevice: device.id, deviceData: undefined }); redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, { method: 'POST', body: JSON.stringify({ id: device.id }), headers: { 'Content-Type': 'application/json' } }) .then((response) => { if (response.status === 200) { return response.arrayBuffer(); } throw Error('Unexpected response code: ' + response.status); }) .then((arrayBuffer) => { const json: any = decoder.decode(arrayBuffer); this.setState({ deviceData: json }); }) .catch((error) => { this.props.enqueueSnackbar( error.message || 'Problem getting device data', { variant: 'error' } ); this.setState({ deviceData: undefined }); }); }; renderDeviceData() { const { deviceData } = this.state; const { width } = this.props; const me = this.props.authenticatedContext.me; if (this.noDevices()) { return; } if (!deviceData) { return; } return (

{deviceData.name} {!this.noDeviceData() && ( {deviceData.data.map((item, i) => ( {item.c && me.admin && ( this.sendCommand(item)} > )} {item.n} {formatValue(item.v, item.u, 0)} ))}
)} {this.noDeviceData() && ( No data available for this device )}
); } render() { const { edit_devicevalue, edit_Sensor } = this.state; return (

{this.renderDeviceItems()} {this.renderDeviceData()} {this.renderSensorItems()} {this.renderAnalog()}

} variant="contained" color="secondary" onClick={this.props.loadData} > Refresh } variant="contained" onClick={this.onScanDevices} > Scan Devices {this.renderScanDevicesDialog()} {edit_devicevalue && ( )} {edit_Sensor && ( )}
); } } export default withAuthenticatedContext(withWidth()(EMSESPDataForm));