99 prepare_client ,
1010 apply_loglevel ,
1111 close_client_quietly ,
12+ extract_device_from_topic ,
13+ _match_from_metrics ,
1214)
1315
1416logger = logging .getLogger (__name__ )
1719)
1820
1921
20- def run (arguments : str | list [str ]) -> None :
22+ def run (arguments : str | list [str ], topic : str | None = None ) -> None :
2123 """Run set coil operation handler
22- Expected arguments (JSON):
23- {
24- "input": false,
25- "address": < Fieldbusaddress >,
26- "coil": < coilnumber >,
27- "value": < 0 | 1 >
28- }
29- Parse JSON payload"""
30- payload = parse_json_arguments (arguments )
3124
32- # Create context with default config directory
33- context = Context ()
25+ Supports two payload formats:
26+
27+ 1. Legacy format (full coil details):
28+ {
29+ "input": false,
30+ "address": < Fieldbusaddress >,
31+ "coil": < coilnumber >,
32+ "value": < 0 | 1 >
33+ }
3434
35- # Load configs and set log level
35+ 2. New format (coilid-based):
36+ {
37+ "timestamp": "2025-09-23T00:00:00Z",
38+ "uuid": "device-id",
39+ "metrics": [{
40+ "name": "<coilid>_xxxxxxxx",
41+ "timestamp": "2025-09-23T01:00:00Z",
42+ "value": 0 or 1
43+ }]
44+ }
45+ Requires topic: te/device/<device-id>///cmd/modbus_SetCoil/<mapper-id>
46+ """
47+ payload = parse_json_arguments (arguments )
48+ context = Context ()
3649 modbus_config = context .base_config
3750 apply_loglevel (logger , modbus_config )
38- logger .info ("New set coil operation. args=%s" , arguments )
39-
40- try :
41- slave_id = int (payload ["address" ]) # Fieldbus address
42- coil_number = int (payload ["coil" ]) # Coil address
43- value_int = int (payload ["value" ]) # 0 or 1
44- except KeyError as err :
45- raise ValueError (f"Missing required field: { err } " ) from err
46- except (TypeError , ValueError ) as err :
47- raise ValueError (f"Invalid numeric field: { err } " ) from err
51+ logger .info ("New set coil operation" )
4852
49- if value_int not in (0 , 1 ):
50- raise ValueError ("value must be 0 or 1 for a coil write" )
53+ if "metrics" in payload and topic :
54+ slave_id , coil_number , value_int , ip_address = _process_new_format_coil (
55+ payload , topic , context
56+ )
57+ else :
58+ slave_id , coil_number , value_int , ip_address = _process_legacy_format_coil (
59+ payload
60+ )
5161
5262 # Prepare client (resolve target, backfill defaults, build client)
5363 client = prepare_client (
54- payload [ "ipAddress" ] ,
64+ ip_address ,
5565 slave_id ,
5666 context .config_dir / "devices.toml" ,
5767 modbus_config ,
@@ -74,3 +84,72 @@ def run(arguments: str | list[str]) -> None:
7484 raise
7585 finally :
7686 close_client_quietly (client )
87+
88+
89+ def _process_new_format_coil (payload : dict , topic : str , context ):
90+ """Process new format coil payload."""
91+ logger .info ("Processing new format payload with metrics array" )
92+ device_name = extract_device_from_topic (topic )
93+ if not device_name :
94+ raise ValueError (f"Could not extract device name from topic: { topic } " )
95+
96+ coil_config , target_device , coil_id , write_value = _match_coil_from_metrics (
97+ payload , device_name , context .config_dir / "devices.toml"
98+ )
99+
100+ if not coil_config :
101+ raise ValueError ("Could not match any coil for metrics in payload" )
102+ if not target_device :
103+ raise ValueError (f"Could not find device '{ device_name } ' in devices.toml" )
104+
105+ logger .info ("Matched CoilID: %s, Value: %s" , coil_id , write_value )
106+ coil_num = coil_config .get ("number" )
107+ if coil_num is None :
108+ raise ValueError (
109+ f"Coil configuration missing 'number' field for coilid '{ coil_id } '"
110+ )
111+
112+ value_int = int (write_value )
113+ if value_int not in (0 , 1 ):
114+ raise ValueError ("Coil value must be 0 or 1" )
115+
116+ return (
117+ target_device .get ("address" ),
118+ coil_num ,
119+ value_int ,
120+ target_device .get ("ip" , "" ),
121+ )
122+
123+
124+ def _process_legacy_format_coil (payload : dict ):
125+ """Process legacy format coil payload."""
126+ logger .info ("Processing legacy format payload" )
127+ try :
128+ slave_id = int (payload ["address" ])
129+ coil_number = int (payload ["coil" ])
130+ value_int = int (payload ["value" ])
131+ except KeyError as err :
132+ raise ValueError (f"Missing required field: { err } " ) from err
133+ except (TypeError , ValueError ) as err :
134+ raise ValueError (f"Invalid numeric field: { err } " ) from err
135+
136+ if value_int not in (0 , 1 ):
137+ raise ValueError ("value must be 0 or 1 for a coil write" )
138+
139+ return slave_id , coil_number , value_int , payload .get ("ipAddress" , "" )
140+
141+
142+ def _match_coil_from_metrics (payload : dict , device_name : str , devices_path ):
143+ """Match coil from devices.toml by checking if metric name starts with coilid.
144+
145+ Args:
146+ payload: Payload containing metrics array
147+ device_name: Name of the device to search
148+ devices_path: Path to devices.toml
149+
150+ Returns:
151+ Tuple of (coil_config, target_device, coilid, value)
152+ """
153+ return _match_from_metrics (
154+ payload , device_name , devices_path , "coils" , "coilid" , int
155+ )
0 commit comments