Compare commits

...

6 commits

13 changed files with 148 additions and 29 deletions

10
backend/Cargo.lock generated
View file

@ -1170,7 +1170,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "powertools" name = "powertools"
version = "2.0.2" version = "2.0.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@ -1481,9 +1481,9 @@ dependencies = [
[[package]] [[package]]
name = "smokepatio" name = "smokepatio"
version = "0.1.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626ef8beee78bebc397d841469fa47bf7e370ddb8b8f3e628e69b03bf968d089" checksum = "3416e8c907d171c4334df3933873c32bff97ca5ad7ae0ee93e6268e04e2041ef"
dependencies = [ dependencies = [
"embedded-io", "embedded-io",
"log", "log",
@ -1540,9 +1540,9 @@ dependencies = [
[[package]] [[package]]
name = "sysfuss" name = "sysfuss"
version = "0.2.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fa4dd5879b3fd41aff63991a59970cdfeced6f0d5920c5da0937279904d9f45" checksum = "f33bae529511a671b5f2ed4cc46ae0b2ccdf8c03ccf7eebe95a5a886ff7914dc"
[[package]] [[package]]
name = "termcolor" name = "termcolor"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "powertools" name = "powertools"
version = "2.0.2" version = "2.0.3"
edition = "2021" edition = "2021"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"] authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
description = "Backend (superuser) functionality for PowerTools" description = "Backend (superuser) functionality for PowerTools"
@ -16,7 +16,7 @@ usdpl-back = { version = "0.10.1", features = ["blocking", "decky"] }#, path = "
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
ron = "0.8" ron = "0.8"
sysfuss = { version = "0.2", features = ["derive"] }#,path = "../../sysfs-nav"} sysfuss = { version = "0.3", features = ["derive"] }#,path = "../../sysfs-nav"}
# async # async
tokio = { version = "*", features = ["time"] } tokio = { version = "*", features = ["time"] }
@ -31,7 +31,7 @@ limits_core = { version = "3", path = "./limits_core" }
regex = "1" regex = "1"
# steam deck libs # steam deck libs
smokepatio = { version = "0.1", features = [ "std" ] } smokepatio = { version = "0.2", default-features = false }
libc = "0.2" libc = "0.2"
# online settings # online settings

View file

@ -34,8 +34,8 @@ impl GenericBatteryLimit {
fn default_steam_deck() -> Self { fn default_steam_deck() -> Self {
Self { Self {
charge_rate: Some(RangeLimit { charge_rate: Some(RangeLimit {
min: Some(250), min: Some(0),
max: Some(2500), max: Some(100),
}), }),
charge_modes: vec![ charge_modes: vec![
"normal".to_owned(), "normal".to_owned(),

View file

@ -32,6 +32,30 @@ pub enum ApiMessage {
UploadCurrentVariant(String, String), // SteamID, Steam username UploadCurrentVariant(String, String), // SteamID, Steam username
} }
impl core::fmt::Display for ApiMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Battery(x) => write!(f, "Battery;{}", x),
Self::Cpu(x) => write!(f, "Cpu;{}", x),
Self::Gpu(x) => write!(f, "Gpu;{}", x),
Self::General(x) => write!(f, "General;{}", x),
Self::OnResume => write!(f, "OnResume"),
Self::OnPluggedIn => write!(f, "OnPluggedIn"),
Self::OnUnplugged => write!(f, "OnUnplugged"),
Self::OnChargeChange(x) => write!(f, "OnChargeChange({:?})", x),
Self::PowerVibeCheck => write!(f, "PowerVibeCheck"),
Self::WaitForEmptyQueue(_) => write!(f, "WaitForEmptyQueue"),
Self::LoadSettings(path, name, variant, variant_name) => write!(f, "LoadSettings({}, {}, {}, {})", path, name, variant, variant_name),
Self::LoadVariant(variant, variant_name) => write!(f, "LoadVariant({}, {})", variant, variant_name),
Self::LoadMainSettings => write!(f, "LoadMainSettings"),
Self::LoadSystemSettings => write!(f, "LoadSystemSettings"),
Self::GetLimits(_) => write!(f, "GetLimits"),
Self::GetProvider(s, _) => write!(f, "GetProvider({})", s),
Self::UploadCurrentVariant(id, user) => write!(f, "UploadCurrentVariant(id: {}, user: {})", id, user),
}
}
}
pub enum BatteryMessage { pub enum BatteryMessage {
SetChargeRate(Option<u64>), SetChargeRate(Option<u64>),
GetChargeRate(Callback<Option<u64>>), GetChargeRate(Callback<Option<u64>>),
@ -46,6 +70,24 @@ pub enum BatteryMessage {
GetChargeLimit(Callback<Option<f64>>), GetChargeLimit(Callback<Option<f64>>),
} }
impl core::fmt::Display for BatteryMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetChargeRate(x) => write!(f, "SetChargeRate({:?})", x),
Self::GetChargeRate(_) => write!(f, "GetChargeRate"),
Self::SetChargeMode(x) => write!(f, "SetChargeMode({:?})", x),
Self::GetChargeMode(_) => write!(f, "GetChargeMode"),
Self::ReadChargeFull(_) => write!(f, "ReadChargeFull"),
Self::ReadChargeNow(_) => write!(f, "ReadChargeNow"),
Self::ReadChargeDesign(_) => write!(f, "ReadChargeDesign"),
Self::ReadCurrentNow(_) => write!(f, "ReadCurrentNow"),
Self::ReadChargePower(_) => write!(f, "ReadChargePower"),
Self::SetChargeLimit(x) => write!(f, "SetChargeLimit({:?})", x),
Self::GetChargeLimit(_) => write!(f, "GetChargeLimit"),
}
}
}
impl BatteryMessage { impl BatteryMessage {
fn process(self, settings: &mut dyn TBattery) -> bool { fn process(self, settings: &mut dyn TBattery) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -87,6 +129,23 @@ pub enum CpuMessage {
GetCpusGovernor(Callback<Vec<String>>), GetCpusGovernor(Callback<Vec<String>>),
} }
impl core::fmt::Display for CpuMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetCpuOnline(i, x) => write!(f, "SetCpuOnline({}, {})", i, x),
Self::SetCpusOnline(x) => write!(f, "SetCpusOnline({:?})", x),
Self::SetSmt(x, _) => write!(f, "SetChargeMode({})", x),
Self::GetSmt(_) => write!(f, "GetSmt"),
Self::GetCpusOnline(_) => write!(f, "GetCpusOnline"),
Self::SetClockLimits(x, y) => write!(f, "SetClockLimits({}, {:?})", x, y),
Self::GetClockLimits(x, _) => write!(f, "GetClockLimits({})", x),
Self::SetCpuGovernor(i, x) => write!(f, "SetCpuGovernor({}, {})", i, x),
Self::SetCpusGovernor(x) => write!(f, "SetCpusGovernor({:?})", x),
Self::GetCpusGovernor(_) => write!(f, "GetCpusGovernor"),
}
}
}
impl CpuMessage { impl CpuMessage {
fn process(self, settings: &mut dyn TCpus) -> bool { fn process(self, settings: &mut dyn TCpus) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -206,6 +265,19 @@ pub enum GpuMessage {
GetMemoryClock(Callback<Option<u64>>), GetMemoryClock(Callback<Option<u64>>),
} }
impl core::fmt::Display for GpuMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetPpt(x, y) => write!(f, "SetPpt(fast {:?}, slow {:?})", x, y),
Self::GetPpt(_) => write!(f, "GetPpt"),
Self::SetClockLimits(x) => write!(f, "SetClockLimits({:?})", x),
Self::GetClockLimits(_) => write!(f, "GetClockLimits"),
Self::SetMemoryClock(x) => write!(f, "SetMemoryClock({:?})", x),
Self::GetMemoryClock(_) => write!(f, "GetMemoryClock"),
}
}
}
impl GpuMessage { impl GpuMessage {
fn process(self, settings: &mut dyn TGpu) -> bool { fn process(self, settings: &mut dyn TGpu) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -242,6 +314,21 @@ pub enum GeneralMessage {
ApplyNow, ApplyNow,
} }
impl core::fmt::Display for GeneralMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetPersistent(x) => write!(f, "SetPersistent({})", x),
Self::GetPersistent(_) => write!(f, "GetPersistent"),
Self::GetCurrentProfileName(_) => write!(f, "GetCurrentProfileName"),
Self::GetPath(_) => write!(f, "GetPath"),
Self::GetCurrentVariant(_) => write!(f, "GetCurrentVariant"),
Self::GetAllVariants(_) => write!(f, "GetAllVariants"),
Self::AddVariant(variant, _) => write!(f, "AddVariant(name: `{}` [...])", variant.name),
Self::ApplyNow => write!(f, "ApplyNow"),
}
}
}
impl GeneralMessage { impl GeneralMessage {
fn process(self, settings: &mut dyn TGeneral) -> bool { fn process(self, settings: &mut dyn TGeneral) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -285,20 +372,31 @@ fn print_errors(call_name: &str, errors: Vec<crate::settings::SettingError>) {
log::error!("Settings {}() err:\n{}", call_name, err_list); log::error!("Settings {}() err:\n{}", call_name, err_list);
} }
fn print_messages(msgs: &Vec<String>) {
let mut log_msg = String::new();
for msg in msgs.iter() {
//use core::fmt::Write;
write!(log_msg, "{}, ", msg).unwrap();
}
log::info!("Processed messages: [{}]", log_msg);
}
impl ApiMessageHandler { impl ApiMessageHandler {
pub fn process_forever(&mut self, settings: &mut Settings) { pub fn process_forever(&mut self, settings: &mut Settings) {
crate::utility::ioperm_power_ec(); crate::utility::ioperm_power_ec();
//let mut dirty_echo = true; // set everything twice, to make sure PowerTools wins on race conditions //let mut dirty_echo = true; // set everything twice, to make sure PowerTools wins on race conditions
while let Ok(msg) = self.intake.recv() { while let Ok(msg) = self.intake.recv() {
let mut messages = vec![msg.to_string()]; // keep messages for logging
let mut dirty = self.process(settings, msg); let mut dirty = self.process(settings, msg);
while let Ok(msg) = self.intake.try_recv() { while let Ok(msg) = self.intake.try_recv() {
messages.push(msg.to_string());
dirty |= self.process(settings, msg); dirty |= self.process(settings, msg);
} }
if dirty if dirty
/*|| dirty_echo */ /*|| dirty_echo */
{ {
//dirty_echo = dirty; // echo only once //dirty_echo = dirty; // echo only once
print_messages(&messages);
// run on_set // run on_set
if let Err(e) = settings.on_set() { if let Err(e) = settings.on_set() {
print_errors("on_set", e); print_errors("on_set", e);

View file

@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex};
use sysfuss::capability::attributes; use sysfuss::capability::attributes;
use sysfuss::{ use sysfuss::{
HwMonAttribute, HwMonAttributeItem, HwMonAttributeType, HwMonPath, PowerSupplyAttribute, HwMonAttribute, HwMonAttributeItem, HwMonAttributeType, HwMonPath, PowerSupplyAttribute,
PowerSupplyPath, SysAttribute, SysEntity, SysEntityAttributesExt, PowerSupplyPath, SysEntity, SysEntityAttributesExt, SysAttributeExt,
}; };
use limits_core::json_v2::GenericBatteryLimit; use limits_core::json_v2::GenericBatteryLimit;
@ -230,12 +230,12 @@ const HWMON_NEEDS: &[HwMonAttribute] = &[
]; ];
const MAX_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute = const MAX_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute =
HwMonAttribute::custom("maximum_battery_charge_rate"); HwMonAttribute::custom("max_battery_charge_rate");
const MAX_BATTERY_CHARGE_LEVEL_ATTR: HwMonAttribute = const MAX_BATTERY_CHARGE_LEVEL_ATTR: HwMonAttribute =
HwMonAttribute::custom("max_battery_charge_level"); HwMonAttribute::custom("max_battery_charge_level");
const MAX_CHARGE_RATE: u64 = 2500; const MAX_CHARGE_RATE: u64 = 100;
const MIN_CHARGE_RATE: u64 = 250; const MIN_CHARGE_RATE: u64 = 0;
impl Battery { impl Battery {
fn find_battery_sysfs(root: Option<impl AsRef<std::path::Path>>) -> PowerSupplyPath { fn find_battery_sysfs(root: Option<impl AsRef<std::path::Path>>) -> PowerSupplyPath {
@ -325,7 +325,7 @@ impl Battery {
self.state.charge_rate_set = true; self.state.charge_rate_set = true;
let path = MAX_BATTERY_CHARGE_RATE_ATTR.path(&*self.sysfs_hwmon); let path = MAX_BATTERY_CHARGE_RATE_ATTR.path(&*self.sysfs_hwmon);
self.sysfs_hwmon self.sysfs_hwmon
.set(MAX_BATTERY_CHARGE_RATE_ATTR, charge_rate) .set(MAX_BATTERY_CHARGE_RATE_ATTR, format!("{}\n", charge_rate))
.map_err(|e| SettingError { .map_err(|e| SettingError {
msg: format!("Failed to write to `{}`: {}", path.display(), e), msg: format!("Failed to write to `{}`: {}", path.display(), e),
setting: crate::settings::SettingVariant::Battery, setting: crate::settings::SettingVariant::Battery,
@ -336,10 +336,10 @@ impl Battery {
self.sysfs_hwmon self.sysfs_hwmon
.set( .set(
MAX_BATTERY_CHARGE_RATE_ATTR, MAX_BATTERY_CHARGE_RATE_ATTR,
self.limits format!("{}\n", self.limits
.charge_rate .charge_rate
.and_then(|lim| lim.max) .and_then(|lim| lim.max)
.unwrap_or(2500), .unwrap_or(100)),
) )
.map_err(|e| SettingError { .map_err(|e| SettingError {
msg: format!("Failed to write to `{}`: {}", path.display(), e), msg: format!("Failed to write to `{}`: {}", path.display(), e),

View file

@ -1,8 +1,7 @@
use std::convert::Into; use std::convert::Into;
use sysfuss::{ use sysfuss::{
capability::attributes, BasicEntityPath, HwMonPath, SysAttribute, SysEntity, capability::attributes, BasicEntityPath, HwMonPath, SysEntity, SysEntityAttributesExt, SysAttributeExt,
SysEntityAttributes, SysEntityAttributesExt,
}; };
use limits_core::json_v2::GenericGpuLimit; use limits_core::json_v2::GenericGpuLimit;
@ -151,7 +150,7 @@ impl Gpu {
if let super::Model::OLED = self.variant { if let super::Model::OLED = self.variant {
if let Ok(f) = self if let Ok(f) = self
.sysfs_card .sysfs_card
.read_value(GPU_CLOCK_READOUT_ATTRIBUTE.to_owned()) .read_value(&GPU_CLOCK_READOUT_ATTRIBUTE.to_owned())
{ {
let options = parse_pp_dpm_sclk(&String::from_utf8_lossy(&f)); let options = parse_pp_dpm_sclk(&String::from_utf8_lossy(&f));
return options return options
@ -192,7 +191,7 @@ impl Gpu {
fn quantize_memory_clock(&self, clock: u64) -> u64 { fn quantize_memory_clock(&self, clock: u64) -> u64 {
if let Ok(f) = self if let Ok(f) = self
.sysfs_card .sysfs_card
.read_value(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned()) .read_value(&GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned())
{ {
let options = parse_pp_dpm_fclk(&String::from_utf8_lossy(&f)); let options = parse_pp_dpm_fclk(&String::from_utf8_lossy(&f));
// round (and find) nearest valid clock step // round (and find) nearest valid clock step
@ -238,7 +237,7 @@ impl Gpu {
{ {
let options_count = self let options_count = self
.sysfs_card .sysfs_card
.read_value(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned()) .read_value(&GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned())
.map(|b| parse_pp_dpm_fclk(&String::from_utf8_lossy(&b)).len()) .map(|b| parse_pp_dpm_fclk(&String::from_utf8_lossy(&b)).len())
.unwrap_or_else(|_| if is_oled { 4 } else { 2 }); .unwrap_or_else(|_| if is_oled { 4 } else { 2 });
let modifier = (options_count - 1) as u64; let modifier = (options_count - 1) as u64;

View file

@ -5,7 +5,7 @@
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use sysfuss::{BasicEntityPath, SysAttribute, SysEntityAttributesExt}; use sysfuss::{BasicEntityPath, SysEntityAttributesExt, SysAttributeExt};
use crate::settings::SettingError; use crate::settings::SettingError;

View file

@ -1,9 +1,13 @@
#![allow(dead_code)] #![allow(dead_code)]
use std::sync::Mutex;
pub const JUPITER_HWMON_NAME: &'static str = "jupiter"; pub const JUPITER_HWMON_NAME: &'static str = "jupiter";
pub const STEAMDECK_HWMON_NAME: &'static str = "steamdeck_hwmon"; pub const STEAMDECK_HWMON_NAME: &'static str = "steamdeck_hwmon";
pub const GPU_HWMON_NAME: &'static str = "amdgpu"; pub const GPU_HWMON_NAME: &'static str = "amdgpu";
pub static THING_EC: Mutex<smokepatio::ec::unnamed_power::UnnamedPowerEC> = Mutex::new(smokepatio::ec::unnamed_power::UnnamedPowerEC::new());
pub fn range_min_or_fallback<I: Copy>( pub fn range_min_or_fallback<I: Copy>(
range: &Option<limits_core::json_v2::RangeLimit<I>>, range: &Option<limits_core::json_v2::RangeLimit<I>>,
fallback: I, fallback: I,
@ -25,6 +29,7 @@ pub fn card_also_has(card: &dyn sysfuss::SysEntity, extensions: &'static [&'stat
} }
const THINGS: &[u8] = &[ const THINGS: &[u8] = &[
0, 0, 0,
1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1,
1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0,
@ -35,13 +40,14 @@ const TIME_UNIT: std::time::Duration = std::time::Duration::from_millis(250);
pub fn flash_led() { pub fn flash_led() {
use smokepatio::ec::ControllerSet; use smokepatio::ec::ControllerSet;
let mut ec = smokepatio::ec::unnamed_power::UnnamedPowerEC::new();
let mut ec = THING_EC.lock().unwrap();
for &code in THINGS { for &code in THINGS {
let on = code != 0; let on = code != 0;
let colour = if on { let colour = if on {
smokepatio::ec::unnamed_power::StaticColour::Red smokepatio::ec::unnamed_power::StaticColour::Red
} else { } else {
smokepatio::ec::unnamed_power::StaticColour::Off smokepatio::ec::unnamed_power::StaticColour::Disabled
}; };
if let Err(e) = ec.set(colour) { if let Err(e) = ec.set(colour) {
log::error!("Thing err: {}", e); log::error!("Thing err: {}", e);

View file

@ -1,6 +1,6 @@
{ {
"name": "PowerTools", "name": "PowerTools",
"version": "2.0.2", "version": "2.0.3",
"description": "Power tweaks for power users", "description": "Power tweaks for power users",
"scripts": { "scripts": {
"build": "shx rm -rf dist && rollup -c", "build": "shx rm -rf dist && rollup -c",

View file

@ -85,7 +85,7 @@ export class Battery extends Component<backend.IdcProps> {
}} }}
/> />
{ get_value(CHARGE_RATE_BATT) != null && <SliderField { get_value(CHARGE_RATE_BATT) != null && <SliderField
label={tr("Maximum (mA)")} label={tr("Maximum (%)")}
value={get_value(CHARGE_RATE_BATT)} value={get_value(CHARGE_RATE_BATT)}
max={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.max} max={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.max}
min={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.min} min={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.min}

BIN
translations/en-US.mo Normal file

Binary file not shown.

16
translations/en-US.po Normal file
View file

@ -0,0 +1,16 @@
# TEMPLATE TITLE.
# Copyright (C) 2024 NGnius
# This file is distributed under the same license as the PowerTools package.
# NGnius (Graham) <ngniusness@gmail.com>, 2024.
msgid ""
msgstr ""
"Project-Id-Version: v1.1\n"
"Report-Msgid-Bugs-To: https://git.ngni.us/NG-SD-Plugins/PowerTools/PowerTools/issues\n"
"POT-Creation-Date: 2023-01-09 19:52-0500\n"
"PO-Revision-Date: 2024-05-07 18:42-0500\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: en-US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"

View file

@ -5,7 +5,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: v1.1\n" "Project-Id-Version: v1.1\n"
"Report-Msgid-Bugs-To: https://github.com/NGnius/PowerTools/issues\n" "Report-Msgid-Bugs-To: https://git.ngni.us/NG-SD-Plugins/PowerTools/issues\n"
"POT-Creation-Date: 2023-01-09 19:52-0500\n" "POT-Creation-Date: 2023-01-09 19:52-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"