tmobile-internet-tools 0.8.0

Set of tools for working with T-Mobile Home Internet gateways
use super::{GatewayModel, GatewayClient, parse_json, ClientError, ClientResult, JSONUnwrappable, parse_login_json};
use std::{
    time::Duration,
    collections::HashMap,
    fmt::Write
};
use json::JsonValue;
use reqwest::{
    blocking::Client,
    header::{CONTENT_LENGTH, CONTENT_TYPE}
};
use rust_utils::logging::LogLevel;
use crate::{
    LOG,
    devices::DeviceList,
    config::{DeviceConfig, GlobalConfig, Config},
    stats::{AdvancedGatewayStats, CellInfo, GatewayInfo, NetworkInfo},

};

pub struct SagemcomClient(Client);

#[allow(clippy::new_without_default)]
impl SagemcomClient {
    pub fn new() -> SagemcomClient {
        SagemcomClient(
            Client::builder()
                .timeout(Duration::from_secs(10))
                .user_agent("homeisp/android/2.10")
                .build()
                .unwrap()
        )
    }

    fn get_token(&self) -> ClientResult<String> {
        let config = GlobalConfig::load();
        let mut login_json = HashMap::new();
        login_json.insert("username", config.username);
        login_json.insert("password", config.password);
        let req = self.0.post("http://192.168.12.1/TMI/v1/auth/login").json(&login_json);

        match req.send() {
            Ok(response) => {
                let json_text = response.text().unwrap();
                let json = parse_login_json(&json_text)?;
                match json["auth"]["token"].as_str() {
                    Some(token) => Ok(token.to_string()),
                    None => Err(ClientError::LoginFailed)
                }
            }

            Err(why) => {
                LOG.line(LogLevel::Error, format!("Error logging into gateway: {why}"), false);
                Err(ClientError::GatewayUnreachable(why.to_string()))
            }
        }
    }

    // POST request to the gateway
    fn post(&self, res: &str, body: Option<String>) -> ClientResult<()> {
        let config = GlobalConfig::load();
        let addr = format!("http://{}/TMI/v1/{res}", config.gateway_ip);
        let mut req = self.0
            .post(addr)
            .bearer_auth(self.get_token()?)
            .timeout(Duration::from_secs(1));

        if let Some(json_str) = body {
            req = req.body(json_str).header(CONTENT_TYPE, "application/json");
        }
        else {
            req = req.header(CONTENT_LENGTH, 0);
        }
        match req.send() {
            Ok(_) => Ok(()),
            Err(why) => Err(ClientError::GatewayUnreachable(why.to_string()))
        }
    }

    // GET request to the gateway (returns JSON)
    fn json_get(&self, res: &str, auth: bool) -> ClientResult<JsonValue> {
        let config = GlobalConfig::load();
        let addr = format!("http://{}/TMI/v1/{res}", config.gateway_ip);
        let mut req = self.0.get(addr);
        if auth {
            req = req.bearer_auth(self.get_token()?);
        }

        match req.send() {
            Ok(response) => {
                let text = response.text().unwrap();
                Ok(parse_json(&text)?)
            },
            Err(why) => Err(ClientError::GatewayUnreachable(why.to_string()))
        }
    }
}

impl GatewayClient for SagemcomClient {
    // request the gateway to reboot (no log output)
    fn reboot_gateway_no_log(&self) -> ClientResult<()> {
        self.post("gateway/reset?set=reboot", None).unwrap_or(());
        Ok(())
    }

    // get a list of devices and their ipv4 addresses connected to the gateway
    fn get_devices(&self) -> ClientResult<DeviceList> {
        let dev_config = DeviceConfig::load();
        let json = self.json_get("network/telemetry?get=clients", true)?;
        DeviceList::get_sgmcm(json, &dev_config)
    }

    // get the 5G metrics
    fn get_5g_stats(&self) -> ClientResult<CellInfo> {
        let json = self.json_get("gateway?get=all", false)?;
        let bars = json["signal"]["5g"]["bars"].as_u8().unwrap_json("bars", &json)?;
        let strength = json["signal"]["5g"]["rsrp"].as_i32().unwrap_json("rsrp", &json)?;
        let band = json["signal"]["5g"]["bands"][0].as_str().unwrap_json("bands", &json)?.to_string();

        Ok(CellInfo {
            band,
            bars,
            strength
        })
    }

