Skip to content

Instantly share code, notes, and snippets.

@fetimo
Created July 9, 2022 21:00
Show Gist options
  • Save fetimo/bfddb1d35524106357e922cd64fcaa2a to your computer and use it in GitHub Desktop.
Save fetimo/bfddb1d35524106357e922cd64fcaa2a to your computer and use it in GitHub Desktop.
Charging energy -> CO2 output
#!/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