UNPKG

bt-sensors-plugin-sk

Version:

Bluetooth Sensors for Signalk - see https://www.npmjs.com/package/bt-sensors-plugin-sk for a list of supported classes

541 lines (439 loc) 15.1 kB
import Form from '@rjsf/core' ; import validator from '@rjsf/validator-ajv8'; import React from "react"; import ReactHtmlParser from 'react-html-parser'; import {useEffect, useState} from 'react' import {Button, Grid } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import { SignalCellular0Bar, SignalCellular1Bar, SignalCellular2Bar, SignalCellular3Bar, SignalCellular4Bar, SignalCellularConnectedNoInternet0Bar } from '@material-ui/icons'; const log = (type) => console.log.bind(console, type); import ListGroup from 'react-bootstrap/ListGroup'; import Tabs from 'react-bootstrap/Tabs'; import Tab from 'react-bootstrap/Tab'; import { ListGroupItem } from 'react-bootstrap'; import ProgressBar from 'react-bootstrap/ProgressBar'; var _sensorMap, _sensorDomains={}, _sensorList={} export default function BTConfig (props) { const _uiSchema= { "ui:options": {label: false}, 'title': { 'ui:widget': 'hidden' } } const baseUISchema = { "ui:field": "LayoutGridField", "ui:layoutGrid": { "ui:row":[ { "ui:row": { "className": "row", "children": [ { "ui:columns": { "className": "col-xs-4", "children": [ "adapter", "transport", "duplicateData", "discoveryTimeout", "discoveryInterval" ] } } ] } } ] } } const useStyles = makeStyles((theme) => ({ root: { '& > *': { margin: theme.spacing(1), }, }, })); const [baseSchema, setBaseSchema] = useState({}) const [baseData, setBaseData] = useState({}) const [schema, setSchema] = useState({}) const [ uiSchema, setUISchema] = useState(_uiSchema ) const [sensorList, setSensorList] = useState([]) const [sensorData, setSensorData] = useState() const [sensorMap, setSensorMap ] = useState(new Map() ) const [progress, setProgress ] = useState({ "progress":0, "maxTimeout": 100, "deviceCount":0, "totalDevices":0}) const [pluginState, setPluginState ] = useState("unknown") const [error, setError ] = useState() const classes = useStyles(); function sendJSONData(cmd, data){ console.log(`sending ${cmd}`) console.log(data) const headers = new Headers(); headers.append("Content-Type", "application/json"); return fetch(`/plugins/bt-sensors-plugin-sk/${cmd}`, { credentials: 'include', method: 'POST', body: JSON.stringify(data), headers:headers }) } async function fetchJSONData(path){ console.log(`fetching ${path}`) var result try { result = fetch(`/plugins/bt-sensors-plugin-sk/${path}`, { credentials: 'include' }) } catch (e) { result= { status: 500, statusText: e.toString() } } return result } async function getSensors(){ console.log("getSensors") const response = await fetchJSONData("getSensors") if (response.status!=200){ throw new Error(`Unable get sensor data: ${response.statusText} (${response.status}) `) } const json = await response.json() console.log(json) //for (let i=0;i<json.length;i++){ // json[i].schema.htmlDescription=<div>{ReactHtmlParser(json[i].schema.htmlDescription)}<p></p></div> //} return json } async function getDomains(){ console.log("getDomains") const response = await fetchJSONData("getDomains") if (response.status!=200){ throw new Error(`Unable get domain data: ${response.statusText} (${response.status}) `) } const json = await response.json() console.log(json) return json } async function getBaseData(){ console.log("getBaseData") const response = await fetchJSONData("getBaseData") if (response.status!=200){ throw new Error(`Unable to get base data: ${response.statusText} (${response.status}) `) } const json = await response.json() console.log(json) json.schema.htmlDescription=<div>{ReactHtmlParser(json.schema.htmlDescription)}<p></p></div> return json } async function getProgress(){ console.log("getProgress") const response = await fetchJSONData("getProgress") if (response.status!=200){ throw new Error(`Unable to get progress: ${response.statusText} (${response.status}) `) } const json = await response.json() console.log(json) return json } function updateSensorData(data){ console.log("updateSensorData") sendJSONData("updateSensorData", data).then((response)=>{ if (response.status != 200) { throw new Error(response.statusText) } sensorMap.get(data.mac_address)._changesMade=false sensorMap.get(data.mac_address).config = data }) } function undoChanges(mac) { console.log("undoChanges") sensorMap.get(mac)._changesMade = false setSensorData( sensorMap.get(mac).config ) } function removeSensorData(mac){ console.log("removeSensorData") try{ sendJSONData("removeSensorData", {mac_address:mac} ).then((response)=>{ if (response.status != 200) { throw new Error(response.statusText) } }) _sensorMap.delete(mac) setSensorMap(new Map(_sensorMap)) setSchema( {} ) } catch {(e)=> setError( new Error(`Couldn't remove ${mac}: ${e}`)) } } function updateBaseData(data){ console.log("updateBaseData") sendJSONData("updateBaseData", data).then( (response )=>{ if (response.status != 200) { setError(new Error(`Unable to update base data: ${response.statusText} (${response.status})`)) } /*else { getProgress().then((json)=>{ setProgress(json) }).catch((e)=>{ setError(e) }) }*/ }) } function refreshSensors(){ console.log('refreshing sensor map') getSensors().then((sensors)=>{ setSensorMap (new Map(sensors.map((sensor)=>[sensor.info.mac,sensor]))); }) .catch((e)=>{ setError(e) }) } useEffect(()=>{ console.log("useEffect([])") let eventSource=null fetchJSONData("getPluginState").then( async (response)=> { if (response.status==404) { setPluginState("unknown") throw new Error("unable to get plugin state") } const json = await response.json() console.log("Setting up eventsource") eventSource = new EventSource("/plugins/bt-sensors-plugin-sk/sse", { withCredentials: true }) setPluginState(json.state) _sensorDomains = await getDomains() eventSource.addEventListener("newsensor", (event) => { console.log("newsensor") let json = JSON.parse(event.data) if (!_sensorMap.has(json.info.mac)) { console.log(`New sensor: ${json.info.mac}`) setSensorMap(new Map(_sensorMap.set(json.info.mac, json))) } }); eventSource.addEventListener("sensorchanged", (event) => { let json = JSON.parse(event.data) console.log("sensorchanged") console.log(json) if (_sensorMap.has(json.mac)) { let sensor = _sensorMap.get(json.mac) Object.assign(sensor.info, json ) setSensorMap(new Map ( _sensorMap )) } }); eventSource.addEventListener("progress", (event) => { console.log("progress") const json = JSON.parse(event.data) setProgress(json) console.log(json) }); eventSource.addEventListener("pluginstate", (event) => { console.log("pluginstate") const json = JSON.parse(event.data) setPluginState(json.state) }); }) .catch( (e) => { setError(e) } ) return () => { console.log("Closing connection to SSE") eventSource.close() }; },[]) useEffect(()=>{ console.log("useEffect([pluginState])") if (pluginState=="started"){ refreshSensors() getBaseData().then((json) => { setBaseSchema(json.schema); setBaseData(json.data); }).catch((e)=>{ setError(e) }) getProgress().then((json)=>{ setProgress(json) }).catch((e)=>{ setError(e) }) } else{ setSensorMap(new Map()) setBaseSchema({}) setBaseData({}) } },[pluginState]) function confirmDelete(mac){ const result = window.confirm(`Delete configuration for ${mac}?`) if (result) removeSensorData(mac) } function signalStrengthIcon(sensor){ if (sensor.info.lastContactDelta ==null || sensor.info.lastContactDelta > sensor.config.discoveryTimeout) return <SignalCellularConnectedNoInternet0Bar/> if (sensor.info.signalStrength > 80) return <SignalCellular4Bar/> if (sensor.info.signalStrength > 60) return <SignalCellular3Bar/> if (sensor.info.signalStrength > 40) return <SignalCellular2Bar/> if (sensor.info.signalStrength > 20) return <SignalCellular1Bar/> return <SignalCellular0Bar/> } function hasConfig(sensor){ return Object.keys(sensor.config).length>0; } function createListGroupItem(sensor){ const config = hasConfig(sensor) return <ListGroupItem action onClick={()=>{ sensor.config.mac_address=sensor.info.mac setSchema(sensor.schema) setSensorData(sensor.config) } }> <div class="d-flex justify-content-between align-items-center" style={config?{fontWeight:"normal"}:{fontStyle:"italic"}}> {`${sensor._changesMade?"*":""}${sensor.info.name} MAC: ${sensor.info.mac} RSSI: ${ifNullNaN(sensor.info.RSSI)}` } <div class="d-flex justify-content-between "> { signalStrengthIcon(sensor) } </div> </div> </ListGroupItem> } function configuredDevices(){ return Array.from(sensorMap.entries()).filter((entry)=>hasConfig(entry[1])) } function devicesInDomain(domain){ return Array.from(sensorMap.entries()).filter((entry)=>entry[1].info.domain===domain) } useEffect(()=>{ console.log("useEffect([sensorMap])") _sensorMap = sensorMap const _sensorDomains = new Set(sensorMap.entries().map((entry)=>{ return entry[1].info.domain})) const sl = { _configured: configuredDevices().map((entry) => { return createListGroupItem(sensorMap.get(entry[0])) }) } _sensorDomains.forEach((d)=>{ sl[d]=devicesInDomain(d).map((entry) => { return createListGroupItem(sensorMap.get(entry[0])) }) }) _sensorList=sl },[sensorMap] ) function ifNullNaN(value){ return value==null? NaN : value } function getSensorList(domain){ return _sensorList[domain] } function getTabs(){ return Object.keys(_sensorList).map((domain)=> {return getTab(domain)}) } // <div style={{paddingBottom: 20}} class="d-flex flex-wrap justify-content-start align-items-start"> // <div class="d-flex flex-column" > function getTab(key){ var title = key.slice(key.charAt(0)==="_"?1:0) return <Tab eventKey={key} title={title.charAt(0).toUpperCase()+title.slice(1) }> <ListGroup style={{ maxHeight: '300px', overflowY: 'auto' }}> {getSensorList(key)} </ListGroup> </Tab> } function openInNewTab (url) { window.open(url, "_blank", "noreferrer"); } function CustomFieldTemplate(props) { const { id, classNames, style, label, help, required, description, errors, children } = props; return ( <div className={classNames} style={style}> <Grid container xs={12} direction="row" spacing="1"> <Grid item xs={1} sm container direction="column"> <Grid item ><label htmlFor={id}>{label}{required ? '*' : null}</label></Grid> <Grid item> {description}</Grid> </Grid> <Grid item>{children}</Grid> </Grid> {errors} {help} </div> ); } if (pluginState=="stopped" || pluginState=="unknown") return (<h3>Enable plugin to see configuration</h3>) else return( <div> <div className={classes.root}> <Button variant="contained" onClick={()=>{openInNewTab("https://github.com/naugehyde/bt-sensors-plugin-sk/tree/1.2.0-beta#configuration")}}>Documentation</Button> <Button variant="contained" onClick={()=>{openInNewTab("https://github.com/naugehyde/bt-sensors-plugin-sk/issues/new/choose")}}>Report Issue</Button> <Button variant="contained" onClick={()=>{openInNewTab("https://discord.com/channels/1170433917761892493/1295425963466952725" )}}>Discord Thread</Button> <p></p> <p></p> </div> <hr style={{"width":"100%","height":"1px","color":"gray","background-color":"gray","text-align":"left","margin-left":0}}></hr> {error?<h2 style="color: red;">{error}</h2>:""} <Form schema={baseSchema} validator={validator} uiSchema={baseUISchema} onChange={(e) => setBaseData(e.formData)} onSubmit={ ({ formData }, e) => { updateBaseData(formData); setSchema({}) } } onError={log('errors')} formData={baseData} /> <hr style={{"width":"100%","height":"1px","color":"gray","background-color":"gray","text-align":"left","margin-left":0}}></hr> <p></p> <p></p> { (progress.deviceCount<progress.totalDevices)? <ProgressBar max={progress.maxTimeout} now={progress.progress} />:"" } <p></p> <Tabs defaultActiveKey="_configured" id="domain-tabs" className="mb-3" onClick={()=>{setSchema({})}} > {getTabs()} </Tabs> <div style= {{ paddingLeft: 10, paddingTop: 10, display: (Object.keys(schema).length==0)? "none" :"" }}> <Grid container direction="column" style={{spacing:5}}> <Grid item><h2>{schema?.title}</h2><p></p></Grid> <Grid item>{ReactHtmlParser(schema?.htmlDescription)}</Grid> </Grid> <Form schema={schema} validator={validator} uiSchema={uiSchema} onChange={(e) => { const s = sensorMap.get(e.formData.mac_address) s._changesMade=true setSensorData(e.formData) } } onSubmit={({ formData }, e) => { updateSensorData(formData) setSchema({}) alert("Changes saved") }} onError={log('errors')} formData={sensorData}> <div className={classes.root}> <Button type='submit' color="primary" variant="contained">Save</Button> <Button variant="contained" onClick={()=>{undoChanges(sensorData.mac_address)}}>Undo</Button> <Button variant="contained" color="secondary" onClick={(e)=>confirmDelete(sensorData.mac_address)}>Delete</Button> </div> </Form> </div> </div> ) }