use crate::tlv;
use anyhow;
use serde_json;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum AdjustmentCause {
Localoptimization = 0,
Gridoptimization = 1,
}
impl AdjustmentCause {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(AdjustmentCause::Localoptimization),
1 => Some(AdjustmentCause::Gridoptimization),
_ => None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
}
impl From<AdjustmentCause> for u8 {
fn from(val: AdjustmentCause) -> Self {
val as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum Cause {
Normalcompletion = 0,
Offline = 1,
Fault = 2,
Useroptout = 3,
Cancelled = 4,
}
impl Cause {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Cause::Normalcompletion),
1 => Some(Cause::Offline),
2 => Some(Cause::Fault),
3 => Some(Cause::Useroptout),
4 => Some(Cause::Cancelled),
_ => None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
}
impl From<Cause> for u8 {
fn from(val: Cause) -> Self {
val as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum CostType {
Financial = 0,
Ghgemissions = 1,
Comfort = 2,
Temperature = 3,
}
impl CostType {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(CostType::Financial),
1 => Some(CostType::Ghgemissions),
2 => Some(CostType::Comfort),
3 => Some(CostType::Temperature),
_ => None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
}
impl From<CostType> for u8 {
fn from(val: CostType) -> Self {
val as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum ESAState {
Offline = 0,
Online = 1,
Fault = 2,
Poweradjustactive = 3,
Paused = 4,
}
impl ESAState {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(ESAState::Offline),
1 => Some(ESAState::Online),
2 => Some(ESAState::Fault),
3 => Some(ESAState::Poweradjustactive),
4 => Some(ESAState::Paused),
_ => None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
}
impl From<ESAState> for u8 {
fn from(val: ESAState) -> Self {
val as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum ESAType {
Evse = 0,
Spaceheating = 1,
Waterheating = 2,
Spacecooling = 3,
Spaceheatingcooling = 4,
Batterystorage = 5,
Solarpv = 6,
Fridgefreezer = 7,
Washingmachine = 8,
Dishwasher = 9,
Cooking = 10,
Homewaterpump = 11,
Irrigationwaterpump = 12,
Poolpump = 13,
Other = 255,
}
impl ESAType {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(ESAType::Evse),
1 => Some(ESAType::Spaceheating),
2 => Some(ESAType::Waterheating),
3 => Some(ESAType::Spacecooling),
4 => Some(ESAType::Spaceheatingcooling),
5 => Some(ESAType::Batterystorage),
6 => Some(ESAType::Solarpv),
7 => Some(ESAType::Fridgefreezer),
8 => Some(ESAType::Washingmachine),
9 => Some(ESAType::Dishwasher),
10 => Some(ESAType::Cooking),
11 => Some(ESAType::Homewaterpump),
12 => Some(ESAType::Irrigationwaterpump),
13 => Some(ESAType::Poolpump),
255 => Some(ESAType::Other),
_ => None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
}
impl From<ESAType> for u8 {
fn from(val: ESAType) -> Self {
val as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum ForecastUpdateReason {
Internaloptimization = 0,
Localoptimization = 1,
Gridoptimization = 2,
}
impl ForecastUpdateReason {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(ForecastUpdateReason::Internaloptimization),
1 => Some(ForecastUpdateReason::Localoptimization),
2 => Some(ForecastUpdateReason::Gridoptimization),
_ => None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
}
impl From<ForecastUpdateReason> for u8 {
fn from(val: ForecastUpdateReason) -> Self {
val as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum OptOutState {
Nooptout = 0,
Localoptout = 1,
Gridoptout = 2,
Optout = 3,
}
impl OptOutState {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(OptOutState::Nooptout),
1 => Some(OptOutState::Localoptout),
2 => Some(OptOutState::Gridoptout),
3 => Some(OptOutState::Optout),
_ => None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
}
impl From<OptOutState> for u8 {
fn from(val: OptOutState) -> Self {
val as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum PowerAdjustReason {
Noadjustment = 0,
Localoptimizationadjustment = 1,
Gridoptimizationadjustment = 2,
}
impl PowerAdjustReason {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(PowerAdjustReason::Noadjustment),
1 => Some(PowerAdjustReason::Localoptimizationadjustment),
2 => Some(PowerAdjustReason::Gridoptimizationadjustment),
_ => None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
}
impl From<PowerAdjustReason> for u8 {
fn from(val: PowerAdjustReason) -> Self {
val as u8
}
}
#[derive(Debug, serde::Serialize)]
pub struct Constraints {
pub start_time: Option<u64>,
pub duration: Option<u32>,
pub nominal_power: Option<u32>,
pub maximum_energy: Option<u64>,
pub load_control: Option<i8>,
}
#[derive(Debug, serde::Serialize)]
pub struct Cost {
pub cost_type: Option<CostType>,
pub value: Option<i32>,
pub decimal_points: Option<u8>,
pub currency: Option<u16>,
}
#[derive(Debug, serde::Serialize)]
pub struct Forecast {
pub forecast_id: Option<u32>,
pub active_slot_number: Option<u16>,
pub start_time: Option<u64>,
pub end_time: Option<u64>,
pub earliest_start_time: Option<u64>,
pub latest_end_time: Option<u64>,
pub is_pausable: Option<bool>,
pub slots: Option<Vec<Slot>>,
pub forecast_update_reason: Option<ForecastUpdateReason>,
}
#[derive(Debug, serde::Serialize)]
pub struct PowerAdjustCapability {
pub power_adjust_capability: Option<Vec<PowerAdjust>>,
pub cause: Option<PowerAdjustReason>,
}
#[derive(Debug, serde::Serialize)]
pub struct PowerAdjust {
pub min_power: Option<u32>,
pub max_power: Option<u32>,
pub min_duration: Option<u32>,
pub max_duration: Option<u32>,
}
#[derive(Debug, serde::Serialize)]
pub struct SlotAdjustment {
pub slot_index: Option<u8>,
pub nominal_power: Option<u32>,
pub duration: Option<u32>,
}
#[derive(Debug, serde::Serialize)]
pub struct Slot {
pub min_duration: Option<u32>,
pub max_duration: Option<u32>,
pub default_duration: Option<u32>,
pub elapsed_slot_time: Option<u32>,
pub remaining_slot_time: Option<u32>,
pub slot_is_pausable: Option<bool>,
pub min_pause_duration: Option<u32>,
pub max_pause_duration: Option<u32>,
pub manufacturer_esa_state: Option<u16>,
pub nominal_power: Option<u32>,
pub min_power: Option<u32>,
pub max_power: Option<u32>,
pub nominal_energy: Option<u64>,
pub costs: Option<Vec<Cost>>,
pub min_power_adjustment: Option<u32>,
pub max_power_adjustment: Option<u32>,
pub min_duration_adjustment: Option<u32>,
pub max_duration_adjustment: Option<u32>,
}
pub fn encode_power_adjust_request(power: u32, duration: u32, cause: AdjustmentCause) -> anyhow::Result<Vec<u8>> {
let tlv = tlv::TlvItemEnc {
tag: 0,
value: tlv::TlvItemValueEnc::StructInvisible(vec![
(0, tlv::TlvItemValueEnc::UInt32(power)).into(),
(1, tlv::TlvItemValueEnc::UInt32(duration)).into(),
(2, tlv::TlvItemValueEnc::UInt8(cause.to_u8())).into(),
]),
};
Ok(tlv.encode()?)
}
pub fn encode_start_time_adjust_request(requested_start_time: u64, cause: AdjustmentCause) -> anyhow::Result<Vec<u8>> {
let tlv = tlv::TlvItemEnc {
tag: 0,
value: tlv::TlvItemValueEnc::StructInvisible(vec![
(0, tlv::TlvItemValueEnc::UInt64(requested_start_time)).into(),
(1, tlv::TlvItemValueEnc::UInt8(cause.to_u8())).into(),
]),
};
Ok(tlv.encode()?)
}
pub fn encode_pause_request(duration: u32, cause: AdjustmentCause) -> anyhow::Result<Vec<u8>> {
let tlv = tlv::TlvItemEnc {
tag: 0,
value: tlv::TlvItemValueEnc::StructInvisible(vec![
(0, tlv::TlvItemValueEnc::UInt32(duration)).into(),
(1, tlv::TlvItemValueEnc::UInt8(cause.to_u8())).into(),
]),
};
Ok(tlv.encode()?)
}
pub fn encode_modify_forecast_request(forecast_id: u32, slot_adjustments: Vec<SlotAdjustment>, cause: AdjustmentCause) -> anyhow::Result<Vec<u8>> {
let tlv = tlv::TlvItemEnc {
tag: 0,
value: tlv::TlvItemValueEnc::StructInvisible(vec![
(0, tlv::TlvItemValueEnc::UInt32(forecast_id)).into(),
(1, tlv::TlvItemValueEnc::Array(slot_adjustments.into_iter().map(|v| {
let mut fields = Vec::new();
if let Some(x) = v.slot_index { fields.push((0, tlv::TlvItemValueEnc::UInt8(x)).into()); }
if let Some(x) = v.nominal_power { fields.push((1, tlv::TlvItemValueEnc::UInt32(x)).into()); }
if let Some(x) = v.duration { fields.push((2, tlv::TlvItemValueEnc::UInt32(x)).into()); }
(0, tlv::TlvItemValueEnc::StructAnon(fields)).into()
}).collect())).into(),
(2, tlv::TlvItemValueEnc::UInt8(cause.to_u8())).into(),
]),
};
Ok(tlv.encode()?)
}
pub fn encode_request_constraint_based_forecast(constraints: Vec<Constraints>, cause: AdjustmentCause) -> anyhow::Result<Vec<u8>> {
let tlv = tlv::TlvItemEnc {
tag: 0,
value: tlv::TlvItemValueEnc::StructInvisible(vec![
(0, tlv::TlvItemValueEnc::Array(constraints.into_iter().map(|v| {
let mut fields = Vec::new();
if let Some(x) = v.start_time { fields.push((0, tlv::TlvItemValueEnc::UInt64(x)).into()); }
if let Some(x) = v.duration { fields.push((1, tlv::TlvItemValueEnc::UInt32(x)).into()); }
if let Some(x) = v.nominal_power { fields.push((2, tlv::TlvItemValueEnc::UInt32(x)).into()); }
if let Some(x) = v.maximum_energy { fields.push((3, tlv::TlvItemValueEnc::UInt64(x)).into()); }
if let Some(x) = v.load_control { fields.push((4, tlv::TlvItemValueEnc::Int8(x)).into()); }
(0, tlv::TlvItemValueEnc::StructAnon(fields)).into()
}).collect())).into(),
(1, tlv::TlvItemValueEnc::UInt8(cause.to_u8())).into(),
]),
};
Ok(tlv.encode()?)
}
pub fn decode_esa_type(inp: &tlv::TlvItemValue) -> anyhow::Result<ESAType> {
if let tlv::TlvItemValue::Int(v) = inp {
ESAType::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
} else {
Err(anyhow::anyhow!("Expected Integer"))
}
}
pub fn decode_esa_can_generate(inp: &tlv::TlvItemValue) -> anyhow::Result<bool> {
if let tlv::TlvItemValue::Bool(v) = inp {
Ok(*v)
} else {
Err(anyhow::anyhow!("Expected Bool"))
}
}
pub fn decode_esa_state(inp: &tlv::TlvItemValue) -> anyhow::Result<ESAState> {
if let tlv::TlvItemValue::Int(v) = inp {
ESAState::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
} else {
Err(anyhow::anyhow!("Expected Integer"))
}
}
pub fn decode_abs_min_power(inp: &tlv::TlvItemValue) -> anyhow::Result<u32> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(*v as u32)
} else {
Err(anyhow::anyhow!("Expected UInt32"))
}
}
pub fn decode_abs_max_power(inp: &tlv::TlvItemValue) -> anyhow::Result<u32> {
if let tlv::TlvItemValue::Int(v) = inp {
Ok(*v as u32)
} else {
Err(anyhow::anyhow!("Expected UInt32"))
}
}
pub fn decode_power_adjustment_capability(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<PowerAdjustCapability>> {
if let tlv::TlvItemValue::List(_fields) = inp {
let item = tlv::TlvItem { tag: 0, value: inp.clone() };
Ok(Some(PowerAdjustCapability {
power_adjust_capability: {
if let Some(tlv::TlvItemValue::List(l)) = item.get(&[0]) {
let mut items = Vec::new();
for list_item in l {
items.push(PowerAdjust {
min_power: list_item.get_int(&[0]).map(|v| v as u32),
max_power: list_item.get_int(&[1]).map(|v| v as u32),
min_duration: list_item.get_int(&[2]).map(|v| v as u32),
max_duration: list_item.get_int(&[3]).map(|v| v as u32),
});
}
Some(items)
} else {
None
}
},
cause: item.get_int(&[1]).and_then(|v| PowerAdjustReason::from_u8(v as u8)),
}))
} else {
Ok(None)
}
}
pub fn decode_forecast(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<Forecast>> {
if let tlv::TlvItemValue::List(_fields) = inp {
let item = tlv::TlvItem { tag: 0, value: inp.clone() };
Ok(Some(Forecast {
forecast_id: item.get_int(&[0]).map(|v| v as u32),
active_slot_number: item.get_int(&[1]).map(|v| v as u16),
start_time: item.get_int(&[2]),
end_time: item.get_int(&[3]),
earliest_start_time: item.get_int(&[4]),
latest_end_time: item.get_int(&[5]),
is_pausable: item.get_bool(&[6]),
slots: {
if let Some(tlv::TlvItemValue::List(l)) = item.get(&[7]) {
let mut items = Vec::new();
for list_item in l {
items.push(Slot {
min_duration: list_item.get_int(&[0]).map(|v| v as u32),
max_duration: list_item.get_int(&[1]).map(|v| v as u32),
default_duration: list_item.get_int(&[2]).map(|v| v as u32),
elapsed_slot_time: list_item.get_int(&[3]).map(|v| v as u32),
remaining_slot_time: list_item.get_int(&[4]).map(|v| v as u32),
slot_is_pausable: list_item.get_bool(&[5]),
min_pause_duration: list_item.get_int(&[6]).map(|v| v as u32),
max_pause_duration: list_item.get_int(&[7]).map(|v| v as u32),
manufacturer_esa_state: list_item.get_int(&[8]).map(|v| v as u16),
nominal_power: list_item.get_int(&[9]).map(|v| v as u32),
min_power: list_item.get_int(&[10]).map(|v| v as u32),
max_power: list_item.get_int(&[11]).map(|v| v as u32),
nominal_energy: list_item.get_int(&[12]),
costs: {
if let Some(tlv::TlvItemValue::List(l)) = list_item.get(&[13]) {
let mut items = Vec::new();
for list_item in l {
items.push(Cost {
cost_type: list_item.get_int(&[0]).and_then(|v| CostType::from_u8(v as u8)),
value: list_item.get_int(&[1]).map(|v| v as i32),
decimal_points: list_item.get_int(&[2]).map(|v| v as u8),
currency: list_item.get_int(&[3]).map(|v| v as u16),
});
}
Some(items)
} else {
None
}
},
min_power_adjustment: list_item.get_int(&[14]).map(|v| v as u32),
max_power_adjustment: list_item.get_int(&[15]).map(|v| v as u32),
min_duration_adjustment: list_item.get_int(&[16]).map(|v| v as u32),
max_duration_adjustment: list_item.get_int(&[17]).map(|v| v as u32),
});
}
Some(items)
} else {
None
}
},
forecast_update_reason: item.get_int(&[8]).and_then(|v| ForecastUpdateReason::from_u8(v as u8)),
}))
} else {
Ok(None)
}
}
pub fn decode_opt_out_state(inp: &tlv::TlvItemValue) -> anyhow::Result<OptOutState> {
if let tlv::TlvItemValue::Int(v) = inp {
OptOutState::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
} else {
Err(anyhow::anyhow!("Expected Integer"))
}
}
pub fn decode_attribute_json(cluster_id: u32, attribute_id: u32, tlv_value: &crate::tlv::TlvItemValue) -> String {
if cluster_id != 0x0098 {
return format!("{{\"error\": \"Invalid cluster ID. Expected 0x0098, got {}\"}}", cluster_id);
}
match attribute_id {
0x0000 => {
match decode_esa_type(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0001 => {
match decode_esa_can_generate(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0002 => {
match decode_esa_state(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0003 => {
match decode_abs_min_power(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0004 => {
match decode_abs_max_power(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0005 => {
match decode_power_adjustment_capability(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0006 => {
match decode_forecast(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
0x0007 => {
match decode_opt_out_state(tlv_value) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(e) => format!("{{\"error\": \"{}\"}}", e),
}
}
_ => format!("{{\"error\": \"Unknown attribute ID: {}\"}}", attribute_id),
}
}
pub fn get_attribute_list() -> Vec<(u32, &'static str)> {
vec![
(0x0000, "ESAType"),
(0x0001, "ESACanGenerate"),
(0x0002, "ESAState"),
(0x0003, "AbsMinPower"),
(0x0004, "AbsMaxPower"),
(0x0005, "PowerAdjustmentCapability"),
(0x0006, "Forecast"),
(0x0007, "OptOutState"),
]
}
#[derive(Debug, serde::Serialize)]
pub struct PowerAdjustEndEvent {
pub cause: Option<Cause>,
pub duration: Option<u32>,
pub energy_use: Option<u64>,
}
#[derive(Debug, serde::Serialize)]
pub struct ResumedEvent {
pub cause: Option<Cause>,
}
pub fn decode_power_adjust_end_event(inp: &tlv::TlvItemValue) -> anyhow::Result<PowerAdjustEndEvent> {
if let tlv::TlvItemValue::List(_fields) = inp {
let item = tlv::TlvItem { tag: 0, value: inp.clone() };
Ok(PowerAdjustEndEvent {
cause: item.get_int(&[0]).and_then(|v| Cause::from_u8(v as u8)),
duration: item.get_int(&[1]).map(|v| v as u32),
energy_use: item.get_int(&[2]),
})
} else {
Err(anyhow::anyhow!("Expected struct fields"))
}
}
pub fn decode_resumed_event(inp: &tlv::TlvItemValue) -> anyhow::Result<ResumedEvent> {
if let tlv::TlvItemValue::List(_fields) = inp {
let item = tlv::TlvItem { tag: 0, value: inp.clone() };
Ok(ResumedEvent {
cause: item.get_int(&[0]).and_then(|v| Cause::from_u8(v as u8)),
})
} else {
Err(anyhow::anyhow!("Expected struct fields"))
}
}