Class: ElectricityRate
- Inherits:
-
Object
- Object
- ElectricityRate
- Defined in:
- app/models/electricity_rate.rb
Overview
Per-state / per-utility electricity-rate lookups powered by the NREL
Utility Rates API. Used by the heat-loss and operating-cost calculators
on the public website to estimate "$/year to run this floor heater"
given a customer's location.
Constant Summary collapse
- US_API_URL =
'https://developer.nrel.gov/api/utility_rates/v3.json'.freeze
- US_API_KEY =
Key used for us api.
'pNYthBjsnd8WKS85K9qkg6mkZ2gISU6iPchwqdKi'.freeze
- CAN_RATE_BY_PROVINCE =
Updated December 2025
Source: https://offgridsolarsystem.ca/blog/Canada-electricity-rates.html (July 2025)
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 =
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 =
Rates by country.
{ 'USA' => US_AVERAGE_RATE, 'CAN' => CAN_AVERAGE_RATE }.freeze
- RATES_BY_COUNTRY_ISO =
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) ⇒ void
- .get_average_rate_for_country_iso_state_code(country_iso, state_code) ⇒ void
- .get_average_rate_for_locale(locale = I18n.locale) ⇒ void
- .get_average_rate_for_locale_as_money(locale = I18n.locale) ⇒ void
- .get_by_lat_lng(lat, lng) ⇒ void
- .get_from_location(location, search_type) ⇒ void
-
.get_from_postal_code(code) ⇒ void
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) ⇒ void
This method returns an undefined value.
218 219 220 221 |
# File 'app/models/electricity_rate.rb', line 218 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) ⇒ void
This method returns an undefined value.
124 125 126 127 128 129 |
# File 'app/models/electricity_rate.rb', line 124 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) ⇒ void
This method returns an undefined value.
133 134 135 136 137 138 139 140 |
# File 'app/models/electricity_rate.rb', line 133 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) ⇒ void
This method returns an undefined value.
144 145 146 147 148 149 150 151 |
# File 'app/models/electricity_rate.rb', line 144 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) ⇒ void
This method returns an undefined value.
226 227 228 |
# File 'app/models/electricity_rate.rb', line 226 def self.get_by_lat_lng(lat, lng) get_from_location([lat, lng], :coordinates) end |
.get_from_location(location, search_type) ⇒ void
This method returns an undefined value.
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'app/models/electricity_rate.rb', line 174 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).find { |g| g.country_code.present? && g.state_code.present? } # Fall back to just country if no state code geo ||= Geocoder.search(location).find { |g| g.country_code.present? } 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) ⇒ void
This method returns an undefined value.
Some test code
J9X4M2 - Quebec
60047 - Lake Zurich
J9X%204M2 - Poorly formatted postal code
J9X region code
160 161 162 163 164 165 166 167 168 169 |
# File 'app/models/electricity_rate.rb', line 160 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 |