Mill. Take 1.
continuous-integration/drone/push Build is failing

This commit is contained in:
Stian Fredrik Aune
2021-11-07 18:38:32 +01:00
parent 6443e0ecd3
commit 88bd4dc073
17 changed files with 471 additions and 22 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ type Driver struct {
bridges []*Bridge
}
func (d *Driver) SearchBridge(ctx context.Context, address string, dryRun bool) ([]models.Bridge, error) {
func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun bool) ([]models.Bridge, error) {
if address == "" {
if !dryRun {
return nil, models.ErrAddressOnlyDryRunnable
+1 -1
View File
@@ -14,7 +14,7 @@ type Driver struct {
bridges []*Bridge
}
func (d *Driver) SearchBridge(ctx context.Context, address string, _ bool) ([]models.Bridge, error) {
func (d *Driver) SearchBridge(ctx context.Context, address, _ string, _ bool) ([]models.Bridge, error) {
if address == "" {
ifaces, err := net.Interfaces()
if err != nil {
+240
View File
@@ -0,0 +1,240 @@
package mill
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha1"
"encoding/json"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"io"
"log"
"net/http"
"strconv"
"sync"
"time"
)
type bridge struct {
mu sync.Mutex
luciferID int
username string
password string
token string
userId int
mustRefreshBy time.Time
luciferMillIDMap map[int]int
millLuciferIDMap map[int]int
}
func (b *bridge) listDevices(ctx context.Context) ([]models.Device, error) {
err := b.authenticate(ctx)
if err != nil {
return nil, err
}
var shlRes listHomeResBody
err = b.command(ctx, "selectHomeList", listHomeReqBody{}, &shlRes)
if err != nil {
return nil, err
}
devices := make([]millDevice, 0, 16)
for _, home := range shlRes.HomeList {
var gidRes listDeviceResBody
err = b.command(ctx, "getIndependentDevices", listDeviceReqBody{HomeID: home.HomeID}, &gidRes)
if err != nil {
return nil, err
}
devices = append(devices, gidRes.DeviceInfo...)
}
luciferDevices := make([]models.Device, len(devices), len(devices))
for i, device := range devices {
luciferDevices[i] = models.Device{
ID: b.millLuciferIDMap[device.DeviceID],
BridgeID: b.luciferID,
InternalID: fmt.Sprintf("%d", device.DeviceID),
Icon: "heater",
Name: device.DeviceName,
Capabilities: []models.DeviceCapability{models.DCTemperatureControl, models.DCPower},
ButtonNames: nil,
DriverProperties: map[string]interface{}{
"subDomain": fmt.Sprintf("%d", device.SubDomainID),
},
UserProperties: nil,
SceneAssignments: nil,
SceneState: nil,
State: models.DeviceState{
Power: device.PowerStatus > 0,
Temperature: device.HolidayTemp,
},
Tags: nil,
}
}
return luciferDevices, nil
}
func (b *bridge) pushStateChange(ctx context.Context, deviceModel models.Device) error {
b.mu.Lock()
if b.luciferMillIDMap == nil {
b.luciferMillIDMap = make(map[int]int, 4)
b.millLuciferIDMap = make(map[int]int, 4)
}
if b.luciferMillIDMap[deviceModel.ID] == 0 {
millID, _ := strconv.Atoi(deviceModel.InternalID)
b.luciferMillIDMap[deviceModel.ID] = millID
b.millLuciferIDMap[millID] = deviceModel.ID
}
b.mu.Unlock()
status := 0
if deviceModel.State.Power {
status = 1
}
powerReq := deviceControlReqBody{
SubDomain: deviceModel.DriverProperties["subDomain"].(string),
DeviceID: b.luciferMillIDMap[deviceModel.ID],
TestStatus: 1,
Status: status,
}
err := b.command(ctx, "deviceControl", powerReq, nil)
if err != nil {
return err
}
tempReq := changeInfoReqBody{
DeviceID: b.luciferMillIDMap[deviceModel.ID],
Value: deviceModel.State.Temperature,
TimeZoneNum: "+02:00",
Key: "holidayTemp",
}
err = b.command(ctx, "changeDeviceInfo", tempReq, nil)
if err != nil {
return err
}
return nil
}
func (b *bridge) command(ctx context.Context, command string, payload interface{}, target interface{}) error {
err := b.authenticate(ctx)
if err != nil {
return err
}
url := serviceEndpoint + command
method := "POST"
nonce := makeNonce()
timestamp := fmt.Sprintf("%d", time.Now().Unix())
timeout := "300"
h := sha1.New()
h.Write([]byte(timeout))
h.Write([]byte(timestamp))
h.Write([]byte(nonce))
h.Write([]byte(b.token))
signature := fmt.Sprintf("%x", h.Sum(nil))
body, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
if err != nil {
return err
}
addDefaultHeaders(req)
req.Header.Add("X-Zc-Timestamp", timestamp)
req.Header.Add("X-Zc-Timeout", timeout)
req.Header.Add("X-Zc-Nonce", nonce)
req.Header.Add("X-Zc-User-Id", fmt.Sprintf("%d", b.userId))
req.Header.Add("X-Zc-User-Signature", signature)
req.Header.Add("X-Zc-Content-Length", fmt.Sprintf("%d", len(body)))
res, err := http.DefaultClient.Do(req)
if err != nil {
return models.ErrCannotForwardRequest
} else if res.StatusCode != 200 {
return models.ErrIncorrectToken
}
if target == nil {
return nil
}
err = json.NewDecoder(res.Body).Decode(&target)
if err != nil {
return models.ErrUnexpectedResponse
}
return nil
}
func (b *bridge) authenticate(ctx context.Context) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.mustRefreshBy.Before(time.Now().Add(-1 * time.Minute)) {
body, err := json.Marshal(authReqBody{
Account: b.username,
Password: b.password,
})
if err != nil {
return models.ErrMissingToken
}
req, err := http.NewRequestWithContext(ctx, "POST", accountEndpoint + "login", bytes.NewReader(body))
if err != nil {
return models.ErrMissingToken
}
addDefaultHeaders(req)
res, err := http.DefaultClient.Do(req)
if err != nil {
return models.ErrCannotForwardRequest
} else if res.StatusCode != 200 {
return models.ErrIncorrectToken
}
var resBody authResBody
err = json.NewDecoder(res.Body).Decode(&resBody)
if err != nil {
return models.ErrBridgeSearchFailed
}
log.Printf("Mill: Authenticated as %s", resBody.NickName)
b.userId = resBody.UserID
b.token = resBody.Token
b.mustRefreshBy, err = time.ParseInLocation("2006-01-02 15:04:05", resBody.TokenExpire, location)
}
return nil
}
func makeNonce() string {
buf := make([]byte, 8)
_, _ = io.ReadFull(rand.Reader, buf)
return fmt.Sprintf("%x", buf)
}
func addDefaultHeaders(req *http.Request) {
req.Header.Add("Content-Type", "application/x-zc-object")
req.Header.Add("Connection", "Keep-Alive")
req.Header.Add("X-Zc-Major-Domain", "seanywell")
req.Header.Add("X-Zc-Msg-Name", "millService")
req.Header.Add("X-Zc-Sub-Domain", "milltype")
req.Header.Add("X-Zc-Seq-Id", "1")
req.Header.Add("X-Zc-Version", "1")
}
+100
View File
@@ -0,0 +1,100 @@
package mill
import (
"context"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"sync"
"time"
)
type Driver struct {
mu sync.Mutex
bridges map[int]*bridge
}
func (d *Driver) SearchBridge(ctx context.Context, address, token string, _ bool) ([]models.Bridge, error) {
bridgeData := models.Bridge{
Name: fmt.Sprintf("Mill account (%s)", address),
Driver: models.DTMill,
Address: address,
Token: token,
}
b, err := d.ensureBridge(bridgeData)
if err != nil {
return nil, err
}
err = b.authenticate(ctx)
if err != nil {
return nil, err
}
return []models.Bridge{bridgeData}, nil
}
func (d *Driver) SearchDevices(context.Context, models.Bridge, time.Duration) ([]models.Device, error) {
// You would have to configure devices with the Mill app, unfortunately.
return []models.Device{}, nil
}
func (d *Driver) ListDevices(ctx context.Context, bridge models.Bridge) ([]models.Device, error) {
b, err := d.ensureBridge(bridge)
if err != nil {
return nil, err
}
return b.listDevices(ctx)
}
func (d *Driver) Publish(ctx context.Context, bridge models.Bridge, devices []models.Device) error {
b, err := d.ensureBridge(bridge)
if err != nil {
return err
}
for _, device := range devices {
err = b.pushStateChange(ctx, device)
if err != nil {
return err
}
}
return nil
}
func (d *Driver) Run(ctx context.Context, _ models.Bridge, _ chan<- models.Event) error {
// TODO: Maybe do something with the thermostat on the device
for {
select {
case <-ctx.Done():
return ctx.Err()
}
}
}
func (d *Driver) ensureBridge(model models.Bridge) (*bridge, error) {
d.mu.Lock()
defer d.mu.Unlock()
if d.bridges == nil {
d.bridges = make(map[int]*bridge, 4)
}
if d.bridges[model.ID] == nil {
newBridge := &bridge{
luciferID: model.ID,
username: model.Address,
password: model.Token,
}
if model.ID <= 0 {
return newBridge, nil
}
d.bridges[model.ID] = newBridge
}
return d.bridges[model.ID], nil
}
+77
View File
@@ -0,0 +1,77 @@
package mill
import "time"
const accountEndpoint = "https://eurouter.ablecloud.cn:9005/zc-account/v1/"
const serviceEndpoint = "https://eurouter.ablecloud.cn:9005/millService/v1/"
type millHome struct {
HomeID int64 `json:"homeId"`
HomeName string `json:"homeName"`
}
type millDevice struct {
DeviceID int `json:"deviceId"`
DeviceName string `json:"deviceName"`
PowerStatus int `json:"powerStatus"`
HolidayTemp int `json:"holidayTemp"`
CurrentTemp float64 `json:"currentTemp"`
SubDomainID int `json:"subDomainId"`
}
type authReqBody struct {
Account string `json:"account"`
Password string `json:"password"`
}
type authResBody struct {
Token string `json:"token"`
UserID int `json:"userId"`
NickName string `json:"nickName"`
TokenExpire string `json:"tokenExpire"`
}
type listHomeReqBody struct{}
type listHomeResBody struct {
HomeList []millHome `json:"homeList"`
}
type listDeviceReqBody struct {
HomeID int64 `json:"homeId"`
}
type listDeviceResBody struct {
DeviceInfo []millDevice `json:"deviceInfo"`
}
type changeInfoReqBody struct {
HomeType int `json:"homeType"`
DeviceID int `json:"deviceId"`
Value int `json:"value"`
TimeZoneNum string `json:"timeZoneNum"`
Key string `json:"key"`
}
type deviceControlReqBody struct {
SubDomain string `json:"subDomain"`
DeviceID int `json:"deviceId"`
TestStatus int `json:"testStatus"`
Operation int `json:"operation"`
Status int `json:"status"`
WindStatus int `json:"windStatus"`
TempType int `json:"tempType"`
PowerLevel int `json:"powerLevel"`
}
var location *time.Location
func init() {
myLocation, err := time.LoadLocation("Europe/Oslo")
if err != nil {
panic(err.Error())
}
location = myLocation
}
+1 -1
View File
@@ -17,7 +17,7 @@ type Driver struct {
// SearchBridge checks the bridge at the address. If it's not a dry-run, you must hold down the power button
// before calling this function and wait for the pattern.
func (d *Driver) SearchBridge(ctx context.Context, address string, dryRun bool) ([]models.Bridge, error) {
func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun bool) ([]models.Bridge, error) {
res, err := http.Get(fmt.Sprintf("http://%s/device_info", address))
if err != nil {
return nil, err