Browse Source

Merge branch 'asmodeus' of git.aiterp.net:lucifer/new-server into asmodeus

pull/1/head
Gisle Aune 4 years ago
parent
commit
883115bf8b
  1. 4
      app/api/bridges.go
  2. 83
      app/api/presets.go
  3. 15
      app/config/driver.go
  4. 9
      app/config/repo.go
  5. 1
      app/server.go
  6. 13
      cmd/bridgetest/main.go
  7. 2
      cmd/goose/main.go
  8. 14
      internal/drivers/provider.go
  9. 2
      internal/mysql/bridgerepo.go
  10. 99
      internal/mysql/presetrepo.go
  11. 2
      models/bridge.go
  12. 26
      models/colorpreset.go
  13. 1
      models/errors.go
  14. 17
      scripts/20210523175111_color_preset.sql
  15. 11
      scripts/20210523175425_color_preset_name.sql
  16. 2
      webui/package.json
  17. 7
      webui/src/App.tsx
  18. 43
      webui/src/primitives/Forms.tsx
  19. 2
      webui/src/primitives/Layout.tsx
  20. 2
      webui/src/res/colors.sass
  21. 23
      webui/yarn.lock

4
app/api/bridges.go

@ -12,6 +12,10 @@ func Bridges(r gin.IRoutes) {
return config.BridgeRepository().FetchAll(ctxOf(c)) return config.BridgeRepository().FetchAll(ctxOf(c))
})) }))
r.GET("/:id", handler(func(c *gin.Context) (interface{}, error) {
return config.BridgeRepository().Find(ctxOf(c), intParam(c, "id"))
}))
r.POST("", handler(func(c *gin.Context) (interface{}, error) { r.POST("", handler(func(c *gin.Context) (interface{}, error) {
var body struct { var body struct {
Driver models.DriverKind `json:"driver"` Driver models.DriverKind `json:"driver"`

83
app/api/presets.go

@ -0,0 +1,83 @@
package api
import (
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin"
)
func ColorPresets(r gin.IRoutes) {
r.GET("", handler(func(c *gin.Context) (interface{}, error) {
return config.ColorPresetRepository().FetchAll(ctxOf(c))
}))
r.GET("/:id", handler(func(c *gin.Context) (interface{}, error) {
return config.ColorPresetRepository().Find(ctxOf(c), intParam(c, "id"))
}))
r.POST("", handler(func(c *gin.Context) (interface{}, error) {
var body struct {
Name string `json:"name"`
ColorString string `json:"colorString"`
}
err := parseBody(c, &body)
if err != nil {
return nil, err
}
newColor, err := models.ParseColorValue(body.ColorString)
if err != nil {
return nil, err
}
preset := models.ColorPreset{
Name: body.Name,
Value: newColor,
}
preset.Validate()
err = config.ColorPresetRepository().Save(ctxOf(c), &preset)
if err != nil {
return nil, err
}
return preset, nil
}))
r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) {
var body struct {
Name *string `json:"name"`
ColorString *string `json:"colorString"`
}
err := parseBody(c, &body)
if err != nil {
return nil, err
}
preset, err := config.ColorPresetRepository().Find(ctxOf(c), intParam(c, "id"))
if err != nil {
return nil, err
}
if body.Name != nil {
preset.Name = *body.Name
}
if body.ColorString != nil {
newColor, err := models.ParseColorValue(*body.ColorString)
if err != nil {
return nil, err
}
preset.Value = newColor
}
preset.Validate()
err = config.ColorPresetRepository().Save(ctxOf(c), &preset)
if err != nil {
return nil, err
}
return preset, nil
}))
}

15
app/config/driver.go

@ -1,12 +1,23 @@
package config package config
import "git.aiterp.net/lucifer/new-server/models"
import (
"git.aiterp.net/lucifer/new-server/internal/drivers"
"git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf"
"git.aiterp.net/lucifer/new-server/models"
"sync"
)
var dp models.DriverProvider var dp models.DriverProvider
var dpMutex sync.Mutex
func DriverProvider() models.DriverProvider { func DriverProvider() models.DriverProvider {
dpMutex.Lock()
defer dpMutex.Unlock()
if dp == nil { if dp == nil {
panic("not implemented yet")
dp = drivers.DriverMap{
models.DTNanoLeaf: &nanoleaf.Driver{},
}
} }
return dp return dp

9
app/config/repo.go

@ -7,6 +7,7 @@ import (
var ( var (
bRepo models.BridgeRepository bRepo models.BridgeRepository
cpRepo models.ColorPresetRepository
dRepo models.DeviceRepository dRepo models.DeviceRepository
ehRepo models.EventHandlerRepository ehRepo models.EventHandlerRepository
) )
@ -19,6 +20,14 @@ func BridgeRepository() models.BridgeRepository {
return bRepo return bRepo
} }
func ColorPresetRepository() models.ColorPresetRepository {
if cpRepo == nil {
cpRepo = &mysql.ColorPresetRepo{DBX: DBX()}
}
return cpRepo
}
func DeviceRepository() models.DeviceRepository { func DeviceRepository() models.DeviceRepository {
if dRepo == nil { if dRepo == nil {
panic("panik") panic("panik")

1
app/server.go

@ -17,6 +17,7 @@ func StartServer() {
apiGin := ginny.Group("/api") apiGin := ginny.Group("/api")
api.Bridges(apiGin.Group("/bridges")) api.Bridges(apiGin.Group("/bridges"))
api.ColorPresets(apiGin.Group("/color-presets"))
api.DriverKinds(apiGin.Group("/driver-kinds")) api.DriverKinds(apiGin.Group("/driver-kinds"))
api.Events(apiGin.Group("/events")) api.Events(apiGin.Group("/events"))

13
cmd/bridgetest/main.go

@ -5,7 +5,7 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"log" "log"
"os" "os"
@ -15,7 +15,7 @@ import (
"time" "time"
) )
var flagDriver = flag.String("driver", "Nanoleaf", "The bridge driver to use")
var flagDriver = flag.String("driver", string(models.DTNanoLeaf), "The bridge driver to use")
var flagAddress = flag.String("address", "127.0.0.1", "The bridge's address") var flagAddress = flag.String("address", "127.0.0.1", "The bridge's address")
var flagToken = flag.String("token", "", "The bridge's access token / api key / login") var flagToken = flag.String("token", "", "The bridge's access token / api key / login")
var flagPair = flag.Bool("pair", false, "Try to pair with the bridge.") var flagPair = flag.Bool("pair", false, "Try to pair with the bridge.")
@ -25,8 +25,11 @@ var flagSearchTimeout = flag.Duration("search-timeout", time.Second*3, "Timeout
func main() { func main() {
flag.Parse() flag.Parse()
// TODO: Select driver
driver := nanoleaf.Driver{}
// Find drivers
driver, err := config.DriverProvider().Provide(models.DriverKind(*flagDriver))
if err != nil {
log.Fatalln("Failed to find driver:", err)
}
// Find bridge // Find bridge
bridges, err := driver.SearchBridge(context.Background(), *flagAddress, !*flagPair) bridges, err := driver.SearchBridge(context.Background(), *flagAddress, !*flagPair)
@ -62,7 +65,7 @@ func main() {
_ = driver.Publish(context.Background(), bridge, devices) _ = driver.Publish(context.Background(), bridge, devices)
ch := make(chan models.Event)
ch := config.EventChannel
go func() { go func() {
err := driver.Run(context.Background(), bridge, ch) err := driver.Run(context.Background(), bridge, ch)
if err != nil { if err != nil {

2
cmd/goose/main.go

@ -4,11 +4,13 @@ import (
"git.aiterp.net/lucifer/new-server/app/config" "git.aiterp.net/lucifer/new-server/app/config"
"github.com/pressly/goose" "github.com/pressly/goose"
"log" "log"
"time"
) )
func main() { func main() {
db := config.DBX().DB db := config.DBX().DB
log.Printf("Target version: %s",time.Now().Format("20060102150405"))
log.Printf("Database: %s:%d/%s", config.MySqlHost(), config.MySqlPort(), config.MySqlSchema()) log.Printf("Database: %s:%d/%s", config.MySqlHost(), config.MySqlPort(), config.MySqlSchema())
log.Printf("Authenticating as: %s", config.MySqlUsername()) log.Printf("Authenticating as: %s", config.MySqlUsername())

14
internal/drivers/provider.go

@ -0,0 +1,14 @@
package drivers
import "git.aiterp.net/lucifer/new-server/models"
type DriverMap map[models.DriverKind]models.Driver
func (m DriverMap) Provide(kind models.DriverKind) (models.Driver, error) {
if m[kind] == nil {
return nil, models.ErrUnknownDriver
}
return m[kind], nil
}

2
internal/mysql/bridgerepo.go

@ -22,7 +22,7 @@ func (b *BridgeRepo) Find(ctx context.Context, id int) (models.Bridge, error) {
func (b *BridgeRepo) FetchAll(ctx context.Context) ([]models.Bridge, error) { func (b *BridgeRepo) FetchAll(ctx context.Context) ([]models.Bridge, error) {
bridges := make([]models.Bridge, 0, 8) bridges := make([]models.Bridge, 0, 8)
err := b.DBX.GetContext(ctx, bridges, "SELECT * FROM bridge")
err := b.DBX.GetContext(ctx, &bridges, "SELECT * FROM bridge")
if err != nil { if err != nil {
return nil, dbErr(err) return nil, dbErr(err)
} }

99
internal/mysql/presetrepo.go

@ -0,0 +1,99 @@
package mysql
import (
"context"
"git.aiterp.net/lucifer/new-server/models"
"github.com/jmoiron/sqlx"
)
type presetRecord struct {
ID int `db:"id"`
Name string `db:"name"`
Hue float64 `db:"hue"`
Saturation float64 `db:"saturation"`
Kelvin int `db:"kelvin"`
}
type ColorPresetRepo struct {
DBX *sqlx.DB
}
func (c *ColorPresetRepo) Find(ctx context.Context, id int) (models.ColorPreset, error) {
var record presetRecord
err := c.DBX.GetContext(ctx, &record, "SELECT * FROM color_preset WHERE id = ?", id)
if err != nil {
return models.ColorPreset{}, dbErr(err)
}
return c.fromRecords(record)[0], nil
}
func (c *ColorPresetRepo) FetchAll(ctx context.Context) ([]models.ColorPreset, error) {
records := make([]presetRecord, 0, 16)
err := c.DBX.SelectContext(ctx, &records, "SELECT * FROM color_preset")
if err != nil {
return nil, dbErr(err)
}
return c.fromRecords(records...), nil
}
func (c *ColorPresetRepo) Save(ctx context.Context, preset *models.ColorPreset) error {
if preset.ID > 0 {
_, err := c.DBX.ExecContext(
ctx,
"UPDATE color_preset SET name = ?, hue = ?, saturation = ?, kelvin = ? WHERE id = ?",
preset.Name, preset.Value.Hue, preset.Value.Saturation, preset.Value.Kelvin, preset.ID,
)
if err != nil {
return dbErr(err)
}
} else {
rs, err := c.DBX.ExecContext(
ctx,
"INSERT INTO color_preset (name, hue, saturation, kelvin) VALUES (?, ?, ?, ?)",
preset.Name, preset.Value.Hue, preset.Value.Saturation, preset.Value.Kelvin,
)
if err != nil {
return dbErr(err)
}
id, err := rs.LastInsertId()
if err != nil {
return dbErr(err)
}
preset.ID = int(id)
}
return nil
}
func (c *ColorPresetRepo) Delete(ctx context.Context, preset *models.ColorPreset) error {
_, err := c.DBX.ExecContext(ctx, "DELETE FROM color_preset WHERE id = ?", preset.ID)
if err != nil {
return dbErr(err)
}
preset.ID = 0
return nil
}
func (c *ColorPresetRepo) fromRecords(records ...presetRecord) []models.ColorPreset {
newList := make([]models.ColorPreset, len(records), len(records))
for i, record := range records {
newList[i] = models.ColorPreset{
ID: record.ID,
Name: record.Name,
Value: models.ColorValue{
Hue: record.Hue,
Saturation: record.Saturation,
Kelvin: record.Kelvin,
},
}
}
return newList
}

2
models/bridge.go

@ -21,7 +21,7 @@ type DriverKind string
var ( var (
DTHue DriverKind = "Hue" DTHue DriverKind = "Hue"
DTNanoLeaf DriverKind = "NanoLeaf"
DTNanoLeaf DriverKind = "Nanoleaf"
) )
var ValidDriverKinds = []DriverKind{ var ValidDriverKinds = []DriverKind{

26
models/colorpreset.go

@ -0,0 +1,26 @@
package models
import (
"context"
"strings"
)
type ColorPreset struct {
ID int `json:"id"`
Name string `json:"name"`
Value ColorValue `json:"value"`
}
type ColorPresetRepository interface {
Find(ctx context.Context, id int) (ColorPreset, error)
FetchAll(ctx context.Context) ([]ColorPreset, error)
Save(ctx context.Context, preset *ColorPreset) error
Delete(ctx context.Context, preset *ColorPreset) error
}
func (c *ColorPreset) Validate() {
c.Name = strings.Trim(c.Name, " \t\n")
if len(c.Name) == 0 {
c.Name = c.Value.String()
}
}

1
models/errors.go

@ -8,6 +8,7 @@ var ErrBadInput = errors.New("bad input")
var ErrBadColor = errors.New("bad color") var ErrBadColor = errors.New("bad color")
var ErrInternal = errors.New("internal") var ErrInternal = errors.New("internal")
var ErrUnknownColorFormat = errors.New("unknown color format") var ErrUnknownColorFormat = errors.New("unknown color format")
var ErrUnknownDriver = errors.New("unknown driver")
var ErrMissingToken = errors.New("driver is missing authentication information") var ErrMissingToken = errors.New("driver is missing authentication information")
var ErrIncorrectToken = errors.New("driver is not accepting authentication information") var ErrIncorrectToken = errors.New("driver is not accepting authentication information")

17
scripts/20210523175111_color_preset.sql

@ -0,0 +1,17 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE color_preset
(
id INT NOT NULL AUTO_INCREMENT,
hue DOUBLE NOT NULL,
saturation DOUBLE NOT NULL,
kelvin INT NOT NULL,
PRIMARY KEY (id)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE color_preset;
-- +goose StatementEnd

11
scripts/20210523175425_color_preset_name.sql

@ -0,0 +1,11 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE color_preset
ADD COLUMN name VARCHAR(255) NOT NULL DEFAULT 'Unnamed';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE color_preset
DROP COLUMN name;
-- +goose StatementEnd

2
webui/package.json

@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@jaames/iro": "^5.5.1",
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
@ -15,6 +16,7 @@
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"react-semantic-ui-range": "^0.7.1",
"typescript": "^4.1.2", "typescript": "^4.1.2",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },

7
webui/src/App.tsx

@ -1,9 +1,14 @@
import React from 'react'; import React from 'react';
import {HookRouter, navigate, usePath, useRoutes} from "hookrouter"; import {HookRouter, navigate, usePath, useRoutes} from "hookrouter";
import {Tabs} from "./primitives/Layout"; import {Tabs} from "./primitives/Layout";
import {HSColorPicker} from "./primitives/Forms";
const routeObj: HookRouter.RouteObject = { const routeObj: HookRouter.RouteObject = {
"/": () => <div>1</div>,
"/": () => (
<div>
<HSColorPicker h={60} s={1} onChange={() => void(0)}/>
</div>
),
"/devices": () => <div>2</div>, "/devices": () => <div>2</div>,
"/settings": () => <div>3</div>, "/settings": () => <div>3</div>,
} }

43
webui/src/primitives/Forms.tsx

@ -0,0 +1,43 @@
import React, {useLayoutEffect, useState} from 'react';
// @ts-ignore
import iro from "@jaames/iro";
interface ColorPickerProps {
h: number
s: number
onChange: (h: number, v: number) => void
}
const randomId = () => Math.floor(Math.random() * 100000);
export const HSColorPicker: React.FC<ColorPickerProps> = ({h, s, onChange}) => {
const [random] = useState(() => `color-picker-${randomId()}`);
useLayoutEffect(() => {
// @ts-ignore
const colorPicker = new iro.ColorPicker(`#${random}`, {
color: {h, s: s * 100, v: 255},
layout: [
{
component: iro.ui.Wheel,
options: {}
}
],
});
colorPicker.on("input:end", (color: { hsv: { h: number, s: number } }) => {
onChange(color.hsv.h || 0, (color.hsv.s || 0) / 100);
});
return () => {
const elem = document.getElementById(`color-picker-${random}`);
if (elem === null) {
return;
}
elem.innerHTML = "";
};
}, [h, s, onChange, random]);
return <div id={random} style={{margin: "0 auto"}}/>;
};

2
webui/src/primitives/Layout.tsx

@ -1,4 +1,4 @@
import React, {useCallback, useEffect, useMemo} from "react";
import React, {useCallback, useEffect} from "react";
import "./Layout.sass"; import "./Layout.sass";
interface TabsProps { interface TabsProps {

2
webui/src/res/colors.sass

@ -1,5 +1,5 @@
$color-background: #111 $color-background: #111
$color-foreground: rgb(238, 238, 238)
$color-foreground: rgb(204, 204, 204)
$color-foreground-dark: rgba(238, 238, 238, 0.05) $color-foreground-dark: rgba(238, 238, 238, 0.05)
body, html body, html

23
webui/yarn.lock

@ -1209,6 +1209,11 @@
dependencies: dependencies:
"@hapi/hoek" "^8.3.0" "@hapi/hoek" "^8.3.0"
"@irojs/iro-core@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@irojs/iro-core/-/iro-core-1.2.0.tgz#3587c2db7a6de09f76dbf75b94605ac251039ca8"
integrity sha512-RVxd4lEx3KPXRBYo7urLSr9C3PJsm3t9AgPQppP5LDqQcMrXCXcZoeneMtQndMk60k72NziNiPpbZlZIvcT4VQ==
"@istanbuljs/load-nyc-config@^1.0.0": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@ -1225,6 +1230,14 @@
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
"@jaames/iro@^5.5.1":
version "5.5.1"
resolved "https://registry.yarnpkg.com/@jaames/iro/-/iro-5.5.1.tgz#1935713fe3cca8c4eadcfc6013b6f846ac8be097"
integrity sha512-HR2phfXjEINFKXxEdbGqp9/MfHfQ/xP+nLi2gjNV4RHB3tBOgHMpS1WFMU7fp1vl//vWXxKxVKa91D+5JoFgMA==
dependencies:
"@irojs/iro-core" "^1.2.0"
preact "^10.0.0"
"@jest/console@^26.6.2": "@jest/console@^26.6.2":
version "26.6.2" version "26.6.2"
resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2"
@ -9070,6 +9083,11 @@ postcss@^8.1.0:
nanoid "^3.1.20" nanoid "^3.1.20"
source-map "^0.6.1" source-map "^0.6.1"
preact@^10.0.0:
version "10.5.13"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.13.tgz#85f6c9197ecd736ce8e3bec044d08fd1330fa019"
integrity sha512-q/vlKIGNwzTLu+jCcvywgGrt+H/1P/oIRSD6mV4ln3hmlC+Aa34C7yfPI4+5bzW8pONyVXYS7SvXosy2dKKtWQ==
prelude-ls@^1.2.1: prelude-ls@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@ -9449,6 +9467,11 @@ react-scripts@4.0.3:
optionalDependencies: optionalDependencies:
fsevents "^2.1.3" fsevents "^2.1.3"
react-semantic-ui-range@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/react-semantic-ui-range/-/react-semantic-ui-range-0.7.1.tgz#bc552f889e4243d500d4673dd7966752664935fe"
integrity sha512-6AQLuFeARHcIO7yl4Pd0BBIB9sJyeNK1mku7luK6aTXrj9EGaqr1fWS7w4b0+GUP7CvkixmcyYqykxzpKS4Lcw==
react@^17.0.2: react@^17.0.2:
version "17.0.2" version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"

Loading…
Cancel
Save