Working on sensors page
This commit is contained in:
parent
85201e254e
commit
c56a678925
|
|
@ -11,7 +11,8 @@
|
|||
"dependencies": {
|
||||
"plotly.js": "^2.14.0",
|
||||
"preact": "^10.10.6",
|
||||
"react-query": "^3.39.2"
|
||||
"react-query": "^3.39.2",
|
||||
"wouter": "^2.8.0-alpha.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.3.0",
|
||||
|
|
@ -6423,6 +6424,14 @@
|
|||
"object-assign": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wouter": {
|
||||
"version": "2.8.0-alpha.2",
|
||||
"resolved": "https://registry.npmjs.org/wouter/-/wouter-2.8.0-alpha.2.tgz",
|
||||
"integrity": "sha512-aPsL5m5rW9RiceClOmGj6t5gn9Ut2TJVr98UDi1u9MIRNYiYVflg6vFIjdDYJ4IAyH0JdnkSgGwfo0LQS3k2zg==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
|
@ -11300,6 +11309,12 @@
|
|||
"object-assign": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"wouter": {
|
||||
"version": "2.8.0-alpha.2",
|
||||
"resolved": "https://registry.npmjs.org/wouter/-/wouter-2.8.0-alpha.2.tgz",
|
||||
"integrity": "sha512-aPsL5m5rW9RiceClOmGj6t5gn9Ut2TJVr98UDi1u9MIRNYiYVflg6vFIjdDYJ4IAyH0JdnkSgGwfo0LQS3k2zg==",
|
||||
"requires": {}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"dependencies": {
|
||||
"plotly.js": "^2.14.0",
|
||||
"preact": "^10.10.6",
|
||||
"react-query": "^3.39.2"
|
||||
"react-query": "^3.39.2",
|
||||
"wouter": "^2.8.0-alpha.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,23 @@
|
|||
import { request } from './request'
|
||||
|
||||
export type SensorInfo = {
|
||||
sensor: string
|
||||
config: Record<string, string>
|
||||
id: number
|
||||
name: string
|
||||
authKey: string
|
||||
}
|
||||
|
||||
export const getSensors = () => request<SensorInfo[]>('/api/sensors')
|
||||
|
||||
export const createSensor = (name: string) =>
|
||||
request<SensorInfo>('/api/sensors', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
|
||||
export const updateSensor = (id: number, name: string) =>
|
||||
request<SensorInfo>(`/api/sensors/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
button {
|
||||
background: var(--button-bg-color);
|
||||
color: var(--button-fg-color);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
padding: 0.25rem 0.8rem;
|
||||
border-radius: 0.25rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button > .svg-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 1px;
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--button-hover-bg-color);
|
||||
color: var(--button-hover-fg-color);
|
||||
}
|
||||
|
||||
button.icon {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
button.icon > .svg-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
.sensors-page {
|
||||
.sensors-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
> button {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
section.content {
|
||||
padding: 1rem;
|
||||
|
||||
> .box {
|
||||
max-width: 50rem;
|
||||
padding: 1rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sensors-list {
|
||||
.sensor-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0.25rem 0;
|
||||
|
||||
&.head {
|
||||
opacity: 0.75;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
> .id {
|
||||
flex: 0.3;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
> .name {
|
||||
margin-right: 0.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
> .key {
|
||||
display: flex;
|
||||
flex: 1.5;
|
||||
|
||||
button {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
> .actions {
|
||||
flex: 2;
|
||||
text-align: right;
|
||||
|
||||
button {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
|
||||
|
After Width: | Height: | Size: 294 B |
|
|
@ -0,0 +1 @@
|
|||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path></svg>
|
||||
|
After Width: | Height: | Size: 494 B |
|
|
@ -0,0 +1 @@
|
|||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
|
||||
|
After Width: | Height: | Size: 431 B |
|
|
@ -1,4 +1,5 @@
|
|||
:root {
|
||||
--main-font: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
--main-bg-color: #eee;
|
||||
--main-fg-color: #000;
|
||||
--button-bg-color: #3988ff;
|
||||
|
|
@ -66,30 +67,16 @@ html {
|
|||
body {
|
||||
background: var(--main-bg-color);
|
||||
color: var(--main-fg-color);
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-family: var(--main-font);
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-family: var(--main-font);
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--button-bg-color);
|
||||
color: var(--button-fg-color);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
padding: 0.25rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--button-hover-bg-color);
|
||||
color: var(--button-hover-fg-color);
|
||||
}
|
||||
@import 'components/button';
|
||||
|
||||
input,
|
||||
select {
|
||||
|
|
@ -197,6 +184,7 @@ header.header > .inner > .menu-button {
|
|||
align-items: center;
|
||||
font-size: 125%;
|
||||
cursor: pointer;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
section.content {
|
||||
|
|
@ -249,7 +237,7 @@ section.content {
|
|||
.settings-modal .inner {
|
||||
background-color: var(--box-bg-color);
|
||||
color: var(--box-fg-color);
|
||||
margin-top: 5%;
|
||||
margin-top: 5vh;
|
||||
box-shadow: var(--box-shadow);
|
||||
border-radius: 0.5rem;
|
||||
width: 20rem;
|
||||
|
|
@ -490,6 +478,11 @@ form.horizontal .input label {
|
|||
stroke: currentColor;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
|
|
@ -501,3 +494,4 @@ form.horizontal .input label {
|
|||
|
||||
@import "components/dashboard-header";
|
||||
@import "components/filters-panel";
|
||||
@import "components/sensors-page";
|
||||
|
|
|
|||
|
|
@ -4,3 +4,6 @@ export { ReactComponent as MenuIcon } from '@/assets/icons/menu.svg'
|
|||
export { ReactComponent as CancelIcon } from '@/assets/icons/cancel.svg'
|
||||
export { ReactComponent as FiltersIcon } from '@/assets/icons/filters.svg'
|
||||
export { ReactComponent as PlusIcon } from '@/assets/icons/plus.svg'
|
||||
export { ReactComponent as EyeIcon } from '@/assets/icons/eye.svg'
|
||||
export { ReactComponent as EyeOffIcon } from '@/assets/icons/eye-off.svg'
|
||||
export { ReactComponent as EditIcon } from '@/assets/icons/edit.svg'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { CancelIcon } from '@/icons'
|
||||
import { Link } from 'wouter'
|
||||
|
||||
type Props = {
|
||||
shown: boolean
|
||||
|
|
@ -16,7 +17,7 @@ export const UserMenu = ({ shown, onHide }: Props) => {
|
|||
|
||||
<nav>
|
||||
<a href="#">Dashboards</a>
|
||||
<a href="#">Sensors</a>
|
||||
<Link href="/sensors">Sensors</Link>
|
||||
<a href="#">Settings</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
import { useAppContext } from '@/contexts/AppContext'
|
||||
import { useHashLocation } from '@/utils/hooks/useHashLocation'
|
||||
import { Route, Router as Wouter } from 'wouter'
|
||||
import { NewDashboardPage } from './dashboard/NewDashboardPage'
|
||||
import { LoginPage } from './login/LoginPage'
|
||||
import { SensorsPage } from './sensors/SensorsPage'
|
||||
|
||||
export const Router = () => {
|
||||
const { loggedIn } = useAppContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
{loggedIn && <NewDashboardPage />}
|
||||
{loggedIn && (
|
||||
<Wouter hook={useHashLocation}>
|
||||
<Route path="/">
|
||||
<NewDashboardPage />
|
||||
</Route>
|
||||
<Route path="/sensors">
|
||||
<SensorsPage />
|
||||
</Route>
|
||||
</Wouter>
|
||||
)}
|
||||
{!loggedIn && <LoginPage />}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
|
|||
onChange={handleChange}
|
||||
>
|
||||
{sensors.data?.map((s) => (
|
||||
<option key={s.sensor} value={s.sensor}>
|
||||
{s.config?.name ?? s.sensor}
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export const EditableBox = ({
|
|||
<div className="box" style={{ height: '100%' }}>
|
||||
<div className="header">
|
||||
<div className="drag-handle" onMouseDown={handleMouseDown}>
|
||||
<div className="name">{box.title ?? box.sensor ?? ''}</div>
|
||||
<div className="name">{box.title || box.sensor || ''}</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<div className="action" onClick={() => refreshRef.current?.()}>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const SensorSettings = ({ sensor, onClose, onUpdate }: Props) => {
|
|||
try {
|
||||
await Promise.all(
|
||||
Object.entries(value).map(([name, value]) =>
|
||||
setSensorConfig({ sensor: sensor.sensor, name, value })
|
||||
setSensorConfig({ sensor: sensor.id, name, value })
|
||||
)
|
||||
)
|
||||
} catch (err) {
|
||||
|
|
@ -64,7 +64,7 @@ export const SensorSettings = ({ sensor, onClose, onUpdate }: Props) => {
|
|||
<form onSubmit={handleSave}>
|
||||
<div className="input">
|
||||
<label>Sensor</label>
|
||||
<input value={sensor.sensor} disabled />
|
||||
<input value={sensor.id} disabled />
|
||||
</div>
|
||||
<div className="input">
|
||||
<label>Name</label>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import { getSensors } from '@/api/sensors'
|
||||
import { PlusIcon } from '@/icons'
|
||||
import { UserLayout } from '@/layouts/UserLayout/UserLayout'
|
||||
import { useQuery } from 'react-query'
|
||||
import { SensorItem } from './components/SensorItem'
|
||||
|
||||
export const SensorsPage = () => {
|
||||
const sensors = useQuery(['/sensors'], getSensors)
|
||||
|
||||
return (
|
||||
<UserLayout
|
||||
header={
|
||||
<div className="sensors-head">
|
||||
<div>Sensors</div>
|
||||
<button>
|
||||
<PlusIcon /> Add sensor
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
className="sensors-page"
|
||||
>
|
||||
<div className="box">
|
||||
<div className="sensors-list">
|
||||
<div className="sensor-item head">
|
||||
<div className="id">ID</div>
|
||||
<div className="name">Name</div>
|
||||
<div className="key">Key</div>
|
||||
<div className="actions"></div>
|
||||
</div>
|
||||
{sensors.data?.map((i) => (
|
||||
<SensorItem key={i.id} sensor={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</UserLayout>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { SensorInfo } from '@/api/sensors'
|
||||
import { CancelIcon, EditIcon, EyeIcon, RefreshIcon } from '@/icons'
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
type Props = {
|
||||
sensor: SensorInfo
|
||||
}
|
||||
|
||||
export const SensorItem = ({ sensor }: Props) => {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="sensor-item">
|
||||
<div className="id">{sensor.id}</div>
|
||||
<div className="name">{sensor.name}</div>
|
||||
<div className="key">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={sensor.authKey}
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
<button className="icon" onClick={() => setShowPassword((v) => !v)}>
|
||||
<EyeIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button>
|
||||
<EditIcon /> Edit
|
||||
</button>
|
||||
<button>
|
||||
<RefreshIcon /> Refresh key
|
||||
</button>
|
||||
<button>
|
||||
<CancelIcon /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/** @source wouter example */
|
||||
import { useState, useEffect } from 'preact/hooks'
|
||||
|
||||
// (excluding the leading '#' symbol)
|
||||
const currentLocation = () => {
|
||||
return window.location.hash.replace(/^#/, '') || '/'
|
||||
}
|
||||
|
||||
const navigate = (to: string) => (window.location.hash = to)
|
||||
|
||||
export const useHashLocation = () => {
|
||||
const [loc, setLoc] = useState(currentLocation())
|
||||
|
||||
useEffect(() => {
|
||||
// this function is called whenever the hash changes
|
||||
const handler = () => setLoc(currentLocation())
|
||||
|
||||
// subscribe to hash changes
|
||||
window.addEventListener('hashchange', handler)
|
||||
|
||||
return () => window.removeEventListener('hashchange', handler)
|
||||
}, [])
|
||||
|
||||
return [loc, navigate]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS sensors (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
auth_key TEXT NOT NULL,
|
||||
/* Temporary column used for migration */
|
||||
ident TEXT NOT NULL
|
||||
);
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/* Add rows for sensors */
|
||||
INSERT INTO sensors (ident, name, auth_key)
|
||||
SELECT
|
||||
sensor,
|
||||
IFNULL(
|
||||
(SELECT c.value FROM sensor_config c WHERE c.sensor = sensor),
|
||||
sensor
|
||||
) as "name",
|
||||
hex(randomblob(32)) as "auth_key"
|
||||
FROM sensor_values
|
||||
GROUP BY sensor;
|
||||
|
||||
/* We need to add FK key and the only way is to create new table */
|
||||
/* So we rename old table, create a new one and migrate the data there */
|
||||
ALTER TABLE sensor_values RENAME TO sensor_values_old;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sensor_values (
|
||||
timestamp INTEGER NOT NULL,
|
||||
sensor_id INTEGER NOT NULL,
|
||||
value REAL NOT NULL,
|
||||
FOREIGN KEY (sensor_id) REFERENCES sensors(id)
|
||||
);
|
||||
|
||||
INSERT INTO sensor_values (timestamp, sensor_id, value)
|
||||
SELECT timestamp, (SELECT s.id FROM sensors s WHERE s.ident = sensor), value
|
||||
FROM sensor_values_old;
|
||||
|
||||
DROP TABLE sensor_values_old;
|
||||
|
||||
/* this column was temporary */
|
||||
ALTER TABLE sensors DROP COLUMN ident;
|
||||
|
|
@ -39,10 +39,12 @@ func main() {
|
|||
// Routes that are only accessible after logging in
|
||||
loginProtected := router.Group("/", middleware.LoginAuthMiddleware(server))
|
||||
loginProtected.GET("/api/sensors", routes.GetSensors(server))
|
||||
loginProtected.POST("/api/sensors", routes.PostSensors(server))
|
||||
loginProtected.PUT("/api/sensors/:sensor", routes.PutSensors(server))
|
||||
loginProtected.GET("/api/sensors/:sensor/values/latest", routes.GetSensorLatestValue(server))
|
||||
loginProtected.GET("/api/sensors/:sensor/values", routes.GetSensorValues(server))
|
||||
loginProtected.GET("/api/sensors/:sensor/config", routes.GetSensorConfig(server))
|
||||
loginProtected.PUT("/api/sensors/:sensor/config/:key", routes.PutSensorConfig(server))
|
||||
//loginProtected.GET("/api/sensors/:sensor/config", routes.GetSensorConfig(server))
|
||||
//loginProtected.PUT("/api/sensors/:sensor/config/:key", routes.PutSensorConfig(server))
|
||||
loginProtected.GET("/api/dashboards", routes.GetDashboards(server))
|
||||
loginProtected.POST("/api/dashboards", routes.PostDashboard(server))
|
||||
loginProtected.GET("/api/dashboards/:id", routes.GetDashboardById(server))
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package middleware
|
|||
import (
|
||||
"basic-sensor-receiver/app"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
|
@ -22,10 +23,23 @@ func LoginAuthMiddleware(server *app.Server) gin.HandlerFunc {
|
|||
}
|
||||
|
||||
func KeyAuthMiddleware(server *app.Server) gin.HandlerFunc {
|
||||
keyWithBearer := "Bearer " + server.Config.AuthKey
|
||||
|
||||
return func(c *gin.Context) {
|
||||
if c.GetHeader("authorization") != keyWithBearer {
|
||||
sensorParam := c.Param("sensor")
|
||||
sensorId, err := strconv.ParseInt(sensorParam, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sensor, err := server.Services.Sensors.GetById(sensorId)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if c.GetHeader("authorization") != "Bearer "+sensor.AuthKey {
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package routes
|
|||
import (
|
||||
"basic-sensor-receiver/app"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
|
@ -23,14 +24,20 @@ type getLatestSensorValueQuery struct {
|
|||
func PostSensorValues(s *app.Server) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var newValue postSensorValueBody
|
||||
sensor := c.Param("sensor")
|
||||
|
||||
if err := c.BindJSON(&newValue); err != nil {
|
||||
c.AbortWithError(400, err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.Services.SensorValues.Push(sensor, newValue.Value); err != nil {
|
||||
sensorId, err := getSensorId(c)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithError(400, err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.Services.SensorValues.Push(sensorId, newValue.Value); err != nil {
|
||||
c.AbortWithError(400, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -43,14 +50,19 @@ func GetSensorValues(s *app.Server) gin.HandlerFunc {
|
|||
return func(c *gin.Context) {
|
||||
var query getSensorValuesQuery
|
||||
|
||||
sensor := c.Param("sensor")
|
||||
sensorId, err := getSensorId(c)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithError(400, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.BindQuery(&query); err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
values, err := s.Services.SensorValues.GetList(sensor, query.From, query.To)
|
||||
values, err := s.Services.SensorValues.GetList(sensorId, query.From, query.To)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
|
|
@ -64,14 +76,19 @@ func GetSensorLatestValue(s *app.Server) gin.HandlerFunc {
|
|||
return func(c *gin.Context) {
|
||||
var query getLatestSensorValueQuery
|
||||
|
||||
sensor := c.Param("sensor")
|
||||
sensorId, err := getSensorId(c)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithError(400, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.BindQuery(&query); err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := s.Services.SensorValues.GetLatest(sensor, query.To)
|
||||
value, err := s.Services.SensorValues.GetLatest(sensorId, query.To)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
|
|
@ -81,3 +98,9 @@ func GetSensorLatestValue(s *app.Server) gin.HandlerFunc {
|
|||
c.JSON(http.StatusOK, value)
|
||||
}
|
||||
}
|
||||
|
||||
func getSensorId(c *gin.Context) (int64, error) {
|
||||
sensor := c.Param("sensor")
|
||||
|
||||
return strconv.ParseInt(sensor, 10, 64)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,70 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type postSensorsBody struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type putSensorsBody struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func GetSensors(s *app.Server) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
sensors, err := s.Services.Sensors.GetList()
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, sensors)
|
||||
}
|
||||
}
|
||||
|
||||
func PostSensors(s *app.Server) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
body := postSensorsBody{}
|
||||
|
||||
if err := c.BindJSON(&body); err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
sensor, err := s.Services.Sensors.Create(body.Name)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, sensor)
|
||||
}
|
||||
}
|
||||
|
||||
func PutSensors(s *app.Server) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
body := putSensorsBody{}
|
||||
|
||||
sensorId, err := getSensorId(c)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.BindJSON(&body); err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
sensor, err := s.Services.Sensors.Update(sensorId, body.Name)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, sensor)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ type sensorValue struct {
|
|||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
func (s *SensorValuesService) Push(sensor string, value float64) (int64, error) {
|
||||
res, err := s.ctx.DB.Exec("INSERT INTO sensor_values (timestamp, sensor, value) VALUES (?, ?, ?)", time.Now().Unix(), sensor, value)
|
||||
func (s *SensorValuesService) Push(sensorId int64, value float64) (int64, error) {
|
||||
res, err := s.ctx.DB.Exec("INSERT INTO sensor_values (timestamp, sensor_id, value) VALUES (?, ?, ?)", time.Now().Unix(), sensorId, value)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
|
@ -21,12 +21,12 @@ func (s *SensorValuesService) Push(sensor string, value float64) (int64, error)
|
|||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
func (s *SensorValuesService) GetList(sensor string, from int64, to int64) ([]sensorValue, error) {
|
||||
func (s *SensorValuesService) GetList(sensorId int64, from int64, to int64) ([]sensorValue, error) {
|
||||
var value float64
|
||||
var timestamp int64
|
||||
values := make([]sensorValue, 0)
|
||||
|
||||
rows, err := s.ctx.DB.Query("SELECT timestamp, value FROM sensor_values WHERE sensor = ? AND timestamp > ? AND timestamp < ? ORDER BY timestamp ASC", sensor, from, to)
|
||||
rows, err := s.ctx.DB.Query("SELECT timestamp, value FROM sensor_values WHERE sensor_id = ? AND timestamp > ? AND timestamp < ? ORDER BY timestamp ASC", sensorId, from, to)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -51,10 +51,10 @@ func (s *SensorValuesService) GetList(sensor string, from int64, to int64) ([]se
|
|||
return values, nil
|
||||
}
|
||||
|
||||
func (s *SensorValuesService) GetLatest(sensor string, to int64) (*sensorValue, error) {
|
||||
func (s *SensorValuesService) GetLatest(sensorId int64, to int64) (*sensorValue, error) {
|
||||
var value = sensorValue{}
|
||||
|
||||
row := s.ctx.DB.QueryRow("SELECT timestamp, value FROM sensor_values WHERE sensor = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT 1", sensor, to)
|
||||
row := s.ctx.DB.QueryRow("SELECT timestamp, value FROM sensor_values WHERE sensor_id = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT 1", sensorId, to)
|
||||
|
||||
err := row.Scan(&value.Timestamp, &value.Value)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
package services
|
||||
|
||||
import "encoding/hex"
|
||||
|
||||
type SensorsService struct {
|
||||
ctx *Context
|
||||
}
|
||||
|
||||
type sensorItem struct {
|
||||
Sensor string `json:"sensor"`
|
||||
LastUpdate string `json:"lastUpdate"`
|
||||
Config map[string]string `json:"config"`
|
||||
type SensorItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AuthKey string `json:"authKey"`
|
||||
}
|
||||
|
||||
func (s *SensorsService) GetList() ([]sensorItem, error) {
|
||||
sensors := make([]sensorItem, 0)
|
||||
var sensor string
|
||||
var lastUpdate string
|
||||
func (s *SensorsService) GetList() ([]SensorItem, error) {
|
||||
sensors := make([]SensorItem, 0)
|
||||
|
||||
rows, err := s.ctx.DB.Query("SELECT sensor, MAX(timestamp) last_update FROM sensor_values GROUP BY sensor")
|
||||
rows, err := s.ctx.DB.Query("SELECT id, name, auth_key FROM sensors")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -24,18 +24,14 @@ func (s *SensorsService) GetList() ([]sensorItem, error) {
|
|||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&sensor, &lastUpdate)
|
||||
item := SensorItem{}
|
||||
|
||||
err := rows.Scan(&item.Id, &item.Name, &item.AuthKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := s.ctx.Services.SensorConfig.GetValues(sensor)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sensors = append(sensors, sensorItem{Sensor: sensor, LastUpdate: lastUpdate, Config: config})
|
||||
sensors = append(sensors, item)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
|
|
@ -45,3 +41,55 @@ func (s *SensorsService) GetList() ([]sensorItem, error) {
|
|||
|
||||
return sensors, nil
|
||||
}
|
||||
|
||||
func (s *SensorsService) Create(name string) (*SensorItem, error) {
|
||||
item := SensorItem{
|
||||
Name: name,
|
||||
AuthKey: hex.EncodeToString(generateRandomKey(32)),
|
||||
}
|
||||
|
||||
res, err := s.ctx.DB.Exec("INSERT INTO sensors (id, name, auth_key) VALUES (?, ?)", item.Name, item.AuthKey)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item.Id, err = res.LastInsertId()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (s *SensorsService) GetById(id int64) (*SensorItem, error) {
|
||||
item := SensorItem{}
|
||||
|
||||
row := s.ctx.DB.QueryRow("SELECT id, name, auth_key FROM sensors WHERE id = ?", id)
|
||||
|
||||
err := row.Scan(&item.Id, &item.Name, &item.AuthKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (s *SensorsService) Update(id int64, name string) (*SensorItem, error) {
|
||||
item, err := s.GetById(id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item.Name = name
|
||||
|
||||
_, err = s.ctx.DB.Exec("UPDATE sensors SET name = ? WHERE id = ?", item.Name, item.Id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue