Working on sensors page

This commit is contained in:
Jan Zípek 2022-08-28 11:56:03 +02:00
parent 85201e254e
commit c56a678925
26 changed files with 492 additions and 64 deletions

View File

@ -11,7 +11,8 @@
"dependencies": { "dependencies": {
"plotly.js": "^2.14.0", "plotly.js": "^2.14.0",
"preact": "^10.10.6", "preact": "^10.10.6",
"react-query": "^3.39.2" "react-query": "^3.39.2",
"wouter": "^2.8.0-alpha.2"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "^2.3.0", "@preact/preset-vite": "^2.3.0",
@ -6423,6 +6424,14 @@
"object-assign": "^4.1.0" "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": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@ -11300,6 +11309,12 @@
"object-assign": "^4.1.0" "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": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@ -28,6 +28,7 @@
"dependencies": { "dependencies": {
"plotly.js": "^2.14.0", "plotly.js": "^2.14.0",
"preact": "^10.10.6", "preact": "^10.10.6",
"react-query": "^3.39.2" "react-query": "^3.39.2",
"wouter": "^2.8.0-alpha.2"
} }
} }

View File

@ -1,8 +1,23 @@
import { request } from './request' import { request } from './request'
export type SensorInfo = { export type SensorInfo = {
sensor: string id: number
config: Record<string, string> name: string
authKey: string
} }
export const getSensors = () => request<SensorInfo[]>('/api/sensors') 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 }),
})

View File

@ -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;
}

View File

@ -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;
}
}
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,5 @@
:root { :root {
--main-font: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
--main-bg-color: #eee; --main-bg-color: #eee;
--main-fg-color: #000; --main-fg-color: #000;
--button-bg-color: #3988ff; --button-bg-color: #3988ff;
@ -66,30 +67,16 @@ html {
body { body {
background: var(--main-bg-color); background: var(--main-bg-color);
color: var(--main-fg-color); color: var(--main-fg-color);
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; font-family: var(--main-font);
} }
button, button,
input, input,
select { select {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; font-family: var(--main-font);
} }
button { @import 'components/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);
}
input, input,
select { select {
@ -197,6 +184,7 @@ header.header > .inner > .menu-button {
align-items: center; align-items: center;
font-size: 125%; font-size: 125%;
cursor: pointer; cursor: pointer;
margin-right: 0.75rem;
} }
section.content { section.content {
@ -249,7 +237,7 @@ section.content {
.settings-modal .inner { .settings-modal .inner {
background-color: var(--box-bg-color); background-color: var(--box-bg-color);
color: var(--box-fg-color); color: var(--box-fg-color);
margin-top: 5%; margin-top: 5vh;
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
border-radius: 0.5rem; border-radius: 0.5rem;
width: 20rem; width: 20rem;
@ -490,6 +478,11 @@ form.horizontal .input label {
stroke: currentColor; stroke: currentColor;
} }
.flex-center {
display: flex;
align-items: center;
}
@keyframes rotate { @keyframes rotate {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
@ -501,3 +494,4 @@ form.horizontal .input label {
@import "components/dashboard-header"; @import "components/dashboard-header";
@import "components/filters-panel"; @import "components/filters-panel";
@import "components/sensors-page";

View File

@ -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 CancelIcon } from '@/assets/icons/cancel.svg'
export { ReactComponent as FiltersIcon } from '@/assets/icons/filters.svg' export { ReactComponent as FiltersIcon } from '@/assets/icons/filters.svg'
export { ReactComponent as PlusIcon } from '@/assets/icons/plus.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'

View File

@ -1,4 +1,5 @@
import { CancelIcon } from '@/icons' import { CancelIcon } from '@/icons'
import { Link } from 'wouter'
type Props = { type Props = {
shown: boolean shown: boolean
@ -16,7 +17,7 @@ export const UserMenu = ({ shown, onHide }: Props) => {
<nav> <nav>
<a href="#">Dashboards</a> <a href="#">Dashboards</a>
<a href="#">Sensors</a> <Link href="/sensors">Sensors</Link>
<a href="#">Settings</a> <a href="#">Settings</a>
</nav> </nav>
</div> </div>

View File

@ -1,13 +1,25 @@
import { useAppContext } from '@/contexts/AppContext' import { useAppContext } from '@/contexts/AppContext'
import { useHashLocation } from '@/utils/hooks/useHashLocation'
import { Route, Router as Wouter } from 'wouter'
import { NewDashboardPage } from './dashboard/NewDashboardPage' import { NewDashboardPage } from './dashboard/NewDashboardPage'
import { LoginPage } from './login/LoginPage' import { LoginPage } from './login/LoginPage'
import { SensorsPage } from './sensors/SensorsPage'
export const Router = () => { export const Router = () => {
const { loggedIn } = useAppContext() const { loggedIn } = useAppContext()
return ( return (
<> <>
{loggedIn && <NewDashboardPage />} {loggedIn && (
<Wouter hook={useHashLocation}>
<Route path="/">
<NewDashboardPage />
</Route>
<Route path="/sensors">
<SensorsPage />
</Route>
</Wouter>
)}
{!loggedIn && <LoginPage />} {!loggedIn && <LoginPage />}
</> </>
) )

View File

@ -69,8 +69,8 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
onChange={handleChange} onChange={handleChange}
> >
{sensors.data?.map((s) => ( {sensors.data?.map((s) => (
<option key={s.sensor} value={s.sensor}> <option key={s.id} value={s.id}>
{s.config?.name ?? s.sensor} {s.name}
</option> </option>
))} ))}
</select> </select>

View File

@ -132,7 +132,7 @@ export const EditableBox = ({
<div className="box" style={{ height: '100%' }}> <div className="box" style={{ height: '100%' }}>
<div className="header"> <div className="header">
<div className="drag-handle" onMouseDown={handleMouseDown}> <div className="drag-handle" onMouseDown={handleMouseDown}>
<div className="name">{box.title ?? box.sensor ?? ''}</div> <div className="name">{box.title || box.sensor || ''}</div>
</div> </div>
<div className="actions"> <div className="actions">
<div className="action" onClick={() => refreshRef.current?.()}> <div className="action" onClick={() => refreshRef.current?.()}>

View File

@ -25,7 +25,7 @@ export const SensorSettings = ({ sensor, onClose, onUpdate }: Props) => {
try { try {
await Promise.all( await Promise.all(
Object.entries(value).map(([name, value]) => Object.entries(value).map(([name, value]) =>
setSensorConfig({ sensor: sensor.sensor, name, value }) setSensorConfig({ sensor: sensor.id, name, value })
) )
) )
} catch (err) { } catch (err) {
@ -64,7 +64,7 @@ export const SensorSettings = ({ sensor, onClose, onUpdate }: Props) => {
<form onSubmit={handleSave}> <form onSubmit={handleSave}>
<div className="input"> <div className="input">
<label>Sensor</label> <label>Sensor</label>
<input value={sensor.sensor} disabled /> <input value={sensor.id} disabled />
</div> </div>
<div className="input"> <div className="input">
<label>Name</label> <label>Name</label>

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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]
}

View File

@ -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
);

View File

@ -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;

View File

@ -39,10 +39,12 @@ func main() {
// Routes that are only accessible after logging in // Routes that are only accessible after logging in
loginProtected := router.Group("/", middleware.LoginAuthMiddleware(server)) loginProtected := router.Group("/", middleware.LoginAuthMiddleware(server))
loginProtected.GET("/api/sensors", routes.GetSensors(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/latest", routes.GetSensorLatestValue(server))
loginProtected.GET("/api/sensors/:sensor/values", routes.GetSensorValues(server)) loginProtected.GET("/api/sensors/:sensor/values", routes.GetSensorValues(server))
loginProtected.GET("/api/sensors/:sensor/config", routes.GetSensorConfig(server)) //loginProtected.GET("/api/sensors/:sensor/config", routes.GetSensorConfig(server))
loginProtected.PUT("/api/sensors/:sensor/config/:key", routes.PutSensorConfig(server)) //loginProtected.PUT("/api/sensors/:sensor/config/:key", routes.PutSensorConfig(server))
loginProtected.GET("/api/dashboards", routes.GetDashboards(server)) loginProtected.GET("/api/dashboards", routes.GetDashboards(server))
loginProtected.POST("/api/dashboards", routes.PostDashboard(server)) loginProtected.POST("/api/dashboards", routes.PostDashboard(server))
loginProtected.GET("/api/dashboards/:id", routes.GetDashboardById(server)) loginProtected.GET("/api/dashboards/:id", routes.GetDashboardById(server))

View File

@ -3,6 +3,7 @@ package middleware
import ( import (
"basic-sensor-receiver/app" "basic-sensor-receiver/app"
"net/http" "net/http"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -22,10 +23,23 @@ func LoginAuthMiddleware(server *app.Server) gin.HandlerFunc {
} }
func KeyAuthMiddleware(server *app.Server) gin.HandlerFunc { func KeyAuthMiddleware(server *app.Server) gin.HandlerFunc {
keyWithBearer := "Bearer " + server.Config.AuthKey
return func(c *gin.Context) { 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) c.AbortWithStatus(http.StatusUnauthorized)
return return

View File

@ -3,6 +3,7 @@ package routes
import ( import (
"basic-sensor-receiver/app" "basic-sensor-receiver/app"
"net/http" "net/http"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -23,14 +24,20 @@ type getLatestSensorValueQuery struct {
func PostSensorValues(s *app.Server) gin.HandlerFunc { func PostSensorValues(s *app.Server) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
var newValue postSensorValueBody var newValue postSensorValueBody
sensor := c.Param("sensor")
if err := c.BindJSON(&newValue); err != nil { if err := c.BindJSON(&newValue); err != nil {
c.AbortWithError(400, err) c.AbortWithError(400, err)
return 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) c.AbortWithError(400, err)
return return
} }
@ -43,14 +50,19 @@ func GetSensorValues(s *app.Server) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
var query getSensorValuesQuery 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 { if err := c.BindQuery(&query); err != nil {
c.AbortWithError(500, err) c.AbortWithError(500, err)
return 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 { if err != nil {
c.AbortWithError(500, err) c.AbortWithError(500, err)
@ -64,14 +76,19 @@ func GetSensorLatestValue(s *app.Server) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
var query getLatestSensorValueQuery 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 { if err := c.BindQuery(&query); err != nil {
c.AbortWithError(500, err) c.AbortWithError(500, err)
return return
} }
value, err := s.Services.SensorValues.GetLatest(sensor, query.To) value, err := s.Services.SensorValues.GetLatest(sensorId, query.To)
if err != nil { if err != nil {
c.AbortWithError(500, err) c.AbortWithError(500, err)
@ -81,3 +98,9 @@ func GetSensorLatestValue(s *app.Server) gin.HandlerFunc {
c.JSON(http.StatusOK, value) c.JSON(http.StatusOK, value)
} }
} }
func getSensorId(c *gin.Context) (int64, error) {
sensor := c.Param("sensor")
return strconv.ParseInt(sensor, 10, 64)
}

View File

@ -7,15 +7,70 @@ import (
"github.com/gin-gonic/gin" "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 { func GetSensors(s *app.Server) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
sensors, err := s.Services.Sensors.GetList() sensors, err := s.Services.Sensors.GetList()
if err != nil { if err != nil {
c.AbortWithError(500, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
c.JSON(http.StatusOK, sensors) 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)
}
}

View File

@ -11,8 +11,8 @@ type sensorValue struct {
Value float64 `json:"value"` Value float64 `json:"value"`
} }
func (s *SensorValuesService) Push(sensor string, value float64) (int64, error) { func (s *SensorValuesService) Push(sensorId int64, value float64) (int64, error) {
res, err := s.ctx.DB.Exec("INSERT INTO sensor_values (timestamp, sensor, value) VALUES (?, ?, ?)", time.Now().Unix(), sensor, value) res, err := s.ctx.DB.Exec("INSERT INTO sensor_values (timestamp, sensor_id, value) VALUES (?, ?, ?)", time.Now().Unix(), sensorId, value)
if err != nil { if err != nil {
return 0, err return 0, err
@ -21,12 +21,12 @@ func (s *SensorValuesService) Push(sensor string, value float64) (int64, error)
return res.LastInsertId() 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 value float64
var timestamp int64 var timestamp int64
values := make([]sensorValue, 0) 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 { if err != nil {
return nil, err return nil, err
@ -51,10 +51,10 @@ func (s *SensorValuesService) GetList(sensor string, from int64, to int64) ([]se
return values, nil 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{} 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) err := row.Scan(&value.Timestamp, &value.Value)
if err != nil { if err != nil {

View File

@ -1,21 +1,21 @@
package services package services
import "encoding/hex"
type SensorsService struct { type SensorsService struct {
ctx *Context ctx *Context
} }
type sensorItem struct { type SensorItem struct {
Sensor string `json:"sensor"` Id int64 `json:"id"`
LastUpdate string `json:"lastUpdate"` Name string `json:"name"`
Config map[string]string `json:"config"` AuthKey string `json:"authKey"`
} }
func (s *SensorsService) GetList() ([]sensorItem, error) { func (s *SensorsService) GetList() ([]SensorItem, error) {
sensors := make([]sensorItem, 0) sensors := make([]SensorItem, 0)
var sensor string
var lastUpdate string
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 { if err != nil {
return nil, err return nil, err
@ -24,18 +24,14 @@ func (s *SensorsService) GetList() ([]sensorItem, error) {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
err := rows.Scan(&sensor, &lastUpdate) item := SensorItem{}
err := rows.Scan(&item.Id, &item.Name, &item.AuthKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
config, err := s.ctx.Services.SensorConfig.GetValues(sensor) sensors = append(sensors, item)
if err != nil {
return nil, err
}
sensors = append(sensors, sensorItem{Sensor: sensor, LastUpdate: lastUpdate, Config: config})
} }
err = rows.Err() err = rows.Err()
@ -45,3 +41,55 @@ func (s *SensorsService) GetList() ([]sensorItem, error) {
return sensors, nil 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
}