feat(timezone): get / set / list miner timezone - #295
Conversation
|
Re the list (your follow-up): agreed — I'll use the miner's own list where it has one and fall back to a built-in default list otherwise, no Re moving it under For context on the end goal: this is so Home Assistant can keep a miner's timezone correct across the DST changeover automatically — VNish has no DST handling, so by hand it just gets forgotten. |
| #[cfg(feature = "python")] | ||
| mod python_impls { | ||
| use asic_rs_pydantic::get_optional_field; | ||
| use pyo3::{Borrowed, PyAny, PyErr, PyResult, conversion::FromPyObject, types::PyAnyMethods}; | ||
|
|
||
| use super::TimezoneConfig; | ||
|
|
||
| impl FromPyObject<'_, '_> for TimezoneConfig { | ||
| type Error = PyErr; | ||
|
|
||
| fn extract(obj: Borrowed<'_, '_, PyAny>) -> PyResult<Self> { | ||
| Ok(TimezoneConfig { | ||
| timezone: get_optional_field(&obj, "timezone")? | ||
| .map(|value| value.extract()) | ||
| .transpose()? | ||
| .flatten(), | ||
| available: get_optional_field(&obj, "available")? | ||
| .map(|value| value.extract()) | ||
| .transpose()? | ||
| .unwrap_or_default(), | ||
| }) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Should be able to remove this, automatic with the pydantic macros, reference here:
asic-rs/asic-rs-core/src/config/pools.rs
Lines 7 to 24 in 38552b1
|
|
||
| #[cfg_attr( | ||
| feature = "python", | ||
| pyclass(skip_from_py_object, get_all, module = "asic_rs") |
| impl GetConfigsLocations for BraiinsV2604 { | ||
| #[allow(unused_variables)] | ||
| fn get_configs_locations(&self, data_field: ConfigField) -> Vec<ConfigLocation> { | ||
| vec![] | ||
| const GQL_TIMEZONE: MinerCommand = MinerCommand::GraphQL { | ||
| command: "{ bos { timezone { id } timezoneList { id } } }", | ||
| }; | ||
| match data_field { | ||
| ConfigField::Timezone => vec![( | ||
| GQL_TIMEZONE, | ||
| ConfigExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/bos"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| _ => vec![], | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl CollectConfigs for BraiinsV2604 { | ||
| fn get_config_collector(&self) -> ConfigCollector<'_> { | ||
| ConfigCollector::new(self) | ||
| } | ||
| } | ||
|
|
||
| impl GetDataLocations for BraiinsV2604 { | ||
| fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> { | ||
| const WEB_NETWORK: MinerCommand = MinerCommand::WebAPI { | ||
| command: "network", | ||
| parameters: None, | ||
| }; | ||
| const WEB_VERSION: MinerCommand = MinerCommand::WebAPI { | ||
| command: "version", | ||
| parameters: None, | ||
| }; | ||
| const WEB_MINER_DETAILS: MinerCommand = MinerCommand::WebAPI { | ||
| command: "miner/details", | ||
| parameters: None, | ||
| }; | ||
| const WEB_LOCATE: MinerCommand = MinerCommand::WebAPI { | ||
| command: "actions/locate", | ||
| parameters: None, | ||
| }; | ||
| const WEB_MINER_STATS: MinerCommand = MinerCommand::WebAPI { | ||
| command: "miner/stats", | ||
| parameters: None, | ||
| }; | ||
| const WEB_PERFORMANCE_TUNER_STATE: MinerCommand = MinerCommand::WebAPI { | ||
| command: "performance/tuner-state", | ||
| parameters: None, | ||
| }; | ||
| const WEB_MINER_CONFIGURATION: MinerCommand = MinerCommand::WebAPI { | ||
| command: "configuration/miner", | ||
| parameters: None, | ||
| }; | ||
| const WEB_POOLS: MinerCommand = MinerCommand::WebAPI { | ||
| command: "pools", | ||
| parameters: None, | ||
| }; | ||
| const WEB_COOLING_STATE: MinerCommand = MinerCommand::WebAPI { | ||
| command: "cooling/state", | ||
| parameters: None, | ||
| }; | ||
| const WEB_HASHBOARDS: MinerCommand = MinerCommand::WebAPI { | ||
| command: "miner/hw/hashboards", | ||
| parameters: None, | ||
| }; | ||
| const GQL_EVENTS_QUERY: MinerCommand = MinerCommand::GraphQL { | ||
| command: r#"{ | ||
| events { | ||
| appeals { | ||
| id | ||
| kind | ||
| message | ||
| timestamp | ||
| } | ||
| } | ||
| }"#, | ||
| }; | ||
|
|
||
| match data_field { | ||
| DataField::Mac => vec![( | ||
| WEB_NETWORK, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/mac_address"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::Hostname => vec![( | ||
| WEB_NETWORK, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/hostname"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::ApiVersion => vec![( | ||
| WEB_VERSION, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some(""), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::FirmwareVersion => vec![( | ||
| WEB_MINER_DETAILS, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/bos_version/current"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::Hashrate => vec![( | ||
| WEB_MINER_STATS, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/miner_stats/real_hashrate/last_5s/gigahash_per_second"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::ExpectedHashrate => vec![( | ||
| WEB_MINER_DETAILS, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/sticker_hashrate/gigahash_per_second"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::Fans => vec![( | ||
| WEB_COOLING_STATE, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/fans"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::Hashboards => vec![( | ||
| WEB_HASHBOARDS, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/hashboards"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::LightFlashing => vec![( | ||
| WEB_LOCATE, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some(""), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::IsMining => vec![( | ||
| WEB_MINER_DETAILS, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/status"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::Uptime => vec![( | ||
| WEB_MINER_DETAILS, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/system_uptime_s"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::ControlBoardVersion => vec![( | ||
| WEB_MINER_DETAILS, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/control_board_soc_family"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::Pools => vec![( | ||
| WEB_POOLS, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/0/pools"), // assuming there is 1 pool group | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::Wattage => vec![( | ||
| WEB_MINER_STATS, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/power_stats/approximated_consumption/watt"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::TuningTarget => vec![ | ||
| ( | ||
| WEB_MINER_CONFIGURATION, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/tuner/tuner_mode"), | ||
| tag: Some("mode"), | ||
| }, | ||
| ), | ||
| ( | ||
| WEB_MINER_CONFIGURATION, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/tuner/power_target/watt"), | ||
| tag: Some("configured_power"), | ||
| }, | ||
| ), | ||
| ( | ||
| WEB_MINER_CONFIGURATION, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/tuner/hashrate_target/terahash_per_second"), | ||
| tag: Some("configured_hashrate"), | ||
| }, | ||
| ), | ||
| ( | ||
| WEB_PERFORMANCE_TUNER_STATE, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/mode_state/powertargetmodestate/current_target/watt"), | ||
| tag: Some("scaled_power"), | ||
| }, | ||
| ), | ||
| ( | ||
| WEB_PERFORMANCE_TUNER_STATE, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some( | ||
| "/mode_state/hashratetargetmodestate/current_target/terahash_per_second", | ||
| ), | ||
| tag: Some("scaled_hashrate"), | ||
| }, | ||
| ), | ||
| ], | ||
| DataField::SerialNumber => vec![( | ||
| WEB_MINER_DETAILS, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/serial_number"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| DataField::Messages => vec![( | ||
| GQL_EVENTS_QUERY, | ||
| DataExtractor { | ||
| func: get_by_pointer, | ||
| key: Some("/events/appeals"), | ||
| tag: None, | ||
| }, | ||
| )], | ||
| _ => vec![], | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl GetIP for BraiinsV2604 { | ||
| fn get_ip(&self) -> IpAddr { | ||
| self.ip | ||
| } | ||
| } | ||
|
|
||
| impl GetDeviceInfo for BraiinsV2604 { | ||
| fn get_device_info(&self) -> DeviceInfo { | ||
| self.device_info.clone() | ||
| } | ||
| } | ||
|
|
||
| impl CollectData for BraiinsV2604 { | ||
| fn get_collector(&self) -> DataCollector<'_> { | ||
| DataCollector::new(self) | ||
| } | ||
| } | ||
|
|
||
| impl GetMAC for BraiinsV2604 { | ||
| fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> { | ||
| data.extract::<String>(DataField::Mac) | ||
| .and_then(|s| MacAddr::from_str(&s).ok()) | ||
| } | ||
| } | ||
|
|
||
| impl GetHostname for BraiinsV2604 { | ||
| fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> { | ||
| data.extract::<String>(DataField::Hostname) | ||
| } | ||
| } | ||
|
|
||
| impl GetApiVersion for BraiinsV2604 { | ||
| fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> { | ||
| let major = data.extract_nested::<f64>(DataField::ApiVersion, "major"); | ||
| let minor = data.extract_nested::<f64>(DataField::ApiVersion, "minor"); | ||
| let patch = data.extract_nested::<f64>(DataField::ApiVersion, "patch"); | ||
|
|
||
| Some(format!("{}.{}.{}", major?, minor?, patch?)) | ||
| } | ||
| } | ||
|
|
||
| impl GetFirmwareVersion for BraiinsV2604 { | ||
| fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> { | ||
| data.extract::<String>(DataField::FirmwareVersion) | ||
| } | ||
| } | ||
|
|
||
| impl GetHashboards for BraiinsV2604 { | ||
| fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> { | ||
| let mut hashboards: Vec<BoardData> = | ||
| (0..self.device_info.hardware.board_count().unwrap_or(0)) | ||
| .map(|idx| { | ||
| BoardData::new(idx, self.device_info.hardware.chips_for_board(idx as usize)) | ||
| }) | ||
| .collect(); | ||
|
|
||
| let Some(chains_array) = data.get(&DataField::Hashboards).and_then(|v| v.as_array()) else { | ||
| return hashboards; | ||
| }; | ||
|
|
||
| for board in hashboards.iter_mut() { | ||
| let Some(chain) = chains_array.iter().find(|c| { | ||
| c.pointer("/id") | ||
| .and_then(|v| v.as_str()) | ||
| .and_then(|s| s.parse::<u8>().ok()) | ||
| .is_some_and(|id| id == board.position + 1) | ||
| }) else { | ||
| continue; | ||
| }; | ||
|
|
||
| let chip_temperature = chain | ||
| .pointer("/highest_chip_temp/temperature/degree_c") | ||
| .and_then(|v| v.as_f64()) | ||
| .map(Temperature::from_celsius); | ||
|
|
||
| board.hashrate = chain | ||
| .pointer("/stats/real_hashrate/last_5s/gigahash_per_second") | ||
| .and_then(|v| v.as_f64()) | ||
| .map(|f| { | ||
| HashRate { | ||
| value: f, | ||
| unit: HashRateUnit::GigaHash, | ||
| algo: "SHA256".to_string(), | ||
| } | ||
| .as_unit(HashRateUnit::default()) | ||
| }); | ||
| board.expected_hashrate = chain | ||
| .pointer("/stats/nominal_hashrate/gigahash_per_second") | ||
| .and_then(|v| v.as_f64()) | ||
| .map(|f| { | ||
| HashRate { | ||
| value: f, | ||
| unit: HashRateUnit::GigaHash, | ||
| algo: "SHA256".to_string(), | ||
| } | ||
| .as_unit(HashRateUnit::default()) | ||
| }); | ||
| board.board_temperature = chain | ||
| .pointer("/board_temp/degree_c") | ||
| .and_then(|v| v.as_f64()) | ||
| .map(Temperature::from_celsius); | ||
| board.inlet_chip_temperature = chip_temperature; | ||
| board.outlet_chip_temperature = chip_temperature; | ||
| board.working_chips = chain | ||
| .pointer("/chips_count") | ||
| .and_then(|v| v.as_u64()) | ||
| .map(|u| u as u16); | ||
| board.serial_number = chain | ||
| .pointer("/serial_number") | ||
| .and_then(|v| v.as_str()) | ||
| .map(|u| u.to_string()); | ||
| board.voltage = chain | ||
| .pointer("/current_voltage/volt") | ||
| .and_then(|v| v.as_f64()) | ||
| .map(Voltage::from_volts); | ||
| board.frequency = chain | ||
| .pointer("/current_frequency/hertz") | ||
| .and_then(|v| v.as_f64()) | ||
| .map(Frequency::from_hertz); | ||
| board.active = chain.pointer("/enabled").and_then(|v| v.as_bool()); | ||
| } | ||
|
|
||
| hashboards | ||
| } | ||
| } | ||
|
|
||
| impl GetHashrate for BraiinsV2604 { | ||
| fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> { | ||
| data.extract_map::<f64, _>(DataField::Hashrate, |f| { | ||
| HashRate { | ||
| value: f, | ||
| unit: HashRateUnit::GigaHash, | ||
| algo: "SHA256".to_string(), | ||
| } | ||
| .as_unit(HashRateUnit::default()) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| impl GetExpectedHashrate for BraiinsV2604 { | ||
| fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> { | ||
| data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| { | ||
| HashRate { | ||
| value: f, | ||
| unit: HashRateUnit::GigaHash, | ||
| algo: "SHA256".to_string(), | ||
| } | ||
| .as_unit(HashRateUnit::default()) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| impl GetFans for BraiinsV2604 { | ||
| fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> { | ||
| let mut fans: Vec<FanData> = Vec::new(); | ||
|
|
||
| if let Some(fans_data) = data.get(&DataField::Fans) | ||
| && let Some(fans_array) = fans_data.as_array() | ||
| { | ||
| for (idx, fan) in fans_array.iter().enumerate() { | ||
| if let Some(rpm) = fan.pointer("/rpm").and_then(|v| v.as_i64()) { | ||
| let pos = fan | ||
| .pointer("/position") | ||
| .and_then(|v| v.as_i64()) | ||
| .unwrap_or(idx as i64); | ||
| fans.push(FanData { | ||
| position: pos as i16, | ||
| rpm: Some(AngularVelocity::from_rpm(rpm as f64)), | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fans | ||
| } | ||
| } | ||
|
|
||
| impl GetLightFlashing for BraiinsV2604 { | ||
| fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> { | ||
| data.extract::<bool>(DataField::LightFlashing) | ||
| } | ||
| } | ||
|
|
||
| impl GetUptime for BraiinsV2604 { | ||
| fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> { | ||
| data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs) | ||
| } | ||
| } | ||
|
|
||
| impl GetIsMining for BraiinsV2604 { | ||
| fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool { | ||
| data.extract::<u64>(DataField::IsMining) == Some(2) | ||
| } | ||
| } | ||
|
|
||
| impl GetPools for BraiinsV2604 { | ||
| fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolGroupData> { | ||
| let mut pools: Vec<PoolData> = Vec::new(); | ||
|
|
||
| if let Some(pools_data) = data.get(&DataField::Pools) | ||
| && let Some(pools_array) = pools_data.as_array() | ||
| { | ||
| for (idx, pool) in pools_array.iter().enumerate() { | ||
| let url = pool | ||
| .pointer("/url") | ||
| .and_then(|v| v.as_str()) | ||
| .map(String::from) | ||
| .map(PoolURL::from); | ||
|
|
||
| let user = pool | ||
| .pointer("/user") | ||
| .and_then(|v| v.as_str()) | ||
| .map(String::from); | ||
|
|
||
| let accepted_shares = pool | ||
| .pointer("/stats/accepted_shares") | ||
| .and_then(|v| v.as_u64()); | ||
| let rejected_shares = pool | ||
| .pointer("/stats/rejected_shares") | ||
| .and_then(|v| v.as_u64()); | ||
| let active = pool.pointer("/active").and_then(|v| v.as_bool()); | ||
| let alive = pool.pointer("/alive").and_then(|v| v.as_bool()); | ||
|
|
||
| pools.push(PoolData { | ||
| position: Some(idx as u16), | ||
| url, | ||
| accepted_shares, | ||
| rejected_shares, | ||
| active, | ||
| alive, | ||
| user, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| vec![PoolGroupData { | ||
| name: String::new(), | ||
| quota: 1, | ||
| pools, | ||
| }] | ||
| } | ||
| } | ||
|
|
||
| impl GetSerialNumber for BraiinsV2604 { | ||
| fn parse_serial_number(&self, data: &HashMap<DataField, Value>) -> Option<String> { | ||
| data.extract::<String>(DataField::SerialNumber) | ||
| } | ||
| } | ||
|
|
||
| impl GetControlBoardVersion for BraiinsV2604 { | ||
| fn parse_control_board_version( | ||
| &self, | ||
| data: &HashMap<DataField, Value>, | ||
| ) -> Option<MinerControlBoard> { | ||
| let cb_type = data.extract::<u64>(DataField::ControlBoardVersion)?; | ||
| match cb_type { | ||
| 0 => None, | ||
| 1 => Some(AntMinerControlBoard::CVITek).map(|cb| cb.into()), | ||
| 2 => Some(AntMinerControlBoard::BeagleBoneBlack).map(|cb| cb.into()), | ||
| 3 => Some(AntMinerControlBoard::AMLogic).map(|cb| cb.into()), | ||
| 4 => Some(AntMinerControlBoard::Xilinx).map(|cb| cb.into()), | ||
| 5 => Some(BraiinsControlBoard::BraiinsCB).map(|cb| cb.into()), | ||
| _ => None, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl GetWattage for BraiinsV2604 { | ||
| fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> { | ||
| data.extract_map::<i64, _>(DataField::Wattage, |w| Power::from_watts(w as f64)) | ||
| } | ||
| } | ||
|
|
||
| impl GetTuningTarget for BraiinsV2604 { | ||
| fn parse_tuning_target(&self, data: &HashMap<DataField, Value>) -> Option<TuningTarget> { | ||
| data.get(&DataField::TuningTarget) | ||
| .and_then(parse_configured_tuning_target) | ||
| } | ||
| } | ||
|
|
||
| impl GetScaledTuningTarget for BraiinsV2604 { | ||
| fn parse_scaled_tuning_target(&self, data: &HashMap<DataField, Value>) -> Option<TuningTarget> { | ||
| data.get(&DataField::TuningTarget) | ||
| .and_then(parse_scaled_tuning_target) | ||
| } | ||
| } | ||
|
|
||
| impl GetFluidTemperature for BraiinsV2604 {} | ||
| #[async_trait] | ||
| impl SupportsTimezoneConfig for BraiinsV2604 { | ||
| fn supports_timezone_config(&self) -> bool { | ||
| true | ||
| } | ||
|
|
||
| fn parse_timezone_config( | ||
| &self, | ||
| data: &HashMap<ConfigField, Value>, | ||
| ) -> anyhow::Result<TimezoneConfig> { | ||
| let obj = data | ||
| .get(&ConfigField::Timezone) | ||
| .ok_or_else(|| anyhow::anyhow!("No timezone data returned"))?; | ||
| let timezone = obj | ||
| .pointer("/timezone/id") | ||
| .and_then(|v| v.as_str()) | ||
| .map(String::from); | ||
| let available = obj | ||
| .pointer("/timezoneList") | ||
| .and_then(|v| v.as_array()) | ||
| .map(|arr| { | ||
| arr.iter() | ||
| .filter_map(|t| t.get("id").and_then(|v| v.as_str()).map(String::from)) | ||
| .collect() | ||
| }) | ||
| .unwrap_or_default(); | ||
| Ok(TimezoneConfig { | ||
| timezone, | ||
| available, | ||
| }) | ||
| } | ||
|
|
||
| async fn set_timezone_config(&self, config: TimezoneConfig) -> anyhow::Result<bool> { | ||
| let timezone = match config.timezone { | ||
| Some(tz) => tz, | ||
| None => anyhow::bail!("Timezone config has no timezone to set"), | ||
| }; | ||
| let mutation = r#"mutation ($tz: String!) { | ||
| bos { setTimezone(timezone: $tz) { __typename } } | ||
| }"#; | ||
| let variables = json!({ "tz": timezone }); | ||
| let result = self | ||
| .graphql | ||
| .send_graphql_command(mutation, true, Some(variables)) | ||
| .await?; | ||
| // BosResult is a union; a `BosError` variant signals failure. | ||
| let typename = result | ||
| .pointer("/bos/setTimezone/__typename") | ||
| .and_then(|v| v.as_str()); | ||
| Ok(matches!(typename, Some(t) if t != "BosError")) | ||
| } | ||
| } |
There was a problem hiding this comment.
Copy paste this impl for every braiins version , for some reason this isn't in the REST api.
| tag: None, | ||
| }, | ||
| )], | ||
| ConfigField::Timezone => vec![], |
There was a problem hiding this comment.
Doesn't need to exist? Rather a _ => vec![],
| ConfigField::Timezone => vec![], | |
| _ => vec![], |
| const DEFAULT_OFFSETS: &[&str] = &[ | ||
| "GMT-12", "GMT-11", "GMT-10", "GMT-9", "GMT-8", "GMT-7", "GMT-6", "GMT-5", "GMT-4", | ||
| "GMT-3", "GMT-2", "GMT-1", "GMT+0", "GMT+1", "GMT+2", "GMT+3", "GMT+4", "GMT+5", | ||
| "GMT+6", "GMT+7", "GMT+8", "GMT+9", "GMT+10", "GMT+11", "GMT+12", "GMT+13", "GMT+14", | ||
| ]; |
There was a problem hiding this comment.
Do we pull in any "real" timezone objects in any the deps? Would be nice to be able to go to/from a properly structure timezone object for this, get more type hinting rather than just strings.
…undation#295) Timezone config-model on current master: SupportsTimezoneConfig + TimezoneConfig under the config-collector; VNish 1.2.x + 1.3.x read/set the fixed UTC offset from /settings /regional/timezone (fallback whole-hour offsets); BraiinsOS reads its timezone list; python bindings + class registration. Single feature on master.
c7d0f1e to
4a41821
Compare
…undation#295) Timezone config-model on current master: SupportsTimezoneConfig + TimezoneConfig under the config-collector; VNish 1.2.x + 1.3.x read/set the fixed UTC offset from /settings /regional/timezone (fallback whole-hour offsets); BraiinsOS reads its timezone list; python bindings + class registration. Single feature on master.
4a41821 to
2742c43
Compare
Why I need this: on VNish the timezone is a fixed UTC offset with no DST handling, so it has to be switched by hand twice a year (summer/winter) — and that gets forgotten, leaving the miner's clock (and its logs/timestamps) an hour off for months. Keeping a device's clock correct across DST is exactly the kind of thing a controller like Home Assistant should own, so I want to be able to read and set the timezone from there instead of remembering to do it on each miner.
This adds a
SetTimezonecapability (get / set / list + supports) so a consumer can do that:SetTimezonetrait (default: unsupported), added to theMinersupertrait + blanket impl; all backends implement it (trivial except the two below).bos.timezone/timezoneList/setTimezone— once a named zone is set, BOS handles DST itself./settingsregional.timezone(read-modify-write); since VNish has no DST awareness, the consumer (re)applies the correct offset at each DST change.get_timezone/list_timezones/set_timezone+supports_set_timezone.CI-green; live write-test pending on my fleet. Happy to shape the API (e.g. a typed timezone, or naming) to your preference.