Created
July 9, 2022 21:00
-
-
Save fetimo/bfddb1d35524106357e922cd64fcaa2a to your computer and use it in GitHub Desktop.
Charging energy -> CO2 output
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
#!/usr/bin/env ruby | |
# TODO: Need to work out how to do impact this charging session and impact overall | |
# <xbar.title>Lush Energy</xbar.title> | |
# <xbar.version>v1.0</xbar.version> | |
# <xbar.author>Tim Stone</xbar.author> | |
# <xbar.author.github>fetimo</xbar.author.github> | |
# <xbar.desc>This plugin displays the current carbon (gC02equivalent) emmissions per kWh of produced electric energy in the requested country/region </xbar.desc> | |
# <xbar.dependencies>ruby, CO2 Signal</xbar.dependencies> | |
# <xbar.abouturl>https://docs.co2signal.com/</xbar.abouturl> | |
# <xbar.image>https://raw.githubusercontent.com/pygoner/Plugin-Bitbar/main/Bitbar%20C02%20Signal%20Plugin%20Image.png</xbar.image> | |
require 'csv' | |
require 'date' | |
require 'json' | |
require 'net/http' | |
require 'uri' | |
require 'fileutils' | |
CACHE_FOLDER_NAME = 'co2_cache'.freeze | |
API_CACHE_NAME = "#{__dir__}/#{CACHE_FOLDER_NAME}/co2.cache.json".freeze | |
WATTS_CACHE_NAME = "#{__dir__}/#{CACHE_FOLDER_NAME}/watts.cache.json".freeze | |
# Show current impact | |
# Reset action to clear caches | |
FileUtils.mkdir(CACHE_FOLDER_NAME) unless File.directory?(CACHE_FOLDER_NAME) | |
def read_from_json_file(filename) | |
return unless File.exist?(filename) | |
file_data = File.read(filename) | |
JSON.parse(file_data) | |
end | |
def write_to_file(filename, data) | |
json = JSON.generate(data) | |
File.write(filename, json, mode: 'w') | |
JSON.parse(json) | |
end | |
def read_co2_cache | |
file_json = read_from_json_file(API_CACHE_NAME) | |
return unless file_json | |
fetched_at = DateTime.parse(file_json['fetched_at']) | |
time_difference_in_sec = (DateTime.now.to_time.to_i - fetched_at.to_time.to_i).abs | |
# 30 minute cache | |
File.delete(API_CACHE_NAME) if time_difference_in_sec > 1800 | |
file_json | |
end | |
def fetch_co2_data | |
uri = URI('https://api.carbonintensity.org.uk/intensity') | |
req = Net::HTTP::Get.new(uri) | |
req['content-type'] = 'application/json' | |
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| | |
http.request(req) | |
end | |
JSON.parse(res.body) | |
end | |
def obtain_co2_data | |
data = read_co2_cache | |
unless data | |
data = fetch_co2_data | |
data['fetched_at'] = DateTime.now.to_time | |
write_to_file(API_CACHE_NAME, data) unless data['error'] | |
end | |
data | |
end | |
def print_exception(exception, explicit) | |
puts "[#{explicit ? 'EXPLICIT' : 'INEXPLICIT'}] #{exception.class}: #{exception.message}" | |
puts exception.backtrace.join("\n") | |
end | |
def calculate_average_watts | |
# Calculate the impact of the last charge. | |
timelapse = read_from_json_file(WATTS_CACHE_NAME) | |
watt_values = timelapse['measurements'].map { |item| item['value'] }.to_a | |
watt_values.sum / watt_values.size | |
end | |
def time_charging_in_hours | |
# Find the duration of the charge | |
data = read_from_json_file(WATTS_CACHE_NAME) | |
timelapse = data['measurements'].to_a | |
start = DateTime.parse(timelapse.first['timestamp']).to_time | |
finish = DateTime.parse(timelapse.last['timestamp']).to_time | |
(finish - start) / 60 / 60 | |
end | |
def calculate_impact | |
average = calculate_average_watts | |
co2_data = obtain_co2_data | |
# Take the carbon intensity for the hour and find what that equates to as a | |
# decimal of the time on charge. Then multiply by the average of watts consumed. | |
(time_charging_in_hours / co2_data['data'][0]['intensity']['actual']) * average | |
rescue StandardError => e | |
print_exception(e, false) | |
0.0 | |
end | |
def calculate_watts | |
watts = `ioreg -rw0 -c AppleSmartBattery | grep BatteryData | grep -o '"AdapterPower"=[0-9]*' | cut -c 16-` | |
# This is arcane AF but the ioreg values are stored as unsigned ints | |
# which we need to convert to single precision floats. | |
[watts.to_i].pack('I').unpack1('f') | |
end | |
battery = `pmset -g batt` | |
is_charging = battery.include? 'AC Power' | |
if is_charging | |
puts '🍂' | |
payload = read_from_json_file(WATTS_CACHE_NAME) | |
payload ||= write_to_file(WATTS_CACHE_NAME, { | |
total_session: 0, | |
total_overall: 0, | |
measurements: [] | |
}) | |
payload['total_session'] = calculate_impact | |
payload['total_overall'] = payload['total_overall'] + payload['total_session'] | |
payload['measurements'].append({ | |
value: calculate_watts, | |
timestamp: DateTime.now.to_time | |
}) | |
write_to_file(WATTS_CACHE_NAME, payload) | |
payload | |
else | |
puts '🍃' | |
end | |
if ARGV.include? 'reset' | |
File.delete(WATTS_CACHE_NAME) | |
File.delete(API_CACHE_NAME) | |
end | |
puts '---' | |
puts "Impact: #{format('%<num>.2f', num: calculate_impact)} gCO2eq" | |
puts "Reset stats | shell='#{__dir__}/#{__FILE__}' param1=reset terminal=false refresh=true" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment