Last active
February 21, 2018 18:29
-
-
Save Agyar/5679d5d9e4d76ec11ee991b18f0b6abe to your computer and use it in GitHub Desktop.
py3status Toggle <3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
""" | |
Display battery information. | |
Configuration parameters: | |
battery_id: id of the battery to be displayed | |
set to 'all' for combined display of all batteries | |
(default 0) | |
blocks: a string, where each character represents battery level | |
especially useful when using icon fonts (e.g. FontAwesome) | |
(default "_▁▂▃▄▅▆▇█") | |
cache_timeout: a timeout to refresh the battery state | |
(default 30) | |
charging_character: a character to represent charging battery | |
especially useful when using icon fonts (e.g. FontAwesome) | |
(default "⚡") | |
format: string that formats the output. See placeholders below. | |
(default "{icon}") | |
format_notify_charging: format of the notification received when you click | |
on the module while your computer is plugged in | |
(default 'Charging ({percent}%)') | |
format_notify_discharging: format of the notification received when you | |
click on the module while your computer is not plugged in | |
(default "{time_remaining}") | |
hide_seconds: hide seconds in remaining time | |
(default False) | |
hide_when_full: hide any information when battery is fully charged (when | |
the battery level is greater than or equal to 'threshold_full') | |
(default False) | |
measurement_mode: either 'acpi' or 'sys', or None to autodetect. 'sys' | |
should be more robust and does not have any extra requirements, however | |
the time measurement may not work in some cases | |
(default None) | |
notification: show current battery state as notification on click | |
(default False) | |
notify_low_level: display notification when battery is running low (when | |
the battery level is less than 'threshold_degraded') | |
(default False) | |
sys_battery_path: set the path to your battery(ies), without including its | |
number | |
(default "/sys/class/power_supply/") | |
threshold_bad: a percentage below which the battery level should be | |
considered bad | |
(default 10) | |
threshold_degraded: a percentage below which the battery level should be | |
considered degraded | |
(default 30) | |
threshold_full: a percentage at or above which the battery level should | |
should be considered full | |
(default 100) | |
Format placeholders: | |
{ascii_bar} - a string of ascii characters representing the battery level, | |
an alternative visualization to '{icon}' option | |
{icon} - a character representing the battery level, | |
as defined by the 'blocks' and 'charging_character' parameters | |
{percent} - the remaining battery percentage (previously '{}') | |
{time_remaining} - the remaining time until the battery is empty | |
Color options: | |
color_bad: Battery level is below threshold_bad | |
color_charging: Battery is charging (default "#FCE94F") | |
color_degraded: Battery level is below threshold_degraded | |
color_good: Battery level is above thresholds | |
Requires: | |
- the `acpi` the acpi command line utility (only if | |
`measurement_mode='acpi'`) | |
@author shadowprince, AdamBSteele, maximbaz, 4iar, m45t3r, Agyar | |
@license Eclipse Public License | |
SAMPLE OUTPUT | |
{'color': '#FCE94F', 'full_text': u'\u26a1'} | |
discharging | |
{'color': '#FF0000', 'full_text': u'\u2340'} | |
""" | |
from __future__ import division # python2 compatibility | |
from re import findall | |
from glob import iglob | |
import math | |
import os | |
BLOCKS = u"_▁▂▃▄▅▆▇█" | |
CHARGING_CHARACTER = u"⚡" | |
EMPTY_BLOCK_CHARGING = u'|' | |
EMPTY_BLOCK_DISCHARGING = u'⍀' | |
FULL_BLOCK = u'█' | |
FORMAT = u"{icon}" | |
FORMAT_NOTIFY_CHARGING = u"Charging ({percent}%)" | |
FORMAT_NOTIFY_DISCHARGING = u"{time_remaining}" | |
SYS_BATTERY_PATH = u"/sys/class/power_supply/" | |
MEASUREMENT_MODE = None | |
FULLY_CHARGED = u'?' | |
class Py3status: | |
""" | |
""" | |
# available configuration parameters | |
battery_id = 0 | |
blocks = BLOCKS | |
cache_timeout = 30 | |
charging_character = CHARGING_CHARACTER | |
format = FORMAT | |
format_notify_charging = FORMAT_NOTIFY_CHARGING | |
format_notify_discharging = FORMAT_NOTIFY_DISCHARGING | |
hide_seconds = False | |
hide_when_full = False | |
measurement_mode = MEASUREMENT_MODE | |
notification = False | |
notify_low_level = False | |
sys_battery_path = SYS_BATTERY_PATH | |
threshold_bad = 10 | |
threshold_degraded = 30 | |
threshold_full = 100 | |
button_toggle = 1 | |
toggle = False | |
class Meta: | |
deprecated = { | |
'format_fix_unnamed_param': [ | |
{ | |
'param': 'format', | |
'placeholder': 'percent', | |
'msg': '{} should not be used in format use `{percent}`', | |
}, | |
], | |
'substitute_by_value': [ | |
{ | |
'param': 'mode', | |
'value': 'ascii_bar', | |
'substitute': { | |
'param': 'format', | |
'value': '{ascii_bar}', | |
}, | |
'msg': 'obsolete parameter use `format = "{ascii_bar}"`', | |
}, | |
{ | |
'param': 'mode', | |
'value': 'text', | |
'substitute': { | |
'param': 'format', | |
'value': 'Battery: {percent}', | |
}, | |
'msg': 'obsolete parameter use `format = "{percent}"`', | |
}, | |
{ | |
'param': 'show_percent_with_blocks', | |
'value': True, | |
'substitute': { | |
'param': 'format', | |
'value': '{icon} {percent}%', | |
}, | |
'msg': 'obsolete parameter use `format = "{icon} {percent}%"`', | |
}, | |
], | |
} | |
def post_config_hook(self): | |
self.last_known_status = '' | |
# Guess mode if not set | |
if self.measurement_mode is None: | |
if self.py3.check_commands(["acpi"]): | |
self.measurement_mode = "acpi" | |
elif os.path.isdir(self.sys_battery_path): | |
self.measurement_mode = "sys" | |
self.py3.log("Measurement mode: " + self.measurement_mode) | |
if self.measurement_mode != "acpi" and self.measurement_mode != "sys": | |
raise NameError("Invalid measurement mode") | |
def battery_level(self): | |
if not os.listdir(self.sys_battery_path): | |
return { | |
"full_text": "", | |
"cached_until": self.py3.time_in(self.cache_timeout) | |
} | |
self._refresh_battery_info() | |
self._update_icon() | |
self._update_ascii_bar() | |
self._update_full_text() | |
return self._build_response() | |
def on_click(self, event): | |
""" | |
Display a notification following the specified format | |
""" | |
button = event['button'] | |
if button == self.button_toggle: | |
self.toggle = not self.toggle | |
if not self.notification: | |
return | |
if self.charging: | |
format = self.format_notify_charging | |
else: | |
format = self.format_notify_discharging | |
message = self.py3.safe_format(format, | |
dict(ascii_bar=self.ascii_bar, | |
icon=self.icon, | |
is_toggled=self.toggle, | |
percent=self.percent_charged, | |
time_remaining=self.time_remaining)) | |
if message: | |
self.py3.notify_user(message, 'info') | |
def _extract_battery_information_from_acpi(self): | |
""" | |
Get the battery info from acpi | |
# Example acpi -bi raw output (Discharging): | |
Battery 0: Discharging, 94%, 09:23:28 remaining | |
Battery 0: design capacity 5703 mAh, last full capacity 5283 mAh = 92% | |
Battery 1: Unknown, 98% | |
Battery 1: design capacity 1880 mAh, last full capacity 1370 mAh = 72% | |
# Example Charging | |
Battery 0: Charging, 96%, 00:20:40 until charged | |
Battery 0: design capacity 5566 mAh, last full capacity 5156 mAh = 92% | |
Battery 1: Unknown, 98% | |
Battery 1: design capacity 1879 mAh, last full capacity 1370 mAh = 72% | |
""" | |
def _parse_battery_info(acpi_battery_lines): | |
battery = {} | |
battery["percent_charged"] = int(findall("(?<= )(\d+)(?=%)", | |
acpi_battery_lines[0])[0]) | |
battery["charging"] = "Charging" in acpi_battery_lines[0] | |
battery["capacity"] = int(findall("(?<= )(\d+)(?= mAh)", | |
acpi_battery_lines[1])[1]) | |
# ACPI only shows time remaining if battery is discharging or | |
# charging | |
try: | |
battery["time_remaining"] = ''.join(findall( | |
"(?<=, )(\d+:\d+:\d+)(?= remaining)|" | |
"(?<=, )(\d+:\d+:\d+)(?= until)", acpi_battery_lines[0])[0]) | |
except IndexError: | |
battery["time_remaining"] = FULLY_CHARGED | |
return battery | |
acpi_list = self.py3.command_output(["acpi", "-b", "-i"]).splitlines() | |
# Separate the output because each pair of lines corresponds to a | |
# single battery. Now the list index will correspond to the index of | |
# the battery we want to look at | |
acpi_list = [acpi_list[i:i + 2] | |
for i in range(0, len(acpi_list) - 1, 2)] | |
return [_parse_battery_info(battery) for battery in acpi_list] | |
def _extract_battery_information_from_sys(self): | |
""" | |
Extract the percent charged, charging state, time remaining, | |
and capacity for a battery, using Linux's kernel /sys interface | |
Only available in kernel 2.6.24(?) and newer. Before kernel provided | |
a similar, yet incompatible interface in /proc | |
""" | |
def _parse_battery_info(sys_path): | |
""" | |
Extract battery information from uevent file, already convert to | |
int if necessary | |
""" | |
raw_values = {} | |
with open(os.path.join(sys_path, u"uevent")) as f: | |
for var in f.read().splitlines(): | |
k, v = var.split("=") | |
try: | |
raw_values[k] = int(v) | |
except ValueError: | |
raw_values[k] = v | |
return raw_values | |
battery_list = [] | |
for path in iglob(os.path.join(self.sys_battery_path, "BAT*")): | |
r = _parse_battery_info(path) | |
capacity = r.get("POWER_SUPPLY_ENERGY_FULL", r.get("POWER_SUPPLY_CHARGE_FULL")) | |
present_rate = r.get("POWER_SUPPLY_POWER_NOW", r.get("POWER_SUPPLY_CURRENT_NOW")) | |
remaining_energy = r.get("POWER_SUPPLY_ENERGY_NOW", r.get("POWER_SUPPLY_CHARGE_NOW")) | |
battery = {} | |
battery["capacity"] = capacity | |
battery["charging"] = "Charging" in r["POWER_SUPPLY_STATUS"] | |
battery["percent_charged"] = int(math.floor( | |
remaining_energy / capacity * 100)) | |
try: | |
if battery["charging"]: | |
time_in_secs = ((capacity - remaining_energy) / | |
present_rate * 3600) | |
else: | |
time_in_secs = (remaining_energy / present_rate * 3600) | |
battery["time_remaining"] = self._seconds_to_hms(time_in_secs) | |
except ZeroDivisionError: | |
# Battery is either full charged or is not discharging | |
battery["time_remaining"] = FULLY_CHARGED | |
battery_list.append(battery) | |
return battery_list | |
def _hms_to_seconds(self, t): | |
h, m, s = [int(i) for i in t.split(':')] | |
return 3600 * h + 60 * m + s | |
def _seconds_to_hms(self, secs): | |
m, s = divmod(secs, 60) | |
h, m = divmod(m, 60) | |
return "%d:%02d:%02d" % (h, m, s) | |
def _refresh_battery_info(self): | |
if self.measurement_mode == "acpi": | |
battery_list = self._extract_battery_information_from_acpi() | |
else: | |
battery_list = self._extract_battery_information_from_sys() | |
if type(self.battery_id) == int: | |
battery = battery_list[self.battery_id] | |
self.percent_charged = battery['percent_charged'] | |
self.charging = battery['charging'] | |
self.time_remaining = battery['time_remaining'] | |
elif self.battery_id == "all": | |
total_capacity = sum([battery['capacity'] for battery in | |
battery_list]) | |
# Average and weigh % charged by the capacities of the batteries so | |
# that self.percent_charged properly represents batteries that have | |
# different capacities. | |
self.percent_charged = int(sum([battery[ | |
"capacity"] / total_capacity * battery["percent_charged"] | |
for battery in battery_list])) | |
self.charging = any([battery["charging"] for battery in | |
battery_list]) | |
# Assumes a system has at max two batteries | |
active_battery = None | |
inactive_battery = battery_list[:] | |
for battery_id in range(0, len(battery_list)): | |
if (battery_list[battery_id]["time_remaining"] and | |
battery_list[battery_id]["time_remaining"] != | |
FULLY_CHARGED): | |
active_battery = battery_list[battery_id] | |
del inactive_battery[battery_id] | |
# Only one battery will be discharging or charging at a time. | |
# Therefore, ACPI does not provide a time remaining value for the | |
# other battery. So the time remaining for the other battery is | |
# calculated using the time remaining of the first battery and the | |
# capacity values for both batteries. | |
if active_battery and inactive_battery: | |
inactive_battery = inactive_battery[0] | |
time_remaining_seconds = self._hms_to_seconds(active_battery[ | |
"time_remaining"]) | |
try: | |
rate_second_per_mah = time_remaining_seconds / ( | |
active_battery["capacity"] * | |
(active_battery["percent_charged"] / 100)) | |
time_remaining_seconds += inactive_battery["capacity"] * \ | |
inactive_battery["percent_charged"] / 100 * \ | |
rate_second_per_mah | |
except ZeroDivisionError: | |
# Either active or inactive battery has 0% charge | |
time_remaining_seconds = 0 | |
rate_second_per_mah = 0 | |
self.time_remaining = self._seconds_to_hms( | |
time_remaining_seconds) | |
elif active_battery: | |
self.time_remaining = active_battery["time_remaining"] | |
else: | |
self.time_remaining = None | |
if self.time_remaining and self.hide_seconds: | |
self.time_remaining = self.time_remaining[:-3] | |
def _update_ascii_bar(self): | |
self.ascii_bar = FULL_BLOCK * int(self.percent_charged / 10) | |
if self.charging: | |
self.ascii_bar += EMPTY_BLOCK_CHARGING * ( | |
10 - int(self.percent_charged / 10)) | |
else: | |
self.ascii_bar += EMPTY_BLOCK_DISCHARGING * ( | |
10 - int(self.percent_charged / 10)) | |
def _update_icon(self): | |
if self.charging: | |
self.icon = self.charging_character | |
else: | |
self.icon = self.blocks[min(len(self.blocks) - 1, | |
int(math.ceil(self.percent_charged / 100 * | |
(len(self.blocks) - 1))))] | |
def _update_full_text(self): | |
self.full_text = self.py3.safe_format( | |
self.format, | |
dict(ascii_bar=self.ascii_bar, | |
is_toggled = self.toggle, | |
icon=self.icon, | |
percent=self.percent_charged, | |
time_remaining=self.time_remaining) | |
) | |
def _build_response(self): | |
self.response = {} | |
self._set_bar_text() | |
self._set_bar_color() | |
self._set_cache_timeout() | |
return self.response | |
def _set_bar_text(self): | |
self.response['full_text'] = '' if self.hide_when_full and \ | |
self.percent_charged >= self.threshold_full else self.full_text | |
def _set_bar_color(self): | |
notify_msg = None | |
if self.charging: | |
self.response['color'] = self.py3.COLOR_CHARGING or "#FCE94F" | |
battery_status = 'charging' | |
elif self.percent_charged < self.threshold_bad: | |
self.response['color'] = self.py3.COLOR_BAD | |
battery_status = 'bad' | |
notify_msg = {'msg': 'Battery level is critically low ({}%)', | |
'level': 'error'} | |
elif self.percent_charged < self.threshold_degraded: | |
self.response['color'] = self.py3.COLOR_DEGRADED | |
battery_status = 'degraded' | |
notify_msg = {'msg': 'Battery level is running low ({}%)', | |
'level': 'warning'} | |
elif self.percent_charged >= self.threshold_full: | |
self.response['color'] = self.py3.COLOR_GOOD | |
battery_status = 'full' | |
else: | |
battery_status = 'good' | |
if (notify_msg and self.notify_low_level and | |
self.last_known_status != battery_status): | |
self.py3.notify_user(notify_msg['msg'].format(self.percent_charged), | |
notify_msg['level']) | |
self.last_known_status = battery_status | |
def _set_cache_timeout(self): | |
self.response['cached_until'] = self.py3.time_in(self.cache_timeout) | |
if __name__ == "__main__": | |
""" | |
Run module in test mode. | |
""" | |
from py3status.module_test import module_test | |
module_test(Py3status) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
""" | |
Display song currently playing in Spotify. | |
Configuration parameters: | |
cache_timeout: how often to update the bar (default 5) | |
format: see placeholders below (default '{artist} : {title}') | |
format_down: define output if spotify is not running | |
(default 'Spotify not running') | |
format_stopped: define output if spotify is not playing | |
(default 'Spotify stopped') | |
sanitize_titles: whether to remove meta data from album/track title | |
(default True) | |
sanitize_words: which meta data to remove | |
(default ['bonus', 'demo', 'edit', 'explicit', | |
'extended', 'feat', 'mono', 'remaster', | |
'stereo', 'version']) | |
Format placeholders: | |
{album} album name | |
{artist} artiste name (first one) | |
{time} time duration of the song | |
{title} name of the song | |
Color options: | |
color_offline: Spotify is not running, defaults to color_bad | |
color_paused: Song is stopped or paused, defaults to color_degraded | |
color_playing: Song is playing, defaults to color_good | |
i3status.conf example: | |
``` | |
spotify { | |
format = "{title} by {artist} -> {time}" | |
format_down = "no Spotify" | |
} | |
``` | |
Requires: | |
spotify (>=1.0.27.71.g0a26e3b2) | |
@author Pierre Guilbert, Jimmy Garpehäll, sondrele, Andrwe, Agyar | |
SAMPLE OUTPUT | |
{'color': '#00FF00', 'full_text': 'Rick Astley : Never Gonna Give You Up'} | |
paused | |
{'color': '#FFFF00', 'full_text': 'Rick Astley : Never Gonna Give You Up'} | |
stopped | |
{'color': '#FF0000', 'full_text': 'Spotify stopped'} | |
""" | |
from datetime import timedelta | |
import dbus | |
import re | |
class Py3status: | |
""" | |
""" | |
# available configuration parameters | |
cache_timeout = 5 | |
format = '{artist} : {title}' | |
format_down = 'Spotify not running' | |
format_stopped = 'Spotify stopped' | |
sanitize_titles = True | |
sanitize_words = [ | |
'bonus', | |
'demo', | |
'edit', | |
'explicit', | |
'extended', | |
'feat', | |
'mono', | |
'remaster', | |
'stereo', | |
'version' | |
] | |
toggle = False | |
button_toggle = 1 | |
def post_config_hook(self): | |
""" | |
""" | |
# Match string after hyphen, comma, semicolon or slash containing any metadata word | |
# examples: | |
# - Remastered 2012 | |
# / Radio Edit | |
# ; Remastered | |
self.after_delimiter = self._compile_re(r"([\-,;/])([^\-,;/])*(META_WORDS_HERE).*") | |
# Match brackets with their content containing any metadata word | |
# examples: | |
# (Remastered 2017) | |
# [Single] | |
# (Bonus Track) | |
self.inside_brackets = self._compile_re(r"([\(\[][^)\]]*?(META_WORDS_HERE)[^)\]]*?[\)\]])") | |
def _compile_re(self, expression): | |
""" | |
Compile given regular expression for current sanitize words | |
""" | |
meta_words = '|'.join(self.sanitize_words) | |
expression = expression.replace('META_WORDS_HERE', meta_words) | |
return re.compile(expression, re.IGNORECASE) | |
def _get_text(self): | |
""" | |
Get the current song metadatas (artist - title) | |
""" | |
bus = dbus.SessionBus() | |
try: | |
self.__bus = bus.get_object('org.mpris.MediaPlayer2.spotify', | |
'/org/mpris/MediaPlayer2') | |
self.player = dbus.Interface( | |
self.__bus, 'org.freedesktop.DBus.Properties') | |
try: | |
metadata = self.player.Get('org.mpris.MediaPlayer2.Player', | |
'Metadata') | |
album = metadata.get('xesam:album') | |
artist = metadata.get('xesam:artist')[0] | |
microtime = metadata.get('mpris:length') | |
rtime = str(timedelta(microseconds=microtime))[:-7] | |
title = metadata.get('xesam:title') | |
if self.sanitize_titles: | |
album = self._sanitize_title(album) | |
title = self._sanitize_title(title) | |
playback_status = self.player.Get( | |
'org.mpris.MediaPlayer2.Player', 'PlaybackStatus' | |
) | |
if playback_status.strip() == 'Playing': | |
color = self.py3.COLOR_PLAYING or self.py3.COLOR_GOOD | |
else: | |
color = self.py3.COLOR_PAUSED or self.py3.COLOR_DEGRADED | |
except Exception: | |
return ( | |
self.format_stopped, | |
self.py3.COLOR_PAUSED or self.py3.COLOR_DEGRADED) | |
return ( | |
self.py3.safe_format( | |
self.format, | |
dict(title=title, | |
is_toggled=self.toggle, | |
artist=artist, | |
album=album, | |
time=rtime) | |
), color) | |
except Exception: | |
return ( | |
self.format_down, | |
self.py3.COLOR_OFFLINE or self.py3.COLOR_BAD) | |
def _sanitize_title(self, title): | |
""" | |
Remove redunant meta data from title and return it | |
""" | |
title = re.sub(self.inside_brackets, "", title) | |
title = re.sub(self.after_delimiter, "", title) | |
return title.strip() | |
def spotify(self): | |
""" | |
Get the current "artist - title" and return it. | |
""" | |
(text, color) = self._get_text() | |
response = { | |
'cached_until': self.py3.time_in(self.cache_timeout), | |
'color': color, | |
'full_text': text | |
} | |
return response | |
def on_click(self, event): | |
button = event['button'] | |
if button == self.button_toggle: | |
self.toggle = not self.toggle | |
if __name__ == "__main__": | |
""" | |
Run module in test mode. | |
""" | |
from py3status.module_test import module_test | |
module_test(Py3status) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
#!/usr/bin/python3 | |
""" | |
A small requests getter to check given stream status | |
Inputs: | |
<stream> channel name such as twitch.tv/<stream> | |
<client> client-id app key you have to create from twitch | |
@author Agyar | |
@licence Beerware | |
""" | |
import requests | |
DEFAULT_FORMAT = '[\?if=live ][\?if=rediff ]' | |
class Py3status: | |
format = DEFAULT_FORMAT | |
live = False | |
rediff = False | |
stream = '' | |
client = '' | |
cache_timeout = 600 | |
color_live='' | |
color_rediff='' | |
button_open = 1 | |
watcher = 'xdg-open' | |
def twitch(self): | |
r = requests.get('https://api.twitch.tv/helix/streams?user_login={0}'.format(self.stream), | |
headers={'Client-ID':self.client}).json() | |
if r['data']: | |
if "Rediffusion" in r['data'][0]['title']: | |
self.rediff = True | |
else: | |
self.live = True | |
full_text = self.py3.safe_format(self.format, dict( live=self.live, rediff=self.rediff)) | |
if self.live: | |
color = self.color_live | |
elif self.rediff: | |
color = self.color_rediff | |
return { 'cache_until': self.py3.time_in(self.cache_timeout), | |
'full_text': full_text, | |
'color': color} | |
def on_click(self, event): | |
button = event['button'] | |
if button == self.button_open: | |
self.py3.command_run(self.watcher +' ' + 'https://twitch.tv/{0}'.format(self.stream)) | |
if __name__ == "__main__": | |
""" | |
Run module in test mode. | |
""" | |
from py3status.module_test import module_test | |
module_test(Py3status) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
""" | |
Display WiFi bit rate, quality, signal and SSID using iw. | |
Configuration parameters: | |
bitrate_bad: Bad bit rate in Mbit/s (default 26) | |
bitrate_degraded: Degraded bit rate in Mbit/s (default 53) | |
blocks: a string, where each character represents quality level | |
(default "_▁▂▃▄▅▆▇█") | |
cache_timeout: Update interval in seconds (default 10) | |
device: Wireless device name (default "wlan0") | |
down_color: Output color when disconnected, possible values: | |
"good", "degraded", "bad" (default "bad") | |
format: Display format for this module | |
(default 'W: {bitrate} {signal_percent} {ssid}|W: down') | |
round_bitrate: If true, bit rate is rounded to the nearest whole number | |
(default True) | |
signal_bad: Bad signal strength in percent (default 29) | |
signal_degraded: Degraded signal strength in percent (default 49) | |
use_sudo: Use sudo to run iw, make sure iw requires some root rights | |
without a password by adding a sudoers entry, eg... | |
'<user> ALL=(ALL) NOPASSWD:/usr/bin/iw dev,/usr/bin/iw dev [a-z]* link' | |
(default False) | |
Format placeholders: | |
{bitrate} Display bit rate | |
{device} Display device name | |
{icon} Character representing the quality based on bitrate, | |
as defined by the 'blocks' | |
{ip} Display IP address | |
{signal_dbm} Display signal in dBm | |
{signal_percent} Display signal in percent | |
{ssid} Display SSID | |
Color options: | |
color_bad: Signal strength signal_bad or lower | |
color_degraded: Signal strength signal_degraded or lower | |
color_good: Signal strength above signal_degraded | |
Requires: | |
iw: cli configuration utility for wireless devices | |
ip: only for {ip}. may be part of iproute2: ip routing utilities | |
__Note: Some distributions eg Debian require `iw` to be run with privileges. | |
In this case you will need to use the `use_sudo` configuration parameter.__ | |
@author Markus Weimar, Agyar | |
@license BSD | |
SAMPLE OUTPUT | |
{'color': '#00FF00', 'full_text': u'W: 54.0 MBit/s 100% Chicken Remixed'} | |
""" | |
import re | |
import math | |
STRING_ERROR = "iw: command failed" | |
DEFAULT_FORMAT = 'W: up [\?if=is_toggled {signal_percent} {ssid}]|W: down' | |
class Py3status: | |
""" | |
""" | |
# available configuration parameters | |
bitrate_bad = 26 | |
bitrate_degraded = 53 | |
blocks = u"_▁▂▃▄▅▆▇█" | |
cache_timeout = 10 | |
device = 'wlan0' | |
down_color = 'bad' | |
format = DEFAULT_FORMAT | |
round_bitrate = True | |
signal_bad = 29 | |
signal_degraded = 49 | |
use_sudo = False | |
toggle = False | |
button_toggle = 1 | |
def post_config_hook(self): | |
self._max_bitrate = 0 | |
self._ssid = '' | |
self.iw_cmd = self.py3.check_commands(['iw', '/sbin/iw']) | |
# Try and guess the wifi interface | |
cmd = [self.iw_cmd, 'dev'] | |
if self.use_sudo: | |
cmd.insert(0, 'sudo') | |
try: | |
iw = self.py3.command_output(cmd) | |
devices = re.findall('Interface\s*([^\s]+)', iw) | |
if not devices or 'wlan0' in devices: | |
self.device = 'wlan0' | |
else: | |
self.device = devices[0] | |
except: | |
pass | |
# DEPRECATION WARNING | |
format_down = getattr(self, 'format_down', None) | |
format_up = getattr(self, 'format_up', None) | |
if self.format != DEFAULT_FORMAT: | |
return | |
if format_up or format_down: | |
self.format = u'{}|{}'.format( | |
format_up or 'W: {bitrate} {signal_percent} {ssid}', | |
format_down or 'W: down', | |
) | |
msg = 'DEPRECATION WARNING: you are using old style configuration ' | |
msg += 'parameters you should update to use the new format.' | |
self.py3.log(msg) | |
def wifi(self): | |
""" | |
Get WiFi status using iw. | |
""" | |
self.signal_dbm_bad = self._percent_to_dbm(self.signal_bad) | |
self.signal_dbm_degraded = self._percent_to_dbm(self.signal_degraded) | |
cmd = [self.iw_cmd, 'dev', self.device, 'link'] | |
if self.use_sudo: | |
cmd.insert(0, 'sudo') | |
try: | |
iw = self.py3.command_output(cmd) | |
except: | |
return {'cache_until': self.py3.CACHE_FOREVER, | |
'color': self.py3.COLOR_ERROR or self.py3.COLOR_BAD, | |
'full_text': STRING_ERROR} | |
# bitrate | |
bitrate_out = re.search('tx bitrate: ([^\s]+) ([^\s]+)', iw) | |
if bitrate_out: | |
bitrate = float(bitrate_out.group(1)) | |
if self.round_bitrate: | |
bitrate = round(bitrate) | |
bitrate_unit = bitrate_out.group(2) | |
if bitrate_unit == 'Gbit/s': | |
bitrate *= 1000 | |
else: | |
bitrate = None | |
bitrate_unit = None | |
# signal | |
signal_out = re.search('signal: ([\-0-9]+)', iw) | |
if signal_out: | |
signal_dbm = int(signal_out.group(1)) | |
signal_percent = min(self._dbm_to_percent(signal_dbm), 100) | |
else: | |
signal_dbm = None | |
signal_percent = None | |
ssid_out = re.search('SSID: (.+)', iw) | |
if ssid_out: | |
ssid = ssid_out.group(1) | |
# `iw` command would prints unicode SSID like `\xe8\x8b\x9f` | |
# the the `ssid` here would be '\\xe8\\x8b\\x9f' (note the escape) | |
# it needs to be decoded using 'unicode_escape', to '苟' | |
ssid = ssid.encode('latin-1').decode('unicode_escape') | |
ssid = ssid.encode('latin-1').decode('utf-8') | |
else: | |
ssid = None | |
# check command | |
if self.py3.format_contains(self.format, 'ip'): | |
cmd = ['ip', 'addr', 'list', self.device] | |
if self.use_sudo: | |
cmd.insert(0, 'sudo') | |
ip_info = self.py3.command_output(cmd) | |
ip_match = re.search('inet\s+([0-9.]+)', ip_info) | |
if ip_match: | |
ip = ip_match.group(1) | |
else: | |
ip = None | |
else: | |
ip = '' | |
# reset _max_bitrate if we have changed network | |
if self._ssid != ssid: | |
self._ssid = ssid | |
self._max_bitrate = self.bitrate_degraded | |
if bitrate: | |
if bitrate > self._max_bitrate: | |
self._max_bitrate = bitrate | |
quality = int((bitrate / self._max_bitrate) * 100) | |
else: | |
quality = 0 | |
icon = self.blocks[int(math.ceil(quality / 100.0 * (len(self.blocks) - 1)))] | |
# wifi down | |
if ssid is None: | |
color = getattr(self.py3, 'COLOR_{}'.format(self.down_color.upper())) | |
full_text = self.py3.safe_format(self.format) | |
# wifi up | |
else: | |
color = self.py3.COLOR_GOOD | |
if bitrate: | |
if bitrate <= self.bitrate_bad: | |
color = self.py3.COLOR_BAD | |
elif bitrate <= self.bitrate_degraded: | |
color = self.py3.COLOR_DEGRADED | |
bitrate = '{} {}'.format(bitrate, bitrate_unit) | |
else: | |
bitrate = '? MBit/s' | |
if signal_dbm: | |
if signal_dbm <= self.signal_dbm_bad: | |
color = self.py3.COLOR_BAD | |
elif signal_dbm <= self.signal_dbm_degraded: | |
color = self.py3.COLOR_DEGRADED | |
signal_dbm = '{} dBm'.format(signal_dbm) | |
signal_percent = '{}%'.format(signal_percent) | |
else: | |
signal_dbm = '? dBm' | |
signal_percent = '?%' | |
full_text = self.py3.safe_format( | |
self.format, | |
dict( | |
is_toggled=self.toggle, | |
bitrate=bitrate, | |
device=self.device, | |
icon=icon, | |
ip=ip, | |
signal_dbm=signal_dbm, | |
signal_percent=signal_percent, | |
ssid=ssid, | |
)) | |
return { | |
'cache_until': self.py3.time_in(self.cache_timeout), | |
'full_text': full_text, | |
'color': color, | |
} | |
def _dbm_to_percent(self, dbm): | |
return 2 * (dbm + 100) | |
def _percent_to_dbm(self, percent): | |
return (percent / 2) - 100 | |
def on_click(self, event): | |
button = event['button'] | |
if button == self.button_toggle: | |
self.toggle = not self.toggle | |
if __name__ == "__main__": | |
""" | |
Run module in test mode. | |
""" | |
from py3status.module_test import module_test | |
module_test(Py3status) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment