Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions cmd/client/api/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ import (
webHandlerTY "github.com/mycontroller-org/server/v2/pkg/types/web_handler"
)

func (c *Client) ExecuteNodeAction(action string, ids []string) error {
return c.executeIDAction(API_ACTION_NODE, action, ids)
// ExecuteNodeAction sends an action to one or more nodes.
func (c *Client) ExecuteNodeAction(action string, ids []string, payload ...string) error {
var pl string
if len(payload) > 0 {
pl = payload[0]
}
return c.executeIDAction(API_ACTION_NODE, action, pl, ids)
}

func (c *Client) ExecuteGatewayAction(action string, ids []string) error {
return c.executeIDAction(API_ACTION_GATEWAY, action, ids)
return c.executeIDAction(API_ACTION_GATEWAY, action, "", ids)
}

func (c *Client) executeIDAction(api, action string, ids []string) error {
func (c *Client) executeIDAction(api, action, payload string, ids []string) error {
if action == "" {
return fmt.Errorf("action is required")
}
Expand All @@ -26,6 +31,9 @@ func (c *Client) executeIDAction(api, action string, ids []string) error {
"action": action,
"id": ids,
}
if payload != "" {
query["payload"] = payload
}
_, err := c.executeJson(api, http.MethodGet, nil, query, nil, http.StatusOK)
return err
}
Expand Down
40 changes: 34 additions & 6 deletions cmd/client/command/action/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ var actionCmd = &cobra.Command{
Short: "Send an action to a node or gateway",
Long: `Send a server action to a node or gateway.

Node actions: reboot, reset, firmware-update, heartbeat, refresh-node-info
Node actions: reboot, reset, firmware-update, heartbeat, refresh-node-info, set-sleep-duration:SENSOR_ID:PERCENT, set-sleep-duration:PERCENT
Gateway actions: discover-nodes

myc action node <alias> reboot mysensor.1
myc action node <alias> set-sleep-duration:206:32 mysensor.1
myc action node <alias> set-sleep-duration:32 mysensor.1
myc action gateway <alias> discover-nodes mysensor
`,
SilenceUsage: true,
Expand All @@ -42,7 +44,9 @@ var nodeActionCmd = &cobra.Command{
myc action node <alias> reset mysensor.1
myc action node <alias> firmware-update mysensor.1
myc action node <alias> heartbeat mysensor.1
myc action node <alias> refresh-node-info mysensor.1`,
myc action node <alias> refresh-node-info mysensor.1
myc action node <alias> set-sleep-duration:206:32 mysensor.1
myc action node <alias> set-sleep-duration:32 mysensor.1`,
Args: cobra.MinimumNArgs(3),
SilenceUsage: true,
SilenceErrors: true,
Expand All @@ -51,14 +55,36 @@ var nodeActionCmd = &cobra.Command{
},
RunE: func(cmd *cobra.Command, args []string) error {
client, rest := rootCmd.TakeAlias(args)
action, err := normalizeNodeAction(rest[0])

// Support "action" and "action:payload"
raw := rest[0]
actionStr := raw
payload := ""
if idx := strings.Index(raw, ":"); idx >= 0 {
actionStr = raw[:idx]
payload = raw[idx+1:]
}

action, err := normalizeNodeAction(actionStr)
if err != nil {
return err
}

// Payload is required for some actions
if action == nodeTY.ActionSetSleepDuration && payload == "" {
return fmt.Errorf("action %q requires a payload (e.g. set-sleep-duration:32 or set-sleep-duration:206:32)", actionStr)
}

ids, resolveErr := client.ResolveNodeIDs(rest[1:])
if len(ids) > 0 {
if err := client.ExecuteNodeAction(action, ids); err != nil {
return err
if payload != "" {
if err := client.ExecuteNodeAction(action, ids, payload); err != nil {
return err
}
} else {
if err := client.ExecuteNodeAction(action, ids); err != nil {
return err
}
}
_, _ = fmt.Fprintf(rootCmd.IOStreams.Out, "sent %s to %d node(s)\n", action, len(ids))
}
Expand Down Expand Up @@ -106,8 +132,10 @@ func normalizeNodeAction(action string) (string, error) {
return nodeTY.ActionHeartbeatRequest, nil
case "refresh-node-info", "refresh":
return nodeTY.ActionRefreshNodeInfo, nil
case "set-sleep-duration":
return nodeTY.ActionSetSleepDuration, nil
default:
return "", fmt.Errorf("unsupported node action %q (supported: reboot, reset, firmware-update, heartbeat, refresh-node-info)", action)
return "", fmt.Errorf("unsupported node action %q (supported: reboot, reset, firmware-update, heartbeat, refresh-node-info, set-sleep-duration)", action)
}
}

Expand Down
19 changes: 10 additions & 9 deletions cmd/client/command/action/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ import (

func TestNormalizeNodeAction(t *testing.T) {
cases := map[string]string{
"reboot": nodeTY.ActionReboot,
"reset": nodeTY.ActionReset,
"firmware-update": nodeTY.ActionFirmwareUpdate,
"firmware_update": nodeTY.ActionFirmwareUpdate,
"heartbeat": nodeTY.ActionHeartbeatRequest,
"heartbeat-request": nodeTY.ActionHeartbeatRequest,
"refresh": nodeTY.ActionRefreshNodeInfo,
"refresh-node-info": nodeTY.ActionRefreshNodeInfo,
"refresh_node_info": nodeTY.ActionRefreshNodeInfo,
"reboot": nodeTY.ActionReboot,
"reset": nodeTY.ActionReset,
"firmware-update": nodeTY.ActionFirmwareUpdate,
"firmware_update": nodeTY.ActionFirmwareUpdate,
"heartbeat": nodeTY.ActionHeartbeatRequest,
"heartbeat-request": nodeTY.ActionHeartbeatRequest,
"refresh": nodeTY.ActionRefreshNodeInfo,
"refresh-node-info": nodeTY.ActionRefreshNodeInfo,
"refresh_node_info": nodeTY.ActionRefreshNodeInfo,
"set-sleep-duration": nodeTY.ActionSetSleepDuration,
}
for in, want := range cases {
got, err := normalizeNodeAction(in)
Expand Down
17 changes: 12 additions & 5 deletions pkg/api/action/actions_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import (
)

// ExecuteNodeAction for a node
func (a *ActionAPI) ExecuteNodeAction(action string, nodeIDs []string) error {
func (a *ActionAPI) ExecuteNodeAction(action, payload string, nodeIDs []string) error {
// verify is a valid action?
switch action {
case nodeTY.ActionFirmwareUpdate,
nodeTY.ActionHeartbeatRequest,
nodeTY.ActionReboot,
nodeTY.ActionRefreshNodeInfo,
nodeTY.ActionReset:
nodeTY.ActionReset,
nodeTY.ActionSetSleepDuration:
// nothing to do, just continue
default:
return fmt.Errorf("invalid node action:%s", action)
Expand All @@ -29,15 +30,18 @@ func (a *ActionAPI) ExecuteNodeAction(action string, nodeIDs []string) error {
}
for index := 0; index < len(nodes); index++ {
node := nodes[index]
err = a.toNode(&node, node.GatewayID, node.NodeID, action)
err = a.toNode(&node, node.GatewayID, node.NodeID, action, payload)
if err != nil {
a.logger.Error("error on sending an action to a node", zap.Error(err), zap.String("gateway", node.GatewayID), zap.String("node", node.NodeID))
a.logger.Error("error on sending an action to a node",
zap.Error(err),
zap.String("gateway", node.GatewayID),
zap.String("node", node.NodeID))
}
}
return nil
}

func (a *ActionAPI) toNode(node *nodeTY.Node, gatewayID, nodeID, action string) error {
func (a *ActionAPI) toNode(node *nodeTY.Node, gatewayID, nodeID, action, payload string) error {
msg := msgTY.NewMessage(false)
msg.GatewayID = gatewayID
msg.NodeID = nodeID
Expand All @@ -58,6 +62,9 @@ func (a *ActionAPI) toNode(node *nodeTY.Node, gatewayID, nodeID, action string)
}
pl := msgTY.NewPayload()
pl.Key = action
if payload != "" {
pl.SetValue(payload)
}
msg.Payloads = append(msg.Payloads, pl)
msg.Type = msgTY.TypeAction
return a.Post(&msg)
Expand Down
4 changes: 2 additions & 2 deletions pkg/api/action/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (a *ActionAPI) ExecuteActionOnResourceByQuickID(data *handlerTY.ResourceDat
case quickIdUtils.QuickIdNode:
gatewayID := kvMap[types.KeyGatewayID]
nodeID := kvMap[types.KeyNodeID]
return a.toNode(nil, gatewayID, nodeID, data.Payload)
return a.toNode(nil, gatewayID, nodeID, data.Payload, "")

case quickIdUtils.QuickIdSource:
// no action needed
Expand Down Expand Up @@ -120,7 +120,7 @@ func (a *ActionAPI) ExecuteActionOnResourceByLabels(data *handlerTY.ResourceData
items := result.Data.(*[]nodeTY.Node)
for index := 0; index < len(*items); index++ {
item := (*items)[index]
err = a.toNode(&item, item.GatewayID, item.NodeID, data.Payload)
err = a.toNode(&item, item.GatewayID, item.NodeID, data.Payload, "")
if err != nil {
a.logger.Error("error on sending data", zap.Error(err), zap.String("nodeID", item.ID), zap.String("payload", data.Payload))
}
Expand Down
8 changes: 7 additions & 1 deletion pkg/http_router/routes/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ func (h *Routes) executeNodeAction(w http.ResponseWriter, r *http.Request) {
return
}

err = h.action.ExecuteNodeAction(actionArr[0], idsArr)
// Optional payload for actions that require one (e.g. SetSleepDuration)
payload := ""
if payloadArr := query[keyPayload]; len(payloadArr) > 0 {
payload = payloadArr[0]
}

err = h.action.ExecuteNodeAction(actionArr[0], payload, idsArr)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand Down
1 change: 1 addition & 0 deletions pkg/types/node/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
ActionRefreshNodeInfo = "refresh_node_info"
ActionReset = "reset"
ActionAwake = "awake"
ActionSetSleepDuration = "SetSleepDuration"
)

// Node struct
Expand Down
50 changes: 48 additions & 2 deletions plugin/gateway/provider/mysensors_v2/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,30 @@ func (p *Provider) handleActions(gwCfg *gwTY.Config, fn string, msg *msgTY.Messa
msMsg.Payload = p.getTimestamp(gwCfg)
msMsg.Type = actionTime

case nodeTY.ActionFirmwareUpdate, "ST_FIRMWARE_CONFIG_REQUEST":
case nodeTY.ActionFirmwareUpdate:
// User-triggered action: optionally reboot the node before serving
// the OTA config. Required for MYSBootloader-based nodes.
if err := p.maybeSendRebootForOta(msg); err != nil {
p.logger.Error("firmware-update: pre-OTA reboot failed",
zap.String("nodeId", msg.NodeID),
zap.Error(err),
)
// Continue: the config will be served anyway.
}
pl, err := p.executeFirmwareConfigRequest(msg)
if err != nil {
return err
}
if pl == "" {
return nil
}
msMsg.Command = cmdStream
msMsg.Type = actionFirmwareConfigResponse
msMsg.Payload = strings.ToUpper(pl)

case "ST_FIRMWARE_CONFIG_REQUEST":
// The node already asked for its config (it is in bootloader mode).
// No reboot needed.
pl, err := p.executeFirmwareConfigRequest(msg)
if err != nil {
return err
Expand All @@ -99,13 +122,36 @@ func (p *Provider) handleActions(gwCfg *gwTY.Config, fn string, msg *msgTY.Messa
msMsg.Type = actionFirmwareResponse
msMsg.Payload = strings.ToUpper(pl)

case nodeTY.ActionSetSleepDuration:
raw := strings.TrimSpace(msg.Payloads[0].Value.String())
sensorID, pct, err := parseSleepDurationPayload(raw)
if err != nil {
return err
}
node, err := p.getNode(msg.GatewayID, msg.NodeID)
if err != nil {
return err
}
if sensorID == "" {
sensorID = node.Labels.Get(LabelSleepCommandSensorID)
}
if sensorID == "" {
return fmt.Errorf("no sleep command sensor configured for node %s; run once: myc action node set-sleep-duration:<sensor_id>:%d %s",
node.NodeID, pct, msg.GatewayID+"."+msg.NodeID)
}
if sensorID != node.Labels.Get(LabelSleepCommandSensorID) {
node.Labels.Set(LabelSleepCommandSensorID, sensorID)
p.setNodeLabels(node)
}
return p.sendSleepCommand(node, sensorID, pct, "")

default:
return fmt.Errorf("this function is not implemented: %s", fn)
}
return nil
}

// geTimestamp returns timestamp in seconds from 1970
// getTimestamp returns timestamp in seconds from 1970
// adds zone offset to the actual timestamp
// user can specify different timezone as a gateway label
// if non set, take system timezone
Expand Down
Loading
Loading