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 =
See: http://developer.nrel.gov/docs/electricity/utility-rates-v3/ for API details
'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)
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 =
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) ⇒ 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
204 205 206 207 |
# File 'app/models/electricity_rate.rb', line 204 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
121 122 123 124 125 126 |
# File 'app/models/electricity_rate.rb', line 121 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
128 129 130 131 132 133 134 135 |
# File 'app/models/electricity_rate.rb', line 128 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
137 138 139 140 141 142 143 144 |
# File 'app/models/electricity_rate.rb', line 137 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
209 210 211 |
# File 'app/models/electricity_rate.rb', line 209 def self.get_by_lat_lng(lat, lng) get_from_location([lat, lng], :coordinates) end |
.get_from_location(location, search_type) ⇒ Object
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 192 193 194 195 196 197 198 199 200 201 202 |
# File 'app/models/electricity_rate.rb', line 162 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) ⇒ Object
Some test code
J9X4M2 - Quebec
60047 - Lake Zurich
J9X%204M2 - Poorly formatted postal code
J9X region code
151 152 153 154 155 156 157 158 159 160 |
# File 'app/models/electricity_rate.rb', line 151 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 |