1use crate::llm::actions::{
2 protocol_trait::{ActionResult, Protocol, Server},
3 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
4};
5use crate::protocol::log_template::LogTemplate;
6use crate::protocol::{EventType, SpawnContext};
7use crate::state::app_state::AppState;
8use anyhow::{bail, Context, Result};
9use serde_json::{json, Value};
10use std::sync::LazyLock;
11
12use super::wire;
13
14#[derive(Default)]
15pub struct KnxProtocol;
16impl KnxProtocol {
17 pub fn new() -> Self {
18 Self
19 }
20}
21
22pub fn parameter(name: &str, kind: &str, description: &str, required: bool) -> Parameter {
23 Parameter {
24 name: name.into(),
25 type_hint: kind.into(),
26 description: description.into(),
27 required,
28 }
29}
30
31pub fn action(
32 name: &str,
33 description: &str,
34 parameters: Vec<Parameter>,
35 example: Value,
36 log: &str,
37) -> ActionDefinition {
38 ActionDefinition {
39 name: name.into(),
40 description: description.into(),
41 parameters,
42 example,
43 log_template: Some(LogTemplate::new().with_info(log)),
44 }
45}
46
47fn value_params() -> Vec<Parameter> {
48 vec![
49 parameter("value", "any", "The value: true/false, a number or text, as its DPT takes", true),
50 parameter("dpt", "string", &format!("Datapoint type, e.g. 9.001; may be omitted when group_types configures the address. Supported: {}", wire::DPT_NOTE), false),
51 ]
52}
53
54fn response_action() -> ActionDefinition {
55 action(
56 "knx_group_response",
57 "Answer the group read with the address's current value (sent on the bus from this gateway's address).",
58 value_params(),
59 json!({"type":"knx_group_response","value":21.5,"dpt":"9.001"}),
60 "-> KNX response {value}",
61 )
62}
63
64pub fn write_action() -> ActionDefinition {
65 let mut params = vec![parameter(
66 "group_address",
67 "string",
68 "Group address main/middle/sub, e.g. 1/2/3",
69 true,
70 )];
71 params.extend(value_params());
72 action(
73 "knx_group_write",
74 "Put a GroupValueWrite on the bus to every tunnel (e.g. an actuator's status feedback).",
75 params,
76 json!({"type":"knx_group_write","group_address":"1/2/10","value":true,"dpt":"1"}),
77 "-> KNX write {group_address} = {value}",
78 )
79}
80
81fn ignore_action() -> ActionDefinition {
82 action(
83 "knx_ignore",
84 "No device reacts to this telegram; nothing further goes on the bus.",
85 vec![],
86 json!({"type":"knx_ignore"}),
87 "-> KNX ignore",
88 )
89}
90
91fn telegram_params() -> Vec<Parameter> {
92 vec![
93 parameter(
94 "source",
95 "string",
96 "Individual address of the sender (the tunnel's)",
97 true,
98 ),
99 parameter(
100 "destination",
101 "string",
102 "Group address, main/middle/sub",
103 true,
104 ),
105 parameter(
106 "dpt",
107 "string",
108 "The DPT configured for this address, if any",
109 false,
110 ),
111 parameter(
112 "value",
113 "any",
114 "The value decoded by that DPT, when one is configured",
115 false,
116 ),
117 parameter(
118 "interpretations",
119 "object",
120 "Every reading the value's size allows, keyed by DPT, when none is configured",
121 true,
122 ),
123 ]
124}
125
126pub static TELEGRAM_EVENT: LazyLock<EventType> = LazyLock::new(|| {
127 let mut params = vec![parameter(
128 "kind",
129 "string",
130 "write, or response (a device answering a read)",
131 true,
132 )];
133 params.extend(telegram_params());
134 EventType::new(
135 "knx_group_telegram",
136 "A tunnel put a group value on the bus (GroupValueWrite or GroupValueResponse). Other tunnels already received it.",
137 ignore_action().example.clone(),
138 )
139 .with_parameters(params)
140 .with_actions(vec![ignore_action(), write_action()])
141});
142
143pub static READ_EVENT: LazyLock<EventType> = LazyLock::new(|| {
144 EventType::new(
145 "knx_group_read",
146 "A tunnel asked for a group address's value (GroupValueRead). Answer with knx_group_response, or stay silent if no device holds it.",
147 response_action().example.clone(),
148 )
149 .with_parameters(vec![
150 parameter("source", "string", "Individual address of the reader", true),
151 parameter("destination", "string", "Group address read, main/middle/sub", true),
152 parameter("dpt", "string", "The DPT configured for this address, if any", false),
153 ])
154 .with_actions(vec![response_action(), ignore_action(), write_action()])
155});
156
157pub fn value_of(action: &Value, configured: Option<&str>) -> Result<wire::Data> {
159 let dpt = action["dpt"]
160 .as_str()
161 .or(configured)
162 .context("dpt required: the address has no configured type")?;
163 wire::encode(dpt, &action["value"])
164}
165
166impl Protocol for KnxProtocol {
167 fn protocol_name(&self) -> &'static str {
168 "KNX/IP"
169 }
170 fn stack_name(&self) -> &'static str {
171 "ETH>IP>UDP>KNXnet/IP"
172 }
173 fn description(&self) -> &'static str {
174 "KNXnet/IP tunnelling gateway: tunnels connect, group writes are routed between them, and the handler plays every device on the bus"
175 }
176 fn keywords(&self) -> Vec<&'static str> {
177 vec![
178 "knx",
179 "knxnet/ip",
180 "knx/ip",
181 "eib",
182 "building automation",
183 "knx tunnelling",
184 ]
185 }
186 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
187 vec![]
188 }
189 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
190 vec![response_action(), write_action(), ignore_action()]
191 }
192 fn get_event_types(&self) -> Vec<EventType> {
193 vec![TELEGRAM_EVENT.clone(), READ_EVENT.clone()]
194 }
195 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
196 vec![
197 ParameterDefinition {
198 name: "group_types".into(),
199 type_hint: "object".into(),
200 description: "DPT per group address, e.g. {\"1/2/3\": \"1\", \"1/2/4\": \"9.001\"}: values on those addresses are decoded and encoded by it".into(),
201 required: false,
202 example: json!({"1/2/3": "1", "1/2/4": "9.001"}),
203 default: None,
204 },
205 ParameterDefinition {
206 name: "individual_address".into(),
207 type_hint: "string".into(),
208 description: "This gateway's own individual address, the source of the telegrams it puts on the bus".into(),
209 required: false,
210 example: json!("1.1.250"),
211 default: Some(json!(super::DEFAULT_ADDRESS)),
212 },
213 ParameterDefinition {
214 name: "max_tunnels".into(),
215 type_hint: "number".into(),
216 description: "How many tunnelling connections at once (1..=64)".into(),
217 required: false,
218 example: json!(8),
219 default: Some(json!(super::DEFAULT_MAX_TUNNELS)),
220 },
221 ]
222 }
223 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
224 use crate::protocol::metadata::*;
225 ProtocolMetadataV2::builder()
226 .state(DevelopmentState::Experimental)
227 .privilege_requirement(PrivilegeRequirement::None)
228 .well_known_udp_port(3671)
229 .implementation("Hand-written KNXnet/IP over Tokio UDP: search, description, connect (tunnel, link layer), connection state, disconnect, tunnelling requests and acks with sequence checking; cEMI L_Data.req/con/ind; group telegrams routed between tunnels; values decoded by DPT")
230 .llm_control("Every group read's answer and any telegram a device sends in reaction; the handler is every device on the bus")
231 .e2e_testing("tests/server/knx: raw frames and bounds; xknx (Python) and knxd (C++, connecting as a tunnel client with -b ipt:, driven by knxtool) as independent clients")
232 .notes("Tunnelling only: no routing (multicast), no device management, no secure tunnelling, no tunnelling v2 over TCP. Telegrams NetGet sends are not retransmitted. Frames are capped at 512 bytes; a tunnel silent for 120 s is dropped, as the specification's heartbeat requires. A handler failure on a read sends nothing — an invented value would be a device that does not exist.")
233 .deliberately_silent()
234 .max_inbound_bytes(wire::MAX_FRAME)
235 .build()
236 }
237 fn example_prompt(&self) -> &'static str {
238 "KNX/IP gateway where 1/2/3 is a light switch and 1/2/4 a temperature of 21.5 °C"
239 }
240 fn get_startup_examples(&self) -> StartupExamples {
241 let llm = json!({"type":"open_server","base_stack":"knx","port":3671,"startup_params":{"group_types":{"1/2/3":"1","1/2/4":"9.001"}},"instruction":"1/2/3 is a light; 1/2/4 reads 21.5 °C"});
242 let mut scripted = llm.clone();
243 scripted["event_handlers"] = json!([{"event_pattern":"knx_group_read","handler":{"type":"script","language":"python","code":"import json,sys\ne=json.load(sys.stdin)['event']\na={'type':'knx_group_response','value':21.5} if e['destination']=='1/2/4' else {'type':'knx_ignore'}\nprint(json.dumps({'actions':[a]}))"}}]);
244 let mut static_example = llm.clone();
245 static_example["event_handlers"] = json!([{"event_pattern":"knx_group_read","handler":{"type":"static","actions":[{"type":"knx_group_response","value":21.5,"dpt":"9.001"}]}}]);
246 StartupExamples::new(llm, scripted, static_example)
247 }
248 fn group_name(&self) -> &'static str {
249 "Industrial"
250 }
251}
252
253impl Server for KnxProtocol {
254 fn spawn(
255 &self,
256 ctx: SpawnContext,
257 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
258 {
259 Box::pin(super::spawn(ctx))
260 }
261 fn execute_action(&self, v: Value) -> Result<ActionResult> {
262 let name = v["type"].as_str().unwrap_or_default().to_string();
263 match name.as_str() {
264 "knx_ignore" => {}
265 "knx_group_response" => {
266 if let Some(dpt) = v["dpt"].as_str() {
267 wire::encode(dpt, &v["value"])?;
268 }
269 }
270 "knx_group_write" => {
271 wire::parse_group(
272 v["group_address"]
273 .as_str()
274 .context("group_address required")?,
275 )?;
276 if let Some(dpt) = v["dpt"].as_str() {
277 wire::encode(dpt, &v["value"])?;
278 }
279 }
280 _ => bail!("Unknown KNX/IP server action"),
281 }
282 Ok(ActionResult::Custom { name, data: v })
283 }
284}