Class: ElectricityRate
- Inherits:
-
Object
- Object
- ElectricityRate
- Defined in:
- app/models/electricity_rate.rb
Constant Summary collapse
- US_API_URL =
See: http://developer.nrel.gov/docs/electricity/utility-rates-v3/ for API details
'https://developer.nrel.gov/api/utility_rates/v3.json'- US_API_KEY =
'pNYthBjsnd8WKS85K9qkg6mkZ2gISU6iPchwqdKi'- CAN_RATE_BY_PROVINCE =
Updated December 2025
Source: https://offgridsolarsystem.ca/blog/Canada-electricity-rates.html (July 2025)
IMPORTANT: These are MARGINAL rates (Tier 2 / Step 2) for provinces with tiered pricing.
Floor heating is additive consumption, so customers pay the higher tier rate, not the blended average.- BC: Step 2 rate (14.08¢) used instead of Step 1 (11.72¢)
- QC: Tier 2 rate (10.7¢) used instead of blended average (7.8¢) per Hydro-Québec Rate D
- ON: Mid-peak TOU rate (15.7¢) used as representative rate for additional heating load
{ 'AB' => 0.258, # Alberta - includes delivery & admin charges 'BC' => 0.141, # BC Hydro Step 2 rate (14.08¢ rounded) 'MB' => 0.102, # Manitoba Hydro 'NB' => 0.139, # NB Power 'NL' => 0.148, # NL Hydro / Newfoundland Power 'NS' => 0.183, # Nova Scotia Power 'NT' => 0.410, # NTPC (high due to diesel generation) 'NU' => 0.354, # Qulliq Energy Corp 'ON' => 0.157, # Ontario - mid-peak TOU rate (Nov 2025) 'PE' => 0.184, # Maritime Electric 'QC' => 0.110, # Hydro-Québec Tier 2 rate (~10.7¢) 'SK' => 0.199, # SaskPower 'YT' => 0.187 # Yukon Energy / ATCO Electric }.freeze
- US_RATE_BY_STATE =
https://www.saveonenergy.com/electricity-rates/electricity-rates-by-state/ as of 12/2024, updated 3/4/2025
{ 'AL' => 0.1491, 'AK' => 0.2238, 'AZ' => 0.1520, 'AR' => 0.1174, 'CA' => 0.3055, 'CO' => 0.1516, 'CT' => 0.2816, 'DE' => 0.1668, 'DC' => 0.1626, # This one i don't know it's not from that table, i used the average 'FL' => 0.1420, 'GA' => 0.1349, 'HI' => 0.4234, 'ID' => 0.1097, 'IL' => 0.1599, 'IN' => 0.1442, 'IA' => 0.1243, 'KS' => 0.1385, 'KY' => 0.1328, 'LA' => 0.1170, 'ME' => 0.2629, 'MD' => 0.1815, 'MA' => 0.3122, 'MI' => 0.1841, 'MN' => 0.1405, 'MS' => 0.1344, 'MO' => 0.1157, 'MT' => 0.1187, 'NE' => 0.1078, 'NV' => 0.1488, 'NH' => 0.2362, 'NJ' => 0.1949, 'NM' => 0.1426, 'NY' => 0.2437, 'NC' => 0.1349, 'ND' => 0.1021, 'OH' => 0.1598, 'OK' => 0.1152, 'OR' => 0.1412, 'PA' => 0.1760, 'RI' => 0.2531, 'SC' => 0.1387, 'SD' => 0.1241, 'TN' => 0.1304, 'TX' => 0.1542, 'UT' => 0.1102, 'VT' => 0.2229, 'VA' => 0.1446, 'WA' => 0.1183, 'WV' => 0.1451, 'WI' => 0.1631, 'WY' => 0.1178 }.freeze
- ALL_STATES_AND_PROVINCES =
CAN_RATE_BY_PROVINCE.merge(US_RATE_BY_STATE).freeze
- CAN_AVERAGE_RATE =
Canadian average rate - used as fallback when province cannot be determined.
This is a weighted average biased toward Tier 2/marginal rates to better reflect
the cost of additional consumption like floor heating.
Updated December 2025 based on provincial rates above. 0.175- US_AVERAGE_RATE =
US average rate - updated March 2025
Source: https://www.saveonenergy.com/electricity-rates/electricity-rates-by-state/ 0.1626- RATES_BY_COUNTRY =
{ 'USA' => US_AVERAGE_RATE, 'CAN' => CAN_AVERAGE_RATE }.freeze
- RATES_BY_COUNTRY_ISO =
{ 'US' => US_AVERAGE_RATE, 'CA' => CAN_AVERAGE_RATE }.freeze
- POSTAL_CA_REGEXP =
This regexp can handle regional postal code as well as full canadian postal codes
/^([ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z])([ -]?\d[ABCEGHJ-NPRSTV-Z]\d)?$/i- POSTAL_US_REGEXP =
This one handles US with +4 optional
/^(\d{5})(-{0,1}\d{4})?$/
Class Method Summary collapse
- .get_average_for_locale(locale = I18n.locale) ⇒ Object
- .get_average_rate_for_country_iso_state_code(country_iso, state_code) ⇒ Object
- .get_average_rate_for_locale(locale = I18n.locale) ⇒ Object
- .get_average_rate_for_locale_as_money(locale = I18n.locale) ⇒ Object
- .get_by_lat_lng(lat, lng) ⇒ Object
- .get_from_location(location, search_type) ⇒ Object
-
.get_from_postal_code(code) ⇒ Object
Some test code J9X4M2 - Quebec 60047 - Lake Zurich J9X%204M2 - Poorly formatted postal code J9X region code.
Class Method Details
.get_average_for_locale(locale = I18n.locale) ⇒ Object
193 194 195 196 |
# File 'app/models/electricity_rate.rb', line 193 def self.get_average_for_locale(locale = I18n.locale) country_iso = locale.to_s.last(2) RATES_BY_COUNTRY_ISO[country_iso] end |
.get_average_rate_for_country_iso_state_code(country_iso, state_code) ⇒ Object
110 111 112 113 114 115 |
# File 'app/models/electricity_rate.rb', line 110 def self.get_average_rate_for_country_iso_state_code(country_iso, state_code) { 'US' => US_RATE_BY_STATE, 'CA' => CAN_RATE_BY_PROVINCE }[country_iso]&.dig(state_code) end |
.get_average_rate_for_locale(locale = I18n.locale) ⇒ Object
117 118 119 120 121 122 123 124 |
# File 'app/models/electricity_rate.rb', line 117 def self.get_average_rate_for_locale(locale = I18n.locale) case LocaleUtility.locale_to_country_iso(locale) when 'US' US_AVERAGE_RATE when 'CA' CAN_AVERAGE_RATE end end |
.get_average_rate_for_locale_as_money(locale = I18n.locale) ⇒ Object
126 127 128 129 130 131 132 133 |
# File 'app/models/electricity_rate.rb', line 126 def self.get_average_rate_for_locale_as_money(locale = I18n.locale) case LocaleUtility.locale_to_country_iso(locale) when 'US' Money.from_amount(US_AVERAGE_RATE, 'USD') when 'CA' Money.from_amount(CAN_AVERAGE_RATE, 'CAD') end end |
.get_by_lat_lng(lat, lng) ⇒ Object
198 199 200 |
# File 'app/models/electricity_rate.rb', line 198 def self.get_by_lat_lng(lat, lng) get_from_location([lat, lng], :coordinates) end |
.get_from_location(location, search_type) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'app/models/electricity_rate.rb', line 151 def self.get_from_location(location, search_type) res = {} res[:location] = location res[:search_type] = search_type # Try to find location with state code first geo = Geocoder.search(location).select { |g| g.country_code.present? && g.state_code.present? }.first # Fall back to just country if no state code geo ||= Geocoder.search(location).select { |g| g.country_code.present? }.first if geo iso_country = ISO3166::Country[geo.country_code] res[:country_iso3] = iso_country&.alpha3 res[:country_iso] = iso_country&.alpha2 res[:currency_code] = iso_country&.currency_code || 'USD' res[:state_code] = geo.state_code res[:postal_code] = geo.postal_code if geo.respond_to?(:postal_code) # Try state/province rate first, then country rate res[:average_rate] = ALL_STATES_AND_PROVINCES[res[:state_code]] if res[:state_code] if res[:average_rate] res[:precision] = :state_or_province elsif res[:average_rate] = RATES_BY_COUNTRY_ISO[res[:country_iso]] res[:precision] = :country end end if res[:average_rate] # Money is expressed in the destination country value rate_money = Money.from_amount(res[:average_rate], res[:currency_code]) res[:currency] = rate_money.currency.iso_code res[:currency_symbol] = rate_money.currency.symbol res[:status] = :ok else # Return OK with no rate - let the client keep its current value res[:status] = :ok res[:message] = 'No electricity rate data available for this location' res[:average_rate] = nil end res end |
.get_from_postal_code(code) ⇒ Object
Some test code
J9X4M2 - Quebec
60047 - Lake Zurich
J9X%204M2 - Poorly formatted postal code
J9X region code
140 141 142 143 144 145 146 147 148 149 |
# File 'app/models/electricity_rate.rb', line 140 def self.get_from_postal_code(code) require 'cgi' clean_code = CGI.unescape(code) # Scan for canadian first postal_code = clean_code.to_s.scan(POSTAL_CA_REGEXP).join.presence&.upcase # For US zips, only use the 5-digit base (strip +4 suffix) since Geocoder doesn't handle ZIP+4 postal_code ||= clean_code.to_s.match(POSTAL_US_REGEXP)&.captures&.first Rails.logger.debug { "ElectricityRate: Looking up postal code #{postal_code}" } get_from_location("#{postal_code}, North America", :postal_code) # patch pending more thorough international support top avoid, for e.g., an IA zip code being returned as Kuala Lumpur end |