    // get the 4G (LTE) metrics
    fn get_4g_stats(&self) -> ClientResult<CellInfo> {
        let json = self.json_get("gateway?get=all", false)?;
        let bars = json["signal"]["4g"]["bars"].as_u8().unwrap_json("bars", &json)?;
        let strength = json["signal"]["4g"]["rsrp"].as_i32().unwrap_json("rsrp", &json)?;
        let band = json["signal"]["4g"]["bands"][0].as_str().unwrap_json("bands", &json)?.to_string();

        Ok(CellInfo {
            band,
            bars,
            strength
        })
    }

    // get the connection status as a boolean
    fn get_conn_status(&self) -> bool {
        let json_res = self.json_get("gateway?get=all", false);
        if let Ok(json) = json_res {
            let val = json["device"]["isEnabled"].as_bool();
            return val.unwrap_or(false);
        }
        false
    }

    // get info about the gateway
    fn get_info(&self) -> ClientResult<GatewayInfo> {
        let json = self.json_get("gateway?get=all", false)?;
        let sim_json = self.json_get("network/telemetry?get=sim", true)?;
        let vendor = json["device"]["manufacturer"].as_str().unwrap_json("manufacturer", &json)?.to_string();
        let ser_num = json["device"]["serial"].as_str().unwrap_json("serial", &json)?.to_string();
        let hw_ver = json["device"]["hardwareVersion"].as_str().unwrap_json("hardwareVersion", &json)?.to_string();
        let sw_ver = json["device"]["softwareVersion"].as_str().unwrap_json("softwareVersion", &json)?.to_string();
        let uptime = json["time"]["upTime"].as_usize().unwrap_json("upTime", &json)?;
        let imei = sim_json["sim"]["imei"].as_str().unwrap_json("imei", &sim_json)?.parse().unwrap();
        let mut line_num_raw = sim_json["sim"]["msisdn"].as_str().unwrap_json(",msisdn", &sim_json)?.to_string();
        line_num_raw.remove(0);
        while line_num_raw.chars().count() < 10 { line_num_raw.push(' '); }
        let mut line_num = format!("{}-{}-{}", &line_num_raw[0..3], &line_num_raw[3..6], &line_num_raw[6..10]);
        line_num.retain(|c| c != ' ');
        Ok(GatewayInfo {
            vendor,
            ser_num,
            hw_ver,
            sw_ver,
            uptime,
            imei,
            line_num
        })
    }

    // get advanced gateway stats
    fn get_adv_stats(&self) -> ClientResult<AdvancedGatewayStats> {
        let json = self.json_get("gateway?get=all", false)?;
        let cell_json = self.json_get("network/telemetry?get=cell", true)?;
        let sim_json = self.json_get("network/telemetry?get=sim", true)?;
        let apn_name = cell_json["cell"]["generic"]["apn"].as_str().unwrap_json("apn", &cell_json)?.to_string();
        let band_5g = json["signal"]["5g"]["bands"][0].as_str().unwrap_json("0", &json)?.to_string();
        let rsrp_5g = cell_json["cell"]["5g"]["sector"]["rsrp"].as_i32().unwrap_json("rsrp", &cell_json)?;
        let snr_5g = cell_json["cell"]["5g"]["sector"]["sinr"].as_i32().unwrap_json("sinr", &cell_json)?;
        let rsrq_5g = cell_json["cell"]["5g"]["sector"]["rsrq"].as_i32().unwrap_json("rsrq", &cell_json)?;
        let rssi = cell_json["cell"]["5g"]["sector"]["rssi"].as_i32().unwrap_json("rssi", &cell_json)?;
        let band_4g = json["signal"]["4g"]["bands"][0].as_str().unwrap_json("0", &json)?.to_string();
        let rsrp_4g = cell_json["cell"]["4g"]["sector"]["rsrp"].as_i32().unwrap_json("rsrp", &cell_json)?;
        let snr_4g = cell_json["cell"]["4g"]["sector"]["sinr"].as_i32().unwrap_json("sinr", &cell_json)?;
        let rsrq_4g = cell_json["cell"]["4g"]["sector"]["rsrq"].as_i32().unwrap_json("rsrq", &cell_json)?;
        let imei = sim_json["sim"]["imei"].as_str().unwrap_json("imei", &sim_json)?.parse().unwrap();
        let imsi = sim_json["sim"]["imsi"].as_str().unwrap_json("imsi", &sim_json)?.parse().unwrap();
        let iccid = sim_json["sim"]["iccId"].as_str().unwrap_json("iccId", &sim_json)?.parse().unwrap();

        Ok(AdvancedGatewayStats {
            apn_name,
            apn_ip4: "N/A".to_string(),
            apn_ip6: "N/A".to_string(),
            band_5g,
            rsrp_5g,
            snr_5g,
            rsrq_5g,
            rssi,
            band_4g,
            rsrp_4g,
            snr_4g,
            rsrq_4g,
            imei,
            imsi,
            iccid
        })
    }

    // get list of active networks
    fn get_networks(&self) -> ClientResult<Vec<NetworkInfo>> {
        let json = self.json_get("network/configuration/v2?get=ap", true)?;
        let mut networks = Vec::new();
        let net_json = json["ssids"].members();
        for (i, net) in net_json.enumerate() {
            networks.push(NetworkInfo {
                id: format!("WLAN{i}"),
                name: net["ssidName"].as_str().unwrap_json("ssidName", net)?.to_string(),
                password: net["wpaKey"].as_str().unwrap_json("wpaKey", net)?.to_string(),
                net_security: net["encryptionVersion"].as_str().unwrap_json("encryptionVersion", net)?.to_string(),
                enabled: net["isBroadcastEnabled"].as_bool().unwrap_json("isBroadcastEnabled", net)?,
                freq2_4g: net["2.4ghzSsid"].as_bool().unwrap_json("2.4ghzSsid", net)?,
                freq5g: net["5.0ghzSsid"].as_bool().unwrap_json("5.0ghzSsid", net)?,

                #[cfg(feature = "nokia")]
                download: 0,

                #[cfg(feature = "nokia")]
                upload: 0
            })
        }

        Ok(networks)
    }

    // gateway picture for the web interface
    fn webui_pic(&self) -> &'static str { "sagemcom" }

    // get the model of the gateway
    fn model(&self) -> GatewayModel { GatewayModel::Sagemcom }

    // set network config for gateway (sagemcom only)
    fn set_networks(&self, networks: &[NetworkInfo]) -> ClientResult<()> {
        let mut net_json_list = String::new();
        for network in networks {
            write!(net_json_list,
                "{{\
                    \"encryptionMode\":\"AES\",\
                    \"encryptionVersion\":\"WPA2\",\
                    \"guest\":false,\
                    \"2.4ghzSsid\":{},\
                    \"5.0ghzSsid\":{},\
                    \"isBroadcastEnabled\":true,\
                    \"ssidName\":\"{}\",
                    \"wpaKey\":\"{}\"
                }},",
                network.freq2_4g,
                network.freq5g,
                network.name,
                network.password
            ).unwrap();
        }

        // I hope this thing NEVER has a trailing comma!
        net_json_list.pop();

        let cfg_json = format!(
            "{{\
                \"2.4ghz\":{{
                    \"airtimeFairness\":true,\
                    \"channel\":\"Auto\",\
                    \"channelBandwidth\":\"Auto\",\
                    \"isMUMIMOEnabled\":true,\
                    \"isRadioEnabled\":true,\
                    \"isWMMEnabled\":true,\
                    \"maxClients\":64,\
                    \"mode\":\"auto\",\
                    \"transmissionPower\":\"100%\"\
                }},\
                \"5.0ghz\":{{\
                    \"airtimeFairness\":true,\
                    \"channel\":\"Auto\",\
                    \"channelBandwidth\":\"Auto\",\
                    \"isMUMIMOEnabled\":true,\
                    \"isRadioEnabled\":true,\
                    \"isWMMEnabled\":true,\
                    \"maxClients\":64,\
                    \"mode\":\"auto\",\
                    \"transmissionPower\":\"100%\"\
                }},\
                \"bandSteering\":{{\
                    \"isEnabled\":true\
                }},\
                \"ssids\":[\
                    {net_json_list}\
                ]\
            }}"
        );

        self.post("network/configuration/v2?set=ap", Some(cfg_json))
    }

    // set admin password (sagemcom only)
    fn set_admin_pswd(&self, password: &str) -> ClientResult<()> {
        let auth_json = format!(
            "{{\
                \"passwordNew\":\"{password}\",\
                \"usernameNew\":\"admin\"\
            }}"
        );

        self.post("auth/admin/reset", Some(auth_json))
    }
}