use crate::error::{ProtocolError, Result};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Address {
Group(GroupAddress),
Individual(IndividualAddress),
}
impl fmt::Display for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Address::Group(addr) => write!(f, "{addr}"),
Address::Individual(addr) => write!(f, "{addr}"),
}
}
}
impl From<GroupAddress> for Address {
fn from(addr: GroupAddress) -> Self {
Address::Group(addr)
}
}
impl From<IndividualAddress> for Address {
fn from(addr: IndividualAddress) -> Self {
Address::Individual(addr)
}
}
impl TryFrom<&str> for Address {
type Error = crate::error::KnxError;
fn try_from(s: &str) -> Result<Self> {
if s.contains('/') {
Ok(Address::Group(GroupAddress::from_str(s)?))
} else if s.contains('.') {
Ok(Address::Individual(IndividualAddress::from_str(s)?))
} else {
Err(ProtocolError::InvalidAddress {
address: s.to_string(),
reason: "Address must contain either '/' (group) or '.' (individual)".to_string(),
}
.into())
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct GroupAddress(u16);
impl GroupAddress {
pub const MAX_MAIN: u8 = 15;
pub const MAX_MIDDLE: u8 = 7;
pub const MAX_SUB: u8 = 255;
pub const MAX_RAW: u16 = 0x7FFF;
#[must_use]
pub const fn new(main: u8, middle: u8, sub: u8) -> Self {
debug_assert!(main <= Self::MAX_MAIN, "main group out of range");
debug_assert!(middle <= Self::MAX_MIDDLE, "middle group out of range");
let raw = ((main as u16) << 11) | ((middle as u16) << 8) | (sub as u16);
Self(raw)
}
pub const fn try_new(main: u8, middle: u8, sub: u8) -> std::result::Result<Self, AddressError> {
if main > Self::MAX_MAIN {
return Err(AddressError::InvalidRange {
component: "main",
value: main as u32,
max: Self::MAX_MAIN as u32,
});
}
if middle > Self::MAX_MIDDLE {
return Err(AddressError::InvalidRange {
component: "middle",
value: middle as u32,
max: Self::MAX_MIDDLE as u32,
});
}
let raw = ((main as u16) << 11) | ((middle as u16) << 8) | (sub as u16);
Ok(Self(raw))
}
pub const fn try_from_raw(raw: u16) -> std::result::Result<Self, AddressError> {
if raw > Self::MAX_RAW {
return Err(AddressError::InvalidRange {
component: "raw",
value: raw as u32,
max: Self::MAX_RAW as u32,
});
}
Ok(Self(raw))
}
#[must_use]
pub const fn from_raw_unchecked(raw: u16) -> Self {
Self(raw)
}
pub const fn from_parts(
main: u8,
middle: u8,
sub: u8,
) -> std::result::Result<Self, AddressError> {
Self::try_new(main, middle, sub)
}
#[must_use]
pub const fn raw(&self) -> u16 {
self.0
}
#[must_use]
pub const fn main(&self) -> u8 {
((self.0 >> 11) & 0x0F) as u8
}
#[must_use]
pub const fn middle(&self) -> u8 {
((self.0 >> 8) & 0x07) as u8
}
#[must_use]
pub const fn sub(&self) -> u8 {
(self.0 & 0xFF) as u8
}
#[must_use]
pub const fn parts(&self) -> (u8, u8, u8) {
(self.main(), self.middle(), self.sub())
}
#[must_use]
pub const fn is_broadcast(&self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn broadcast() -> Self {
Self(0)
}
}
impl fmt::Display for GroupAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}/{}", self.main(), self.middle(), self.sub())
}
}
impl FromStr for GroupAddress {
type Err = crate::error::KnxError;
fn from_str(s: &str) -> Result<Self> {
let parts: Vec<&str> = s.split('/').collect();
if parts.len() != 3 {
return Err(ProtocolError::InvalidAddress {
address: s.to_string(),
reason: "Group address must have format main/middle/sub".to_string(),
}
.into());
}
let main = parts[0]
.parse::<u8>()
.map_err(|_| ProtocolError::InvalidAddress {
address: s.to_string(),
reason: "Invalid main group number".to_string(),
})?;
let middle = parts[1]
.parse::<u8>()
.map_err(|_| ProtocolError::InvalidAddress {
address: s.to_string(),
reason: "Invalid middle group number".to_string(),
})?;
let sub = parts[2]
.parse::<u8>()
.map_err(|_| ProtocolError::InvalidAddress {
address: s.to_string(),
reason: "Invalid sub group number".to_string(),
})?;
Self::try_new(main, middle, sub).map_err(|e| {
ProtocolError::InvalidAddress {
address: s.to_string(),
reason: e.to_string(),
}
.into()
})
}
}
impl TryFrom<u16> for GroupAddress {
type Error = AddressError;
fn try_from(raw: u16) -> std::result::Result<Self, Self::Error> {
Self::try_from_raw(raw)
}
}
impl From<GroupAddress> for u16 {
fn from(addr: GroupAddress) -> Self {
addr.raw()
}
}
impl TryFrom<(u8, u8, u8)> for GroupAddress {
type Error = AddressError;
fn try_from((main, middle, sub): (u8, u8, u8)) -> std::result::Result<Self, Self::Error> {
Self::try_new(main, middle, sub)
}
}
impl From<GroupAddress> for (u8, u8, u8) {
fn from(addr: GroupAddress) -> Self {
addr.parts()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct IndividualAddress(u16);
impl IndividualAddress {
pub const MAX_AREA: u8 = 15;
pub const MAX_LINE: u8 = 15;
pub const MAX_DEVICE: u8 = 255;
pub const MAX_RAW: u16 = 0xFFFF;
#[must_use]
pub const fn new(area: u8, line: u8, device: u8) -> Self {
debug_assert!(area <= Self::MAX_AREA, "area out of range");
debug_assert!(line <= Self::MAX_LINE, "line out of range");
let raw = ((area as u16) << 12) | ((line as u16) << 8) | (device as u16);
Self(raw)
}
pub const fn try_new(
area: u8,
line: u8,
device: u8,
) -> std::result::Result<Self, AddressError> {
if area > Self::MAX_AREA {
return Err(AddressError::InvalidRange {
component: "area",
value: area as u32,
max: Self::MAX_AREA as u32,
});
}
if line > Self::MAX_LINE {
return Err(AddressError::InvalidRange {
component: "line",
value: line as u32,
max: Self::MAX_LINE as u32,
});
}
let raw = ((area as u16) << 12) | ((line as u16) << 8) | (device as u16);
Ok(Self(raw))
}
#[must_use]
pub const fn from_raw(raw: u16) -> Self {
Self(raw)
}
#[must_use]
pub const fn from_parts_unchecked(area: u8, line: u8, device: u8) -> Self {
let raw = ((area as u16) << 12) | ((line as u16) << 8) | (device as u16);
Self(raw)
}
#[must_use]
pub const fn raw(&self) -> u16 {
self.0
}
#[must_use]
pub const fn area(&self) -> u8 {
((self.0 >> 12) & 0x0F) as u8
}
#[must_use]
pub const fn line(&self) -> u8 {
((self.0 >> 8) & 0x0F) as u8
}
#[must_use]
pub const fn device(&self) -> u8 {
(self.0 & 0xFF) as u8
}
#[must_use]
pub const fn parts(&self) -> (u8, u8, u8) {
(self.area(), self.line(), self.device())
}
#[must_use]
pub const fn is_broadcast(&self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn broadcast() -> Self {
Self(0)
}
}
impl fmt::Display for IndividualAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.area(), self.line(), self.device())
}
}
impl FromStr for IndividualAddress {
type Err = crate::error::KnxError;
fn from_str(s: &str) -> Result<Self> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return Err(ProtocolError::InvalidAddress {
address: s.to_string(),
reason: "Individual address must have format area.line.device".to_string(),
}
.into());
}
let area = parts[0]
.parse::<u8>()
.map_err(|_| ProtocolError::InvalidAddress {
address: s.to_string(),
reason: "Invalid area number".to_string(),
})?;
let line = parts[1]
.parse::<u8>()
.map_err(|_| ProtocolError::InvalidAddress {
address: s.to_string(),
reason: "Invalid line number".to_string(),
})?;
let device = parts[2]
.parse::<u8>()
.map_err(|_| ProtocolError::InvalidAddress {
address: s.to_string(),
reason: "Invalid device number".to_string(),
})?;
Self::try_new(area, line, device).map_err(|e| {
ProtocolError::InvalidAddress {
address: s.to_string(),
reason: e.to_string(),
}
.into()
})
}
}
impl TryFrom<u16> for IndividualAddress {
type Error = AddressError;
fn try_from(raw: u16) -> std::result::Result<Self, Self::Error> {
Ok(Self::from_raw(raw))
}
}
impl From<IndividualAddress> for u16 {
fn from(addr: IndividualAddress) -> Self {
addr.raw()
}
}
impl TryFrom<(u8, u8, u8)> for IndividualAddress {
type Error = AddressError;
fn try_from((area, line, device): (u8, u8, u8)) -> std::result::Result<Self, Self::Error> {
Self::try_new(area, line, device)
}
}
impl From<IndividualAddress> for (u8, u8, u8) {
fn from(addr: IndividualAddress) -> Self {
addr.parts()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AddressError {
InvalidRange {
component: &'static str,
value: u32,
max: u32,
},
InvalidFormat {
input: String,
expected: &'static str,
},
}
impl fmt::Display for AddressError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AddressError::InvalidRange {
component,
value,
max,
} => {
write!(f, "Invalid {component} value: {value} (max: {max})")
}
AddressError::InvalidFormat { input, expected } => {
write!(
f,
"Invalid address format: '{input}' (expected: {expected})"
)
}
}
}
}
impl std::error::Error for AddressError {}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use std::str::FromStr;
#[test]
fn property_address_validation_completeness() {
proptest!(|(
// Generate valid group address components
main in 0u8..=GroupAddress::MAX_MAIN,
middle in 0u8..=GroupAddress::MAX_MIDDLE,
sub in 0u8..=GroupAddress::MAX_SUB,
// Generate valid individual address components
area in 0u8..=IndividualAddress::MAX_AREA,
line in 0u8..=IndividualAddress::MAX_LINE,
device in 0u8..=IndividualAddress::MAX_DEVICE,
// Generate invalid components for testing validation
invalid_main in (GroupAddress::MAX_MAIN + 1)..=255u8,
invalid_middle in (GroupAddress::MAX_MIDDLE + 1)..=255u8,
invalid_area in (IndividualAddress::MAX_AREA + 1)..=255u8,
invalid_line in (IndividualAddress::MAX_LINE + 1)..=255u8,
)| {
let group_addr = GroupAddress::new(main, middle, sub);
prop_assert_eq!(group_addr.main(), main);
prop_assert_eq!(group_addr.middle(), middle);
prop_assert_eq!(group_addr.sub(), sub);
let group_str = group_addr.to_string();
let parsed_group = GroupAddress::from_str(&group_str)?;
prop_assert_eq!(group_addr, parsed_group);
let individual_addr = IndividualAddress::new(area, line, device);
prop_assert_eq!(individual_addr.area(), area);
prop_assert_eq!(individual_addr.line(), line);
prop_assert_eq!(individual_addr.device(), device);
let individual_str = individual_addr.to_string();
let parsed_individual = IndividualAddress::from_str(&individual_str)?;
prop_assert_eq!(individual_addr, parsed_individual);
let group_enum: Address = group_addr.into();
let individual_enum: Address = individual_addr.into();
prop_assert_eq!(group_enum.to_string(), group_str);
prop_assert_eq!(individual_enum.to_string(), individual_str);
prop_assert!(GroupAddress::try_new(invalid_main, middle, sub).is_err());
prop_assert!(GroupAddress::try_new(main, invalid_middle, sub).is_err());
prop_assert!(IndividualAddress::try_new(invalid_area, line, device).is_err());
prop_assert!(IndividualAddress::try_new(area, invalid_line, device).is_err());
let group_tuple: (u8, u8, u8) = group_addr.into();
prop_assert_eq!(group_tuple, (main, middle, sub));
let individual_tuple: (u8, u8, u8) = individual_addr.into();
prop_assert_eq!(individual_tuple, (area, line, device));
let group_raw: u16 = group_addr.into();
let individual_raw: u16 = individual_addr.into();
prop_assert_eq!(GroupAddress::try_from(group_raw)?, group_addr);
prop_assert_eq!(IndividualAddress::try_from(individual_raw)?, individual_addr);
});
}
#[test]
fn test_group_address_validation() {
let addr = GroupAddress::new(15, 7, 255);
assert_eq!(addr.main(), 15);
assert_eq!(addr.middle(), 7);
assert_eq!(addr.sub(), 255);
let result = GroupAddress::try_new(16, 0, 0);
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("main"));
}
let result = GroupAddress::try_new(0, 8, 0);
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("middle"));
}
let addr = GroupAddress::from_str("1/2/3").unwrap();
assert_eq!(addr.main(), 1);
assert_eq!(addr.middle(), 2);
assert_eq!(addr.sub(), 3);
assert!(GroupAddress::from_str("1/2").is_err());
assert!(GroupAddress::from_str("1/2/3/4").is_err());
assert!(GroupAddress::from_str("16/0/0").is_err());
assert!(GroupAddress::from_str("0/8/0").is_err());
assert!(GroupAddress::from_str("a/b/c").is_err());
}
#[test]
fn test_individual_address_validation() {
let addr = IndividualAddress::new(15, 15, 255);
assert_eq!(addr.area(), 15);
assert_eq!(addr.line(), 15);
assert_eq!(addr.device(), 255);
let result = IndividualAddress::try_new(16, 0, 0);
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("area"));
}
let result = IndividualAddress::try_new(0, 16, 0);
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("line"));
}
let addr = IndividualAddress::from_str("1.2.3").unwrap();
assert_eq!(addr.area(), 1);
assert_eq!(addr.line(), 2);
assert_eq!(addr.device(), 3);
assert!(IndividualAddress::from_str("1.2").is_err());
assert!(IndividualAddress::from_str("1.2.3.4").is_err());
assert!(IndividualAddress::from_str("16.0.0").is_err());
assert!(IndividualAddress::from_str("0.16.0").is_err());
assert!(IndividualAddress::from_str("a.b.c").is_err());
}
#[test]
fn test_address_display() {
let group_addr = GroupAddress::new(1, 2, 3);
assert_eq!(group_addr.to_string(), "1/2/3");
let individual_addr = IndividualAddress::new(1, 2, 3);
assert_eq!(individual_addr.to_string(), "1.2.3");
let group_enum = Address::Group(group_addr);
assert_eq!(group_enum.to_string(), "1/2/3");
let individual_enum = Address::Individual(individual_addr);
assert_eq!(individual_enum.to_string(), "1.2.3");
}
#[test]
fn test_address_raw_values() {
let addr = GroupAddress::new(1, 2, 3);
let expected_raw = (1u16 << 11) | (2u16 << 8) | 3u16;
assert_eq!(addr.raw(), expected_raw);
let addr_from_raw = GroupAddress::try_from_raw(expected_raw).unwrap();
assert_eq!(addr, addr_from_raw);
let addr = IndividualAddress::new(1, 2, 3);
let expected_raw = (1u16 << 12) | (2u16 << 8) | 3u16;
assert_eq!(addr.raw(), expected_raw);
let addr_from_raw = IndividualAddress::from_raw(expected_raw);
assert_eq!(addr, addr_from_raw);
}
#[test]
fn test_broadcast_addresses() {
let group_broadcast = GroupAddress::broadcast();
assert!(group_broadcast.is_broadcast());
assert_eq!(group_broadcast.parts(), (0, 0, 0));
let individual_broadcast = IndividualAddress::broadcast();
assert!(individual_broadcast.is_broadcast());
assert_eq!(individual_broadcast.parts(), (0, 0, 0));
}
#[test]
fn test_address_ordering() {
let addr1 = GroupAddress::from_parts(1, 0, 0).unwrap();
let addr2 = GroupAddress::from_parts(1, 0, 1).unwrap();
let addr3 = GroupAddress::from_parts(1, 1, 0).unwrap();
assert!(addr1 < addr2);
assert!(addr2 < addr3);
assert!(addr1 < addr3);
let iaddr1 = IndividualAddress::new(1, 0, 0);
let iaddr2 = IndividualAddress::new(1, 0, 1);
let iaddr3 = IndividualAddress::new(1, 1, 0);
assert!(iaddr1 < iaddr2);
assert!(iaddr2 < iaddr3);
assert!(iaddr1 < iaddr3);
}
#[test]
fn test_conversion_traits() {
let group_addr = GroupAddress::new(5, 3, 100);
let tuple: (u8, u8, u8) = group_addr.into();
assert_eq!(tuple, (5, 3, 100));
let from_tuple = GroupAddress::try_from(tuple).unwrap();
assert_eq!(from_tuple, group_addr);
let raw: u16 = group_addr.into();
let from_raw = GroupAddress::try_from(raw).unwrap();
assert_eq!(from_raw, group_addr);
let individual_addr = IndividualAddress::new(2, 5, 50);
let tuple: (u8, u8, u8) = individual_addr.into();
assert_eq!(tuple, (2, 5, 50));
let from_tuple = IndividualAddress::try_from(tuple).unwrap();
assert_eq!(from_tuple, individual_addr);
let raw: u16 = individual_addr.into();
let from_raw = IndividualAddress::try_from(raw).unwrap();
assert_eq!(from_raw, individual_addr);
}
#[test]
fn test_address_enum_parsing() {
let group_str = "1/2/3";
let addr = Address::try_from(group_str).unwrap();
if let Address::Group(group_addr) = addr {
assert_eq!(group_addr.parts(), (1, 2, 3));
} else {
panic!("Expected Group address");
}
let individual_str = "1.2.3";
let addr = Address::try_from(individual_str).unwrap();
if let Address::Individual(individual_addr) = addr {
assert_eq!(individual_addr.parts(), (1, 2, 3));
} else {
panic!("Expected Individual address");
}
assert!(Address::try_from("invalid").is_err());
}
#[test]
fn test_const_functions() {
const GROUP_ADDR: GroupAddress = GroupAddress::new(1, 2, 3);
const INDIVIDUAL_ADDR: IndividualAddress = IndividualAddress::new(1, 2, 3);
const VALID_GROUP: std::result::Result<GroupAddress, AddressError> =
GroupAddress::try_new(1, 2, 3);
const VALID_INDIVIDUAL: std::result::Result<IndividualAddress, AddressError> =
IndividualAddress::try_new(1, 2, 3);
const INVALID_GROUP: std::result::Result<GroupAddress, AddressError> =
GroupAddress::try_new(32, 0, 0);
const INVALID_INDIVIDUAL: std::result::Result<IndividualAddress, AddressError> =
IndividualAddress::try_new(16, 0, 0);
assert_eq!(GROUP_ADDR.parts(), (1, 2, 3));
assert_eq!(INDIVIDUAL_ADDR.parts(), (1, 2, 3));
assert!(VALID_GROUP.is_ok());
assert!(VALID_INDIVIDUAL.is_ok());
assert!(INVALID_GROUP.is_err());
assert!(INVALID_INDIVIDUAL.is_err());
}
}