294 lines
7.8 KiB
Go
294 lines
7.8 KiB
Go
package mijia
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
)
|
|
|
|
type Home struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
UID int64 `json:"uid"`
|
|
RoomList json.RawMessage `json:"roomlist,omitempty"`
|
|
Extra map[string]json.RawMessage `json:"-"`
|
|
}
|
|
|
|
func (home *Home) UnmarshalJSON(payload []byte) error {
|
|
type homeFields struct {
|
|
ID json.RawMessage `json:"id"`
|
|
Name string `json:"name"`
|
|
UID int64 `json:"uid"`
|
|
RoomList json.RawMessage `json:"roomlist"`
|
|
}
|
|
var fields homeFields
|
|
if err := json.Unmarshal(payload, &fields); err != nil {
|
|
return err
|
|
}
|
|
id, err := decodeStringOrNumber(fields.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("decode home id: %w", err)
|
|
}
|
|
home.ID = id
|
|
home.Name = fields.Name
|
|
home.UID = fields.UID
|
|
home.RoomList = fields.RoomList
|
|
home.Extra, err = decodeExtraFields(payload, "id", "name", "uid", "roomlist")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (home Home) MarshalJSON() ([]byte, error) {
|
|
type homeFields Home
|
|
return encodeJSONWithExtra(homeFields(home), home.Extra, "id", "name", "uid", "roomlist")
|
|
}
|
|
|
|
type Device struct {
|
|
DID string `json:"did"`
|
|
Name string `json:"name"`
|
|
Model string `json:"model"`
|
|
UID int64 `json:"uid"`
|
|
Owner bool `json:"owner"`
|
|
HomeID string `json:"home_id,omitempty"`
|
|
Extra map[string]json.RawMessage `json:"-"`
|
|
properties map[string]PropertySpec
|
|
actions map[string]ActionSpec
|
|
client *Client
|
|
delay time.Duration
|
|
}
|
|
|
|
func (device *Device) UnmarshalJSON(payload []byte) error {
|
|
type deviceFields Device
|
|
var fields deviceFields
|
|
if err := json.Unmarshal(payload, &fields); err != nil {
|
|
return err
|
|
}
|
|
*device = Device(fields)
|
|
extra, err := decodeExtraFields(payload, "did", "name", "model", "uid", "owner", "home_id")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
device.Extra = extra
|
|
return nil
|
|
}
|
|
|
|
func (device Device) MarshalJSON() ([]byte, error) {
|
|
type deviceFields Device
|
|
return encodeJSONWithExtra(deviceFields(device), device.Extra, "did", "name", "model", "uid", "owner", "home_id")
|
|
}
|
|
|
|
type Scene struct {
|
|
SceneID string `json:"scene_id"`
|
|
Name string `json:"name"`
|
|
HomeID string `json:"home_id,omitempty"`
|
|
Extra map[string]json.RawMessage `json:"-"`
|
|
}
|
|
|
|
func (scene *Scene) UnmarshalJSON(payload []byte) error {
|
|
type sceneFields Scene
|
|
var fields sceneFields
|
|
if err := json.Unmarshal(payload, &fields); err != nil {
|
|
return err
|
|
}
|
|
*scene = Scene(fields)
|
|
extra, err := decodeExtraFields(payload, "scene_id", "name", "home_id")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
scene.Extra = extra
|
|
return nil
|
|
}
|
|
|
|
func (scene Scene) MarshalJSON() ([]byte, error) {
|
|
type sceneFields Scene
|
|
return encodeJSONWithExtra(sceneFields(scene), scene.Extra, "scene_id", "name", "home_id")
|
|
}
|
|
|
|
type Consumable struct {
|
|
DID string `json:"did"`
|
|
Name string `json:"name"`
|
|
Details json.RawMessage `json:"details"`
|
|
HomeID string `json:"home_id,omitempty"`
|
|
Extra map[string]json.RawMessage `json:"-"`
|
|
}
|
|
|
|
func (consumable *Consumable) UnmarshalJSON(payload []byte) error {
|
|
type consumableFields Consumable
|
|
var fields consumableFields
|
|
if err := json.Unmarshal(payload, &fields); err != nil {
|
|
return err
|
|
}
|
|
*consumable = Consumable(fields)
|
|
extra, err := decodeExtraFields(payload, "did", "name", "details", "home_id")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
consumable.Extra = extra
|
|
return nil
|
|
}
|
|
|
|
func (consumable Consumable) MarshalJSON() ([]byte, error) {
|
|
type consumableFields Consumable
|
|
return encodeJSONWithExtra(consumableFields(consumable), consumable.Extra, "did", "name", "details", "home_id")
|
|
}
|
|
|
|
type PropertyRequest struct {
|
|
DID string `json:"did"`
|
|
SIID int `json:"siid"`
|
|
PIID int `json:"piid"`
|
|
}
|
|
|
|
type PropertySetRequest struct {
|
|
DID string `json:"did"`
|
|
SIID int `json:"siid"`
|
|
PIID int `json:"piid"`
|
|
Value any `json:"value"`
|
|
}
|
|
|
|
type PropertyResult struct {
|
|
DID string `json:"did"`
|
|
SIID int `json:"siid"`
|
|
PIID int `json:"piid"`
|
|
Value any `json:"value,omitempty"`
|
|
Code int `json:"code"`
|
|
UpdateTime int64 `json:"updateTime,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
type ActionRequest struct {
|
|
DID string `json:"did"`
|
|
SIID int `json:"siid"`
|
|
AIID int `json:"aiid"`
|
|
Value any `json:"value,omitempty"`
|
|
Extra map[string]any `json:"-"`
|
|
}
|
|
|
|
// MarshalJSON 先序列化固定字段,再合并 Extra;Extra 与保留键
|
|
// (did, siid, aiid, value) 冲突时返回错误。
|
|
func (request ActionRequest) MarshalJSON() ([]byte, error) {
|
|
type actionRequestFields ActionRequest
|
|
payload, err := json.Marshal(actionRequestFields(request))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(request.Extra) == 0 {
|
|
return payload, nil
|
|
}
|
|
var combined map[string]json.RawMessage
|
|
if err := json.Unmarshal(payload, &combined); err != nil {
|
|
return nil, err
|
|
}
|
|
for key, value := range request.Extra {
|
|
if actionRequestReservedKey(key) {
|
|
return nil, fmt.Errorf("无效的参数: %s. 请勿使用保留键 (did, siid, aiid, value)", key)
|
|
}
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("编码扩展参数 %s: %w", key, err)
|
|
}
|
|
combined[key] = encoded
|
|
}
|
|
return json.Marshal(combined)
|
|
}
|
|
|
|
func actionRequestReservedKey(key string) bool {
|
|
switch key {
|
|
case "did", "siid", "aiid", "value":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type ActionResult struct {
|
|
DID string `json:"did"`
|
|
SIID int `json:"siid"`
|
|
AIID int `json:"aiid"`
|
|
Code int `json:"code"`
|
|
Out json.RawMessage `json:"out,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
type StatisticsRequest struct {
|
|
DID string `json:"did"`
|
|
Key string `json:"key"`
|
|
DataType string `json:"data_type"`
|
|
Limit int `json:"limit"`
|
|
TimeStart int64 `json:"time_start"`
|
|
TimeEnd int64 `json:"time_end"`
|
|
}
|
|
|
|
func decodeStringOrNumber(raw json.RawMessage) (string, error) {
|
|
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
|
|
return "", nil
|
|
}
|
|
var value string
|
|
if err := json.Unmarshal(raw, &value); err == nil {
|
|
return value, nil
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.UseNumber()
|
|
var number json.Number
|
|
if err := decoder.Decode(&number); err != nil {
|
|
return "", err
|
|
}
|
|
return number.String(), nil
|
|
}
|
|
|
|
func decodeJSON(payload []byte, target any) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(payload))
|
|
decoder.UseNumber()
|
|
if err := decoder.Decode(target); err != nil {
|
|
return err
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return fmt.Errorf("unexpected trailing JSON value")
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decodeExtraFields(payload []byte, knownFields ...string) (map[string]json.RawMessage, error) {
|
|
var extra map[string]json.RawMessage
|
|
if err := json.Unmarshal(payload, &extra); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, field := range knownFields {
|
|
delete(extra, field)
|
|
}
|
|
if len(extra) == 0 {
|
|
return nil, nil
|
|
}
|
|
return extra, nil
|
|
}
|
|
|
|
func encodeJSONWithExtra(fields any, extra map[string]json.RawMessage, knownFields ...string) ([]byte, error) {
|
|
payload, err := json.Marshal(fields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(extra) == 0 {
|
|
return payload, nil
|
|
}
|
|
var combined map[string]json.RawMessage
|
|
if err := json.Unmarshal(payload, &combined); err != nil {
|
|
return nil, err
|
|
}
|
|
reserved := make(map[string]struct{}, len(knownFields))
|
|
for _, field := range knownFields {
|
|
reserved[field] = struct{}{}
|
|
}
|
|
for field, value := range extra {
|
|
if _, known := reserved[field]; !known {
|
|
combined[field] = value
|
|
}
|
|
}
|
|
return json.Marshal(combined)
|
|
}
|