Skip to content

Commit c137332

Browse files
authored
Aprontest fixes (#18)
* Add support for older firmware & zigbee On_Off as a string So weird. * add changelog entry * eh? * fix the new extension * cargo fmt * Add uint16/uint32 types as well * forgot the fmt * Update CHANGELOG.md
1 parent 87c6b3e commit c137332

File tree

6 files changed

+260
-26
lines changed

6 files changed

+260
-26
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
## Next
2+
- Add support for older version of firmware that doesn't report Gang data.
3+
- Add support for STRING-based properties & discovery of STRING-based On_Off switches.
4+
- Add support for UInt16/32-based properties in parsing.
25

36
## 0.1.3
47
- Report errors more readably better when things fail

src/controller.rs

Lines changed: 163 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,31 @@ pub struct ShortDevice {
2020

2121
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2222
pub enum AttributeType {
23-
UInt8,
2423
Bool,
24+
String,
25+
UInt8,
26+
UInt16,
27+
UInt32,
2528
}
2629

27-
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30+
#[derive(Clone, Debug, Eq, PartialEq)]
2831
pub enum AttributeValue {
2932
NoValue,
30-
UInt8(u8),
3133
Bool(bool),
34+
String(String),
35+
UInt8(u8),
36+
UInt16(u16),
37+
UInt32(u32),
3238
}
3339

3440
impl AttributeType {
3541
pub fn parse(&self, s: &str) -> Result<AttributeValue, Box<dyn Error>> {
3642
let payload_str = s.trim();
3743
Ok(match self {
3844
AttributeType::UInt8 => AttributeValue::UInt8(payload_str.parse::<u8>()?),
45+
AttributeType::UInt16 => AttributeValue::UInt16(payload_str.parse::<u16>()?),
46+
AttributeType::UInt32 => AttributeValue::UInt32(payload_str.parse::<u32>()?),
47+
AttributeType::String => AttributeValue::String(payload_str.to_string()),
3948
AttributeType::Bool => {
4049
AttributeValue::Bool(match payload_str.to_ascii_lowercase().as_str() {
4150
"true" | "1" | "yes" | "on" => true,
@@ -141,7 +150,7 @@ lazy_static! {
141150
static ref ATTRIBUTE_REGEX_STR: String = r"\s*(?P<id>\d+)\s*\|\s*(?P<description>[^\|]+)\s*\|\s*(?P<type>[^ ]+)\s*\|\s*(?P<mode>[^ ]+)\s*\|\s*(?P<get>[^ ]*)\s*\| *(?P<set>[^\n ]*)".to_owned();
142151
static ref LONG_DEVICE_REGEX : Regex = Regex::new(&((
143152
"".to_owned() +
144-
r"(?ms)Gang ID: (?P<gang_id>(0x)?[0-9a-fA-F]+)\n" +
153+
r"(?ms)(?:Gang ID: (?P<gang_id>(0x)?[0-9a-fA-F]+)\n)?" +
145154
// r"(?:[^\n]+\n)*" +
146155
r"(?:Generic/Specific device types: (?P<generic_device_type>(0x)?[0-9a-fA-F]+)/(?P<specific_device_type>(0x)?[0-9a-fA-F]+)\n)?" +
147156
// r"(?:[^\n]+\n)*" +
@@ -180,11 +189,14 @@ fn parse_attr_value(t: AttributeType, v: &str) -> Result<AttributeValue, Box<dyn
180189
"" => AttributeValue::NoValue,
181190
v => match t {
182191
AttributeType::UInt8 => AttributeValue::UInt8(v.parse()?),
192+
AttributeType::UInt16 => AttributeValue::UInt16(v.parse()?),
193+
AttributeType::UInt32 => AttributeValue::UInt32(v.parse()?),
183194
AttributeType::Bool => AttributeValue::Bool(match v {
184195
"TRUE" => true,
185196
"FALSE" => false,
186197
_ => bail!("Bad attribute value: {}", v),
187198
}),
199+
AttributeType::String => AttributeValue::String(v.to_string()),
188200
},
189201
})
190202
}
@@ -253,7 +265,10 @@ impl DeviceController for AprontestController {
253265
.map(|m| -> Result<DeviceAttribute, Box<dyn Error>> {
254266
let attribute_type = match m.name("type").unwrap().as_str() {
255267
"UINT8" => AttributeType::UInt8,
268+
"UINT16" => AttributeType::UInt16,
269+
"UINT32" => AttributeType::UInt32,
256270
"BOOL" => AttributeType::Bool,
271+
"STRING" => AttributeType::String,
257272
_ => bail!("Bad attribute type: {}", m.name("type").unwrap().as_str()),
258273
};
259274
Ok(DeviceAttribute {
@@ -285,7 +300,10 @@ impl DeviceController for AprontestController {
285300
let value = match value {
286301
AttributeValue::NoValue => bail!("Invalid attribute value: none"),
287302
AttributeValue::UInt8(v) => format!("{}", v),
303+
AttributeValue::UInt16(v) => format!("{}", v),
304+
AttributeValue::UInt32(v) => format!("{}", v),
288305
AttributeValue::Bool(v) => if *v { "TRUE" } else { "FALSE" }.to_string(),
306+
AttributeValue::String(v) => v.clone(),
289307
};
290308
(self.runner)(&[
291309
"aprontest",
@@ -340,29 +358,33 @@ impl DeviceController for FakeController {
340358
attribute_type: AttributeType::UInt8,
341359
supports_write: true,
342360
supports_read: true,
343-
current_value: *self
361+
current_value: self
344362
.attr_values
345363
.get(&(master_id, 1 as AttributeId))
346-
.unwrap_or(&AttributeValue::UInt8(0)),
347-
setting_value: *self
364+
.unwrap_or(&AttributeValue::UInt8(0))
365+
.clone(),
366+
setting_value: self
348367
.attr_values
349368
.get(&(master_id, 1 as AttributeId))
350-
.unwrap_or(&AttributeValue::UInt8(0)),
369+
.unwrap_or(&AttributeValue::UInt8(0))
370+
.clone(),
351371
},
352372
DeviceAttribute {
353373
id: 3,
354374
description: "Level".to_string(),
355375
attribute_type: AttributeType::UInt8,
356376
supports_write: true,
357377
supports_read: true,
358-
current_value: *self
378+
current_value: self
359379
.attr_values
360380
.get(&(master_id, 3 as AttributeId))
361-
.unwrap_or(&AttributeValue::UInt8(0)),
362-
setting_value: *self
381+
.unwrap_or(&AttributeValue::UInt8(0))
382+
.clone(),
383+
setting_value: self
363384
.attr_values
364385
.get(&(master_id, 3 as AttributeId))
365-
.unwrap_or(&AttributeValue::UInt8(0)),
386+
.unwrap_or(&AttributeValue::UInt8(0))
387+
.clone(),
366388
},
367389
DeviceAttribute {
368390
id: 4,
@@ -519,4 +541,133 @@ Bedroom Fan
519541
controller.describe(2).unwrap()
520542
)
521543
}
544+
545+
const TEST_OLD_LIST_STRING: &str = r###"
546+
Found 4 devices in database...
547+
MASTERID | INTERCONNECT | USERNAME
548+
1 | ZIGBEE | LV_Lamp1
549+
2 | ZIGBEE | LV_Lamp2
550+
3 | ZIGBEE | Fireplace-L
551+
4 | ZIGBEE | Fireplace-R
552+
"###;
553+
554+
#[test]
555+
fn older_list() {
556+
let controller = AprontestController {
557+
runner: |_| Ok(TEST_OLD_LIST_STRING.to_string()),
558+
};
559+
560+
assert_eq!(
561+
vec![
562+
ShortDevice {
563+
id: 1,
564+
name: "LV_Lamp1".to_string()
565+
},
566+
ShortDevice {
567+
id: 2,
568+
name: "LV_Lamp2".to_string()
569+
},
570+
ShortDevice {
571+
id: 3,
572+
name: "Fireplace-L".to_string()
573+
},
574+
ShortDevice {
575+
id: 4,
576+
name: "Fireplace-R".to_string()
577+
}
578+
],
579+
controller.list().unwrap()
580+
)
581+
}
582+
583+
const TEST_OLD_DESCRIBE_STRING: &str = r###"
584+
Device has 2 attributes...
585+
LV_Lamp1
586+
ATTRIBUTE | DESCRIPTION | TYPE | MODE | GET | SET
587+
1 | On_Off | STRING | R/W | ON | ON
588+
2 | Level | UINT8 | R/W | 0 | 0
589+
"###;
590+
591+
#[test]
592+
fn old_describe() {
593+
let controller = AprontestController {
594+
runner: |_| Ok(TEST_OLD_DESCRIBE_STRING.to_string()),
595+
};
596+
597+
assert_eq!(
598+
LongDevice {
599+
gang_id: None,
600+
generic_device_type: None,
601+
specific_device_type: None,
602+
manufacturer_id: None,
603+
product_type: None,
604+
product_number: None,
605+
id: 2,
606+
status: "".to_string(),
607+
name: "LV_Lamp1".to_string(),
608+
attributes: vec![
609+
DeviceAttribute {
610+
id: 1,
611+
description: "On_Off".to_string(),
612+
attribute_type: AttributeType::String,
613+
supports_write: true,
614+
supports_read: true,
615+
current_value: AttributeValue::String("ON".to_string()),
616+
setting_value: AttributeValue::String("ON".to_string()),
617+
},
618+
DeviceAttribute {
619+
id: 2,
620+
description: "Level".to_string(),
621+
attribute_type: AttributeType::UInt8,
622+
supports_write: true,
623+
supports_read: true,
624+
current_value: AttributeValue::UInt8(0),
625+
setting_value: AttributeValue::UInt8(0),
626+
},
627+
]
628+
},
629+
controller.describe(2).unwrap()
630+
)
631+
}
632+
633+
const OTHER_TYPES_DESCRIBE: &str = r###"
634+
Gang ID: 0x7ce8f9f9
635+
Manufacturer ID: 0x10dc, Product Number: 0xdfbf
636+
Device is ONLINE, 0 failed tx attempts, 4 seconds since last msg rx'ed, polling period 0 seconds
637+
Device has 14 attributes...
638+
New HA Dimmable Light
639+
ATTRIBUTE | DESCRIPTION | TYPE | MODE | GET | SET
640+
1 | On_Off | STRING | R/W | OFF | OFF
641+
2 | Level | UINT8 | R/W | 254 |
642+
4 | NameSupport | UINT8 | R | 0 |
643+
61440 | ZCLVersion | UINT8 | R | 1 |
644+
61441 | ApplicationVersion | UINT8 | R | 2 |
645+
61442 | StackVersion | UINT8 | R | 2 |
646+
61443 | HWVersion | UINT8 | R | 1 |
647+
61444 | ManufacturerName | STRING | R | GE |
648+
61445 | ModelIdentifier | STRING | R | SoftWhite |
649+
61446 | DateCode | STRING | R | 20150515 |
650+
61447 | PowerSource | UINT8 | R | 1 |
651+
258048 | IdentifyTime | UINT16 | R/W | 0 |
652+
1699842 | ZB_CurrentFileVersion | UINT32 | R | 33554952 |
653+
4294901760 | WK_TransitionTime | UINT16 | R/W | |
654+
"###;
655+
656+
#[test]
657+
fn types_describe() {
658+
let controller = AprontestController {
659+
runner: |_| Ok(OTHER_TYPES_DESCRIBE.to_string()),
660+
};
661+
662+
let result = controller.describe(2).unwrap();
663+
assert_eq!(14, result.attributes.len());
664+
assert_eq!(
665+
AttributeType::UInt32,
666+
result.attributes[result.attributes.len() - 2].attribute_type
667+
);
668+
assert_eq!(
669+
AttributeValue::UInt32(33554952),
670+
result.attributes[result.attributes.len() - 2].current_value
671+
);
672+
}
522673
}

src/converter.rs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use crate::controller::{AttributeType, LongDevice};
22
use serde_json::json;
3+
use simple_error::bail;
4+
use std::error::Error;
5+
6+
use crate::utils::ResultExtensions;
37

48
pub struct AutodiscoveryMessage {
59
pub component: &'static str,
@@ -10,40 +14,62 @@ pub fn device_to_discovery_payload(
1014
topic_prefix: &str,
1115
device: &LongDevice,
1216
) -> Option<AutodiscoveryMessage> {
13-
if device.attribute("Up_Down").is_some() && device.attribute("Level").is_some() {
14-
return Some(dimmer_to_discovery_payload(topic_prefix, device));
17+
if device.attribute("Level").is_some() {
18+
return dimmer_to_discovery_payload(topic_prefix, device)
19+
.log_failing_result("dimmer_discovery_failed");
1520
}
1621
if device.attribute("On_Off").is_some() {
17-
return Some(switch_to_discovery_payload(topic_prefix, device));
22+
return switch_to_discovery_payload(topic_prefix, device)
23+
.log_failing_result("switch_discovery_failed");
1824
}
1925
return None;
2026
}
2127

22-
fn switch_to_discovery_payload(topic_prefix: &str, device: &LongDevice) -> AutodiscoveryMessage {
28+
fn switch_to_discovery_payload(
29+
topic_prefix: &str,
30+
device: &LongDevice,
31+
) -> Result<AutodiscoveryMessage, Box<dyn Error>> {
2332
let on_off = device.attribute("On_Off").unwrap();
24-
AutodiscoveryMessage {
33+
34+
let (payload_on, payload_off) = match on_off.attribute_type {
35+
AttributeType::UInt8 => ("0", "255"),
36+
AttributeType::UInt16 => ("0", "65535"),
37+
AttributeType::UInt32 => ("0", "4294967295"),
38+
AttributeType::Bool => ("TRUE", "FALSE"),
39+
AttributeType::String => ("ON", "OFF"),
40+
};
41+
42+
Ok(AutodiscoveryMessage {
2543
component: "switch",
2644
discovery_info: json!({
2745
"platform": "mqtt",
2846
"unique_id": format!("{}/{}", topic_prefix, device.id),
2947
"name": device.name,
3048
"state_topic": format!("{}{}/status", topic_prefix, device.id),
31-
"value_template": "{{ value_json.On_Off | lower }}",
49+
"value_template": "{{ value_json.On_Off | upper }}",
3250
"command_topic": format!("{}{}/{}/set", topic_prefix, device.id, on_off.id),
33-
"payload_on": "true",
34-
"payload_off": "false",
51+
"payload_on": payload_on,
52+
"payload_off": payload_off,
3553
}),
36-
}
54+
})
3755
}
3856

39-
fn dimmer_to_discovery_payload(topic_prefix: &str, device: &LongDevice) -> AutodiscoveryMessage {
57+
fn dimmer_to_discovery_payload(
58+
topic_prefix: &str,
59+
device: &LongDevice,
60+
) -> Result<AutodiscoveryMessage, Box<dyn Error>> {
4061
let level = device.attribute("Level").unwrap();
41-
let scale = match level.attribute_type {
42-
AttributeType::UInt8 => 255,
62+
let scale: u32 = match level.attribute_type {
63+
AttributeType::UInt8 => u8::max_value() as u32,
64+
AttributeType::UInt16 => u16::max_value() as u32,
65+
AttributeType::UInt32 => u32::max_value(),
4366
AttributeType::Bool => 1,
67+
AttributeType::String => {
68+
bail!("A string level type! Please report with `aprontest -l` output!")
69+
}
4470
};
4571

46-
AutodiscoveryMessage {
72+
Ok(AutodiscoveryMessage {
4773
component: "light",
4874
discovery_info: json!({
4975
"platform": "mqtt",
@@ -60,5 +86,5 @@ fn dimmer_to_discovery_payload(topic_prefix: &str, device: &LongDevice) -> Autod
6086
"brightness_value_template": "{{value_json.Level}}",
6187
"brightness_scale": scale,
6288
}),
63-
}
89+
})
6490
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use url::Url;
2020
mod controller;
2121
mod converter;
2222
mod syncer;
23+
mod utils;
2324

2425
fn init_logger(args: &ArgMatches) -> GlobalLoggerGuard {
2526
let min_log_level = match args.occurrences_of("verbose") {

src/syncer.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,13 @@ where
306306
AttributeValue::UInt8(i) => {
307307
serde_json::Value::Number(serde_json::Number::from(i))
308308
}
309+
AttributeValue::UInt16(i) => {
310+
serde_json::Value::Number(serde_json::Number::from(i))
311+
}
312+
AttributeValue::UInt32(i) => {
313+
serde_json::Value::Number(serde_json::Number::from(i))
314+
}
315+
AttributeValue::String(s) => serde_json::Value::String(s),
309316
},
310317
)
311318
})

0 commit comments

Comments
 (0)