1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Client for interacting with eNet gateways.

macro_rules! bail {
  ($err:expr) => {
    return Err($err.into())
  };
}

pub mod cmd;
mod conn;
pub mod dev;
mod enc;
mod evt;
mod room;

use std::convert::TryFrom;

pub use cmd::SetValuesCommandError;
pub use conn::ConnectError;
use dev::DeviceState;
pub use dev::{BinaryDevice, Device, DimmerDevice, EnetDevice};
pub use enet_proto::{ClickDuration, ItemSetValue, ItemValueRes, SetValue};

use crate::{dev::DeviceDesc, room::RoomDesc};
use cmd::CommandHandler;
use evt::EventHandler;
use thiserror::Error;
use tokio::net::ToSocketAddrs;
use tracing::{event, instrument, Level};

pub struct EnetClient {
  #[allow(dead_code)]
  commands: CommandHandler,
  #[allow(dead_code)]
  events: EventHandler,
  #[allow(dead_code)]
  rooms: Vec<RoomDesc>,
  devices: Vec<Device>,
}

impl EnetClient {
  #[instrument(level = "info", target = "enet-client", skip(addr), err)]
  pub async fn new<A>(addr: A) -> Result<Self, ClientConnectError>
  where
    A: ToSocketAddrs + Clone + Send + Sync + 'static,
  {
    let mut commands = CommandHandler::new(addr.clone()).await?;
    let version = commands.get_version().await?;
    event!(target: "enet-client", Level::INFO, %version.firmware, %version.hardware, %version.enet, "connected to eNet Gateway");

    let channel_types = commands.get_channel_info().await?;
    let project = commands.get_project().await?;
    let rooms = project
      .lists
      .into_iter()
      .filter(|l| l.visible)
      .map(RoomDesc::from)
      .collect::<Vec<_>>();

    let (writers, devices) = project
      .items
      .into_iter()
      .enumerate()
      .filter(|(idx, _)| channel_types.devices.get(*idx) == Some(&1))
      .filter_map(|(idx, item)| DeviceDesc::try_from(item).ok().map(|v| (idx, v)))
      .map(|(idx, desc)| Device::new(desc, idx as u32))
      .unzip();

    let devices: Vec<_> = devices;
    event!(target: "enet-client", Level::INFO, rooms.len = %rooms.len(), devices.len = %devices.len(), "got project info");

    let events = EventHandler::new(addr, writers).await?;

    Ok(Self {
      commands,
      events,
      rooms,
      devices,
    })
  }

  pub fn devices(&self) -> &[Device] {
    &self.devices
  }

  pub fn device(&self, number: u32) -> Option<&Device> {
    self.devices.iter().find(|d| d.number() == number)
  }

  pub async fn set_value(
    &mut self,
    number: u32,
    value: SetValue,
  ) -> Result<(), SetValuesCommandError> {
    let values = vec![ItemSetValue { number, value }];
    self.set_values(values).await
  }

  pub async fn set_values(
    &mut self,
    values: impl IntoIterator<Item = ItemSetValue>,
  ) -> Result<(), SetValuesCommandError> {
    let values: Vec<ItemSetValue> = values.into_iter().collect();
    let new_states = values
      .iter()
      .map(|v| (v.number, DeviceState::from(v.value)))
      .collect();

    self.commands.set_values(values).await?;
    let _ = self.events.update_values(new_states);

    Ok(())
  }
}

#[non_exhaustive]
#[derive(Debug, Error)]
#[error("Failed to connect to gateway.")]
pub enum ClientConnectError {
  Connect(#[from] ConnectError),
  GetVersionCommand(#[from] cmd::GetVersionCommandError),
  GetChannelInfoCommand(#[from] cmd::GetChannelInfoCommandError),
  GetProjectCommand(#[from] cmd::GetProjectCommandError),
}