Stian Fredrik Aune
4 years ago
25 changed files with 24454 additions and 11560 deletions
-
88app/api/bridges.go
-
30app/api/util.go
-
2app/config/channels.go
-
14app/config/repo.go
-
3app/server.go
-
3app/services/events.go
-
46app/services/publish.go
-
4cmd/goose/main.go
-
78internal/mysql/bridgerepo.go
-
17internal/mysql/util.go
-
19models/bridge.go
-
2models/errors.go
-
9models/event.go
-
20scripts/20210522140146_device.sql
-
17scripts/20210522140147_device_property.sql
-
20scripts/20210522140148_device_state.sql
-
91webui/package.json
-
57webui/src/App.tsx
-
20webui/src/index.css
-
34webui/src/index.tsx
-
17webui/src/primitives/Layout.sass
-
45webui/src/primitives/Layout.tsx
-
7webui/src/res/colors.sass
-
11959webui/yarn-error.log
-
23412webui/yarn.lock
@ -1 +1,89 @@ |
|||||
package api |
package api |
||||
|
|
||||
|
import ( |
||||
|
"git.aiterp.net/lucifer/new-server/app/config" |
||||
|
"git.aiterp.net/lucifer/new-server/models" |
||||
|
"github.com/gin-gonic/gin" |
||||
|
"log" |
||||
|
) |
||||
|
|
||||
|
func Bridges(r gin.IRoutes) { |
||||
|
r.GET("", handler(func(c *gin.Context) (interface{}, error) { |
||||
|
return config.BridgeRepository().FetchAll(ctxOf(c)) |
||||
|
})) |
||||
|
|
||||
|
r.POST("", handler(func(c *gin.Context) (interface{}, error) { |
||||
|
var body struct { |
||||
|
Driver models.DriverKind `json:"driver"` |
||||
|
Address string `json:"address"` |
||||
|
DryRun bool `json:"dryRun"` |
||||
|
} |
||||
|
err := parseBody(c, &body) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
driver, err := config.DriverProvider().Provide(body.Driver) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
bridges, err := driver.SearchBridge(ctxOf(c), body.Address, body.DryRun) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
if !body.DryRun { |
||||
|
for _, bridge := range bridges { |
||||
|
err := config.BridgeRepository().Save(ctxOf(c), &bridge) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
log.Printf("Saved new bridge: %s (%s)", bridge.Name, bridge.Address) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return bridges, nil |
||||
|
})) |
||||
|
|
||||
|
r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) { |
||||
|
var body struct { |
||||
|
Name *string `json:"name"` |
||||
|
} |
||||
|
err := parseBody(c, &body) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
bridge, err := config.BridgeRepository().Find(ctxOf(c), intParam(c, "id")) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
if body.Name != nil { |
||||
|
bridge.Name = *body.Name |
||||
|
} |
||||
|
|
||||
|
err = config.BridgeRepository().Save(ctxOf(c), &bridge) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return bridge, nil |
||||
|
})) |
||||
|
|
||||
|
r.DELETE("/:id", handler(func(c *gin.Context) (interface{}, error) { |
||||
|
bridge, err := config.BridgeRepository().Find(ctxOf(c), intParam(c, "id")) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
err = config.BridgeRepository().Delete(ctxOf(c), &bridge) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return bridge, nil |
||||
|
})) |
||||
|
} |
@ -1 +1,47 @@ |
|||||
package services |
package services |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"git.aiterp.net/lucifer/new-server/app/config" |
||||
|
"git.aiterp.net/lucifer/new-server/models" |
||||
|
"log" |
||||
|
) |
||||
|
|
||||
|
func StartPublisher() { |
||||
|
ctx := context.Background() |
||||
|
|
||||
|
go func() { |
||||
|
for devices := range config.PublishChannel { |
||||
|
if len(devices) == 0 { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
// Emergency solution! Please avoid!
|
||||
|
// Send devices not belonging to the first channel separately
|
||||
|
bridgeID := devices[0].BridgeID |
||||
|
for _, device := range devices { |
||||
|
if device.BridgeID != bridgeID { |
||||
|
config.PublishChannel<-[]models.Device{device} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bridge, err := config.BridgeRepository().Find(ctx, devices[0].BridgeID) |
||||
|
if err != nil { |
||||
|
log.Println("Publishing error (1): " + err.Error()) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
driver, err := config.DriverProvider().Provide(bridge.Driver) |
||||
|
if err != nil { |
||||
|
log.Println("Publishing error (2): " + err.Error()) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
err = driver.Publish(ctx, bridge, devices) |
||||
|
if err != nil { |
||||
|
log.Println("Publishing error (3): " + err.Error()) |
||||
|
continue |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
} |
@ -1 +1,79 @@ |
|||||
package mysql |
package mysql |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"git.aiterp.net/lucifer/new-server/models" |
||||
|
"github.com/jmoiron/sqlx" |
||||
|
) |
||||
|
|
||||
|
type BridgeRepo struct { |
||||
|
DBX *sqlx.DB |
||||
|
} |
||||
|
|
||||
|
func (b *BridgeRepo) Find(ctx context.Context, id int) (models.Bridge, error) { |
||||
|
var bridge models.Bridge |
||||
|
err := b.DBX.GetContext(ctx, &bridge, "SELECT * FROM bridge WHERE id = ?", id) |
||||
|
if err != nil { |
||||
|
return models.Bridge{}, dbErr(err) |
||||
|
} |
||||
|
|
||||
|
return bridge, nil |
||||
|
} |
||||
|
|
||||
|
func (b *BridgeRepo) FetchAll(ctx context.Context) ([]models.Bridge, error) { |
||||
|
bridges := make([]models.Bridge, 0, 8) |
||||
|
err := b.DBX.GetContext(ctx, bridges, "SELECT * FROM bridge") |
||||
|
if err != nil { |
||||
|
return nil, dbErr(err) |
||||
|
} |
||||
|
|
||||
|
return bridges, nil |
||||
|
} |
||||
|
|
||||
|
func (b *BridgeRepo) Save(ctx context.Context, bridge *models.Bridge) error { |
||||
|
if bridge.ID > 0 { |
||||
|
_, err := b.DBX.ExecContext( |
||||
|
ctx, |
||||
|
"UPDATE bridge SET name = ?, address = ?, token = ? WHERE id = ?", |
||||
|
bridge.Name, bridge.Address, bridge.Token, bridge.ID, |
||||
|
) |
||||
|
|
||||
|
if err != nil { |
||||
|
return dbErr(err) |
||||
|
} |
||||
|
} else { |
||||
|
rs, err := b.DBX.ExecContext( |
||||
|
ctx, |
||||
|
"INSERT INTO bridge (name, driver, address, token) VALUES (?, ?, ?, ?)", |
||||
|
bridge.Name, bridge.Driver, bridge.Address, bridge.Token, |
||||
|
) |
||||
|
|
||||
|
if err != nil { |
||||
|
return dbErr(err) |
||||
|
} |
||||
|
|
||||
|
id, err := rs.LastInsertId() |
||||
|
if err != nil { |
||||
|
return dbErr(err) |
||||
|
} |
||||
|
|
||||
|
bridge.ID = int(id) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (b *BridgeRepo) Delete(ctx context.Context, bridge *models.Bridge) error { |
||||
|
_, err := b.DBX.ExecContext(ctx, "DELETE FROM bridge WHERE id = ?", bridge.ID) |
||||
|
if err != nil { |
||||
|
return dbErr(err) |
||||
|
} |
||||
|
|
||||
|
_, err = b.DBX.ExecContext(ctx, "DELETE FROM device WHERE bridge_id = ?", bridge.ID) |
||||
|
if err != nil { |
||||
|
return dbErr(err) |
||||
|
} |
||||
|
|
||||
|
bridge.ID = 0 |
||||
|
return nil |
||||
|
} |
@ -1 +1,18 @@ |
|||||
package mysql |
package mysql |
||||
|
|
||||
|
import ( |
||||
|
"database/sql" |
||||
|
"git.aiterp.net/lucifer/new-server/models" |
||||
|
"log" |
||||
|
) |
||||
|
|
||||
|
func dbErr(err error) error { |
||||
|
if err == sql.ErrNoRows { |
||||
|
return models.ErrNotFound |
||||
|
} else if err != nil { |
||||
|
log.Printf("Internal error: %s", err.Error()) |
||||
|
return models.ErrInternal |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
@ -0,0 +1,20 @@ |
|||||
|
-- +goose Up |
||||
|
-- +goose StatementBegin |
||||
|
CREATE TABLE device |
||||
|
( |
||||
|
id INT NOT NULL AUTO_INCREMENT, |
||||
|
bridge_id INT NOT NULL, |
||||
|
internal_id VARCHAR(255) NOT NULL, |
||||
|
icon CHAR NOT NULL, |
||||
|
name VARCHAR(255) NOT NULL, |
||||
|
capabilities VARCHAR(255) NOT NULL, |
||||
|
button_names VARCHAR(255) NOT NULL, |
||||
|
|
||||
|
PRIMARY KEY (id) |
||||
|
); |
||||
|
-- +goose StatementEnd |
||||
|
|
||||
|
-- +goose Down |
||||
|
-- +goose StatementBegin |
||||
|
DROP TABLE device; |
||||
|
-- +goose StatementEnd |
@ -0,0 +1,17 @@ |
|||||
|
-- +goose Up |
||||
|
-- +goose StatementBegin |
||||
|
CREATE TABLE device_property |
||||
|
( |
||||
|
device_id INT NOT NULL, |
||||
|
prop_key VARCHAR(255) NOT NULL, |
||||
|
prop_value VARCHAR(255) NOT NULL, |
||||
|
is_user TINYINT NOT NULL DEFAULT 0, |
||||
|
|
||||
|
PRIMARY KEY (device_id, prop_key) |
||||
|
); |
||||
|
-- +goose StatementEnd |
||||
|
|
||||
|
-- +goose Down |
||||
|
-- +goose StatementBegin |
||||
|
DROP TABLE device_property; |
||||
|
-- +goose StatementEnd |
@ -0,0 +1,20 @@ |
|||||
|
-- +goose Up |
||||
|
-- +goose StatementBegin |
||||
|
CREATE TABLE device_state |
||||
|
( |
||||
|
device_id INT NOT NULL, |
||||
|
hue DOUBLE NOT NULL, |
||||
|
saturation DOUBLE NOT NULL, |
||||
|
kelvin INT NOT NULL, |
||||
|
power TINYINT NOT NULL, |
||||
|
intensity INT NOT NULL, |
||||
|
|
||||
|
PRIMARY KEY (device_id) |
||||
|
); |
||||
|
|
||||
|
-- +goose StatementEnd |
||||
|
|
||||
|
-- +goose Down |
||||
|
-- +goose StatementBegin |
||||
|
DROP TABLE device_state; |
||||
|
-- +goose StatementEnd |
@ -1,43 +1,48 @@ |
|||||
{ |
|
||||
"name": "webui", |
|
||||
"version": "0.1.0", |
|
||||
"private": true, |
|
||||
"dependencies": { |
|
||||
"@testing-library/jest-dom": "^5.11.4", |
|
||||
"@testing-library/react": "^11.1.0", |
|
||||
"@testing-library/user-event": "^12.1.10", |
|
||||
"@types/jest": "^26.0.15", |
|
||||
"@types/node": "^12.0.0", |
|
||||
"@types/react": "^17.0.0", |
|
||||
"@types/react-dom": "^17.0.0", |
|
||||
"react": "^17.0.2", |
|
||||
"react-dom": "^17.0.2", |
|
||||
"react-scripts": "4.0.3", |
|
||||
"typescript": "^4.1.2", |
|
||||
"web-vitals": "^1.0.1" |
|
||||
}, |
|
||||
"scripts": { |
|
||||
"start": "react-scripts start", |
|
||||
"build": "react-scripts build", |
|
||||
"test": "react-scripts test", |
|
||||
"eject": "react-scripts eject" |
|
||||
}, |
|
||||
"eslintConfig": { |
|
||||
"extends": [ |
|
||||
"react-app", |
|
||||
"react-app/jest" |
|
||||
] |
|
||||
}, |
|
||||
"browserslist": { |
|
||||
"production": [ |
|
||||
">0.2%", |
|
||||
"not dead", |
|
||||
"not op_mini all" |
|
||||
], |
|
||||
"development": [ |
|
||||
"last 1 chrome version", |
|
||||
"last 1 firefox version", |
|
||||
"last 1 safari version" |
|
||||
] |
|
||||
} |
|
||||
} |
|
||||
|
{ |
||||
|
"name": "webui", |
||||
|
"version": "0.1.0", |
||||
|
"private": true, |
||||
|
"dependencies": { |
||||
|
"@testing-library/jest-dom": "^5.11.4", |
||||
|
"@testing-library/react": "^11.1.0", |
||||
|
"@testing-library/user-event": "^12.1.10", |
||||
|
"@types/jest": "^26.0.15", |
||||
|
"@types/node": "^12.0.0", |
||||
|
"@types/react": "^17.0.0", |
||||
|
"@types/react-dom": "^17.0.0", |
||||
|
"hookrouter": "^1.2.5", |
||||
|
"node-sass": "4.14.1", |
||||
|
"react": "^17.0.2", |
||||
|
"react-dom": "^17.0.2", |
||||
|
"react-scripts": "4.0.3", |
||||
|
"typescript": "^4.1.2", |
||||
|
"web-vitals": "^1.0.1" |
||||
|
}, |
||||
|
"scripts": { |
||||
|
"start": "react-scripts start", |
||||
|
"build": "react-scripts build", |
||||
|
"test": "react-scripts test", |
||||
|
"eject": "react-scripts eject" |
||||
|
}, |
||||
|
"eslintConfig": { |
||||
|
"extends": [ |
||||
|
"react-app", |
||||
|
"react-app/jest" |
||||
|
] |
||||
|
}, |
||||
|
"browserslist": { |
||||
|
"production": [ |
||||
|
">0.2%", |
||||
|
"not dead", |
||||
|
"not op_mini all" |
||||
|
], |
||||
|
"development": [ |
||||
|
"last 1 chrome version", |
||||
|
"last 1 firefox version", |
||||
|
"last 1 safari version" |
||||
|
] |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@types/hookrouter": "^2.2.5" |
||||
|
} |
||||
|
} |
@ -1,26 +1,31 @@ |
|||||
import React from 'react'; |
|
||||
import logo from './logo.svg'; |
|
||||
import './App.css'; |
|
||||
|
|
||||
function App() { |
|
||||
return ( |
|
||||
<div className="App"> |
|
||||
<header className="App-header"> |
|
||||
<img src={logo} className="App-logo" alt="logo" /> |
|
||||
<p> |
|
||||
Edit <code>src/App.tsx</code> and save to reload. |
|
||||
</p> |
|
||||
<a |
|
||||
className="App-link" |
|
||||
href="https://reactjs.org" |
|
||||
target="_blank" |
|
||||
rel="noopener noreferrer" |
|
||||
> |
|
||||
Learn React |
|
||||
</a> |
|
||||
</header> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export default App; |
|
||||
|
import React from 'react'; |
||||
|
import {HookRouter, navigate, usePath, useRoutes} from "hookrouter"; |
||||
|
import {Tabs} from "./primitives/Layout"; |
||||
|
|
||||
|
const routeObj: HookRouter.RouteObject = { |
||||
|
"/": () => <div>1</div>, |
||||
|
"/devices": () => <div>2</div>, |
||||
|
"/settings": () => <div>3</div>, |
||||
|
} |
||||
|
|
||||
|
const routeList = ["/", "/devices", "/settings"]; |
||||
|
|
||||
|
const tabNames = ["Lucifer", "Enheter", "Oppsett"]; |
||||
|
|
||||
|
function App() { |
||||
|
const route = useRoutes(routeObj); |
||||
|
const path = usePath(); |
||||
|
|
||||
|
return ( |
||||
|
<div className="App"> |
||||
|
<Tabs tabNames={tabNames} |
||||
|
index={routeList.indexOf(path)} |
||||
|
onChange={i => navigate(routeList[i])} |
||||
|
boldIndex={0} |
||||
|
/> |
||||
|
{route || <div>B</div>} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default App; |
@ -1,13 +1,7 @@ |
|||||
body { |
|
||||
margin: 0; |
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', |
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', |
|
||||
sans-serif; |
|
||||
-webkit-font-smoothing: antialiased; |
|
||||
-moz-osx-font-smoothing: grayscale; |
|
||||
} |
|
||||
|
|
||||
code { |
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', |
|
||||
monospace; |
|
||||
} |
|
||||
|
body, html { |
||||
|
margin: 0; |
||||
|
font-family: sans-serif; |
||||
|
-webkit-font-smoothing: antialiased; |
||||
|
-moz-osx-font-smoothing: grayscale; |
||||
|
padding: 0; |
||||
|
} |
@ -1,17 +1,17 @@ |
|||||
import React from 'react'; |
|
||||
import ReactDOM from 'react-dom'; |
|
||||
import './index.css'; |
|
||||
import App from './App'; |
|
||||
import reportWebVitals from './reportWebVitals'; |
|
||||
|
|
||||
ReactDOM.render( |
|
||||
<React.StrictMode> |
|
||||
<App /> |
|
||||
</React.StrictMode>, |
|
||||
document.getElementById('root') |
|
||||
); |
|
||||
|
|
||||
// If you want to start measuring performance in your app, pass a function
|
|
||||
// to log results (for example: reportWebVitals(console.log))
|
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
||||
reportWebVitals(); |
|
||||
|
import React from 'react'; |
||||
|
import ReactDOM from 'react-dom'; |
||||
|
import './index.css'; |
||||
|
import App from './App'; |
||||
|
import reportWebVitals from "./reportWebVitals"; |
||||
|
|
||||
|
ReactDOM.render( |
||||
|
<React.StrictMode> |
||||
|
<App /> |
||||
|
</React.StrictMode>, |
||||
|
document.getElementById('root') |
||||
|
); |
||||
|
|
||||
|
// If you want to start measuring performance in your app, pass a function
|
||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
|
reportWebVitals(); |
@ -0,0 +1,17 @@ |
|||||
|
@import "../res/colors" |
||||
|
|
||||
|
.Tabs-container |
||||
|
background-color: $color-foreground-dark |
||||
|
|
||||
|
.Tabs-element |
||||
|
display: inline-block |
||||
|
padding: 0.25em 0.7ch |
||||
|
cursor: pointer |
||||
|
user-select: none |
||||
|
|
||||
|
.Tabs-active |
||||
|
border-bottom: 1px solid $color-foreground |
||||
|
|
||||
|
.Tabs-bold |
||||
|
font-family: 'Bitstream Vera Serif', 'Lucida Fax', serif |
||||
|
font-weight: 600 |
@ -0,0 +1,45 @@ |
|||||
|
import React, {useCallback, useEffect, useMemo} from "react"; |
||||
|
import "./Layout.sass"; |
||||
|
|
||||
|
interface TabsProps { |
||||
|
tabNames: string[] |
||||
|
index: number |
||||
|
onChange: (newIndex: number) => void |
||||
|
boldIndex?: number |
||||
|
} |
||||
|
|
||||
|
export function Tabs({tabNames, index, onChange, boldIndex}: TabsProps) { |
||||
|
useEffect(() => { |
||||
|
if (index < 0) { |
||||
|
onChange(0); |
||||
|
} else if (tabNames.length > 0 && index >= tabNames.length) { |
||||
|
onChange(tabNames.length - 1); |
||||
|
} |
||||
|
}, [tabNames, index, onChange]); |
||||
|
|
||||
|
const tabClass = useCallback((i: number) => { |
||||
|
const classes = ['Tabs-element']; |
||||
|
|
||||
|
if (i === index) { |
||||
|
classes.push("Tabs-active"); |
||||
|
} |
||||
|
|
||||
|
if (i === boldIndex) { |
||||
|
classes.push("Tabs-bold"); |
||||
|
} |
||||
|
|
||||
|
return classes.join(" "); |
||||
|
}, [index, boldIndex]) |
||||
|
|
||||
|
return ( |
||||
|
<div className="Tabs-container"> |
||||
|
{tabNames.map((name, i) => ( |
||||
|
<div className={tabClass(i)} |
||||
|
onClick={() => onChange(i)} |
||||
|
> |
||||
|
{name} |
||||
|
</div> |
||||
|
))} |
||||
|
</div> |
||||
|
); |
||||
|
} |
@ -0,0 +1,7 @@ |
|||||
|
$color-background: #111 |
||||
|
$color-foreground: rgb(238, 238, 238) |
||||
|
$color-foreground-dark: rgba(238, 238, 238, 0.05) |
||||
|
|
||||
|
body, html |
||||
|
background-color: $color-background |
||||
|
color: $color-foreground |
11959
webui/yarn-error.log
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
23412
webui/yarn.lock
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue