├── .github └── workflows │ ├── hacs.yml │ └── release.yml ├── README.md ├── custom_components └── tarif_edf │ ├── __init__.py │ ├── config_flow.py │ ├── const.py │ ├── coordinator.py │ ├── manifest.json │ ├── sensor.py │ ├── strings.json │ └── translations │ ├── en.json │ └── fr.json └── hacs.json /.github/workflows/hacs.yml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest and HACS 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v3" 14 | - uses: "home-assistant/actions/hassfest@master" 15 | hacs: 16 | name: HACS Action 17 | runs-on: "ubuntu-latest" 18 | steps: 19 | - name: HACS Action 20 | uses: "hacs/action@main" 21 | with: 22 | category: "integration" 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: Tarif EDF release 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: "Set version number" 16 | run: | 17 | yq -i -o json '.version="${{ github.event.release.tag_name }}"' \ 18 | "${{ github.workspace }}/custom_components/tarif_edf/manifest.json" 19 | # Pack the HACS dir as a zip and upload to the release 20 | - name: ZIP Tarif EDF Dir 21 | run: | 22 | cd "${{ github.workspace }}/custom_components/tarif_edf" 23 | zip tarif_edf.zip -r ./ 24 | - name: Upload zip to release 25 | uses: softprops/action-gh-release@v2.2.1 26 | with: 27 | files: ${{ github.workspace }}/custom_components/tarif_edf/tarif_edf.zip 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tarif EDF integration for Home Assistant 2 | 3 | ## Installation 4 | 5 | ### Using HACS 6 | 7 | [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=delphiki&repository=hass-tarif-edf&category=integration) 8 | 9 | ### Manual install 10 | 11 | Copy the `tarif_edf` folder from latest release to the `custom_components` folder in your `config` folder. 12 | 13 | ## Configuration 14 | 15 | [![Open your Home Assistant instance and add the integration via the UI.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=tarif_edf) 16 | 17 | ## Available Sensors 18 | 19 | ### Common Sensors (All Contracts) 20 | | Sensor | Description | Unit | Example | 21 | |--------|-------------|------|---------| 22 | | `sensor.tarif_actuel_[type]_[power]kva_ttc` | Current applicable rate | EUR/kWh | `sensor.tarif_actuel_base_6kva_ttc` | 23 | 24 | ### Base Contract 25 | | Sensor | Description | Unit | 26 | |--------|-------------|------| 27 | | `sensor.tarif_base_ttc` | Base rate | EUR/kWh | 28 | 29 | ### HP/HC Contract (Peak/Off-Peak) 30 | | Sensor | Description | Unit | 31 | |--------|-------------|------| 32 | | `sensor.tarif_heures_creuses_ttc` | Off-peak hours rate | EUR/kWh | 33 | | `sensor.tarif_heures_pleines_ttc` | Peak hours rate | EUR/kWh | 34 | 35 | ### Tempo Contract 36 | | Sensor | Description | Unit | 37 | |--------|-------------|------| 38 | | `sensor.tarif_tempo_couleur` | Current Tempo color | - | 39 | | `sensor.tarif_tempo_couleur_hier` | Yesterday's Tempo color | - | 40 | | `sensor.tarif_tempo_couleur_aujourd_hui` | Today's Tempo color | - | 41 | | `sensor.tarif_tempo_couleur_demain` | Tomorrow's Tempo color | - | 42 | | `sensor.tarif_tempo_heures_creuses_ttc` | Current off-peak hours rate | EUR/kWh | 43 | | `sensor.tarif_tempo_heures_pleines_ttc` | Current peak hours rate | EUR/kWh | 44 | | `sensor.tarif_bleu_tempo_heures_creuses_ttc` | Blue days off-peak rate | EUR/kWh | 45 | | `sensor.tarif_bleu_tempo_heures_pleines_ttc` | Blue days peak rate | EUR/kWh | 46 | | `sensor.tarif_blanc_tempo_heures_creuses_ttc` | White days off-peak rate | EUR/kWh | 47 | | `sensor.tarif_blanc_tempo_heures_pleines_ttc` | White days peak rate | EUR/kWh | 48 | | `sensor.tarif_rouge_tempo_heures_creuses_ttc` | Red days off-peak rate | EUR/kWh | 49 | | `sensor.tarif_rouge_tempo_heures_pleines_ttc` | Red days peak rate | EUR/kWh | 50 | -------------------------------------------------------------------------------- /custom_components/tarif_edf/__init__.py: -------------------------------------------------------------------------------- 1 | """The Tarif EDF integration.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.config_entries import ConfigEntry 5 | from homeassistant.core import HomeAssistant 6 | from homeassistant.exceptions import ConfigEntryNotReady 7 | 8 | from datetime import timedelta 9 | 10 | from .coordinator import TarifEdfDataUpdateCoordinator 11 | 12 | from .const import ( 13 | DOMAIN, 14 | PLATFORMS, 15 | DEFAULT_REFRESH_INTERVAL 16 | ) 17 | 18 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 19 | """Set up Tarif EDF from a config entry.""" 20 | hass.data.setdefault(DOMAIN, {}) 21 | 22 | coordinator = TarifEdfDataUpdateCoordinator(hass, entry) 23 | 24 | await coordinator.async_config_entry_first_refresh() 25 | 26 | if not coordinator.last_update_success: 27 | raise ConfigEntryNotReady 28 | 29 | hass.data[DOMAIN][entry.entry_id] = { 30 | "coordinator": coordinator, 31 | } 32 | 33 | entry.async_on_unload(entry.add_update_listener(update_listener)) 34 | 35 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 36 | 37 | return True 38 | 39 | 40 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 41 | """Unload a config entry.""" 42 | if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): 43 | hass.data[DOMAIN].pop(entry.entry_id) 44 | 45 | return unload_ok 46 | 47 | async def update_listener(hass: HomeAssistant, entry: ConfigEntry): 48 | """Handle options update.""" 49 | hass.data[DOMAIN][entry.entry_id]['coordinator'].update_interval = timedelta(days=entry.options.get("refresh_interval", DEFAULT_REFRESH_INTERVAL)) 50 | 51 | return True 52 | -------------------------------------------------------------------------------- /custom_components/tarif_edf/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Tarif EDF integration.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from typing import Any 6 | 7 | import voluptuous as vol 8 | 9 | from homeassistant import config_entries 10 | from homeassistant.data_entry_flow import FlowResult 11 | from homeassistant.exceptions import HomeAssistantError 12 | from homeassistant.core import callback 13 | from homeassistant.helpers.selector import SelectSelector 14 | 15 | from .const import ( 16 | DOMAIN, 17 | DEFAULT_REFRESH_INTERVAL, 18 | CONTRACT_TYPE_BASE, 19 | CONTRACT_TYPE_HPHC, 20 | CONTRACT_TYPE_TEMPO, 21 | TEMPO_OFFPEAK_HOURS 22 | ) 23 | 24 | _LOGGER = logging.getLogger(__name__) 25 | 26 | STEP_USER = vol.Schema( 27 | { 28 | vol.Required("contract_power", default="6"): SelectSelector({ 29 | "options": ['3', '6', '9', '12', '15', '18', '30', '36'], 30 | "mode": "dropdown" 31 | }), 32 | vol.Required("contract_type"): vol.In({ 33 | CONTRACT_TYPE_BASE: 'Base', 34 | CONTRACT_TYPE_HPHC: 'Heures pleines / Heures creuses', 35 | CONTRACT_TYPE_TEMPO: 'Tempo', 36 | }) 37 | } 38 | ) 39 | 40 | @config_entries.HANDLERS.register(DOMAIN) 41 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 42 | """Handle a config flow for Tarif EDF.""" 43 | 44 | VERSION = 1 45 | 46 | async def async_step_user(self, user_input: dict | None = None) -> FlowResult: 47 | """Handle a flow initialized by the user.""" 48 | _LOGGER.debug("Setup process initiated by user.") 49 | 50 | if user_input is None: 51 | return self.async_show_form( 52 | step_id="user", data_schema=STEP_USER 53 | ) 54 | 55 | return self.async_create_entry(title="Option "+str.upper(user_input['contract_type']) + ", " + user_input['contract_power']+"kVA", data=user_input) 56 | 57 | @staticmethod 58 | @callback 59 | def async_get_options_flow( 60 | config_entry: config_entries.ConfigEntry, 61 | ) -> config_entries.OptionsFlow: 62 | """Create the options flow.""" 63 | return OptionsFlowHandler(config_entry.entry_id) 64 | 65 | class CannotConnect(HomeAssistantError): 66 | """Error to indicate we cannot connect.""" 67 | 68 | 69 | class InvalidAuth(HomeAssistantError): 70 | """Error to indicate there is invalid auth.""" 71 | 72 | class OptionsFlowHandler(config_entries.OptionsFlow): 73 | def __init__(self, config_entry_id: str) -> None: 74 | """Initialize options flow.""" 75 | self.config_entry_id = config_entry_id 76 | 77 | async def async_step_init( 78 | self, user_input: dict[str, Any] | None = None 79 | ) -> FlowResult: 80 | """Manage the options.""" 81 | if user_input is not None: 82 | return self.async_create_entry(title="", data=user_input) 83 | 84 | config_entry = self.hass.config_entries.async_get_entry(self.config_entry_id) 85 | 86 | default_offpeak_hours = None 87 | if config_entry.data['contract_type'] == CONTRACT_TYPE_TEMPO: 88 | default_offpeak_hours = TEMPO_OFFPEAK_HOURS 89 | 90 | return self.async_show_form( 91 | step_id="init", 92 | data_schema=vol.Schema( 93 | { 94 | vol.Optional("refresh_interval", default=config_entry.options.get("refresh_interval", DEFAULT_REFRESH_INTERVAL)): int, 95 | vol.Optional("off_peak_hours_ranges", default=config_entry.options.get("off_peak_hours_ranges", default_offpeak_hours)): str, 96 | } 97 | ), 98 | ) 99 | -------------------------------------------------------------------------------- /custom_components/tarif_edf/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Tarif EDF integration.""" 2 | 3 | from homeassistant.const import Platform 4 | 5 | DOMAIN = "tarif_edf" 6 | 7 | CONTRACT_TYPE_BASE="base" 8 | CONTRACT_TYPE_HPHC="hphc" 9 | CONTRACT_TYPE_TEMPO="tempo" 10 | 11 | TARIF_BASE_URL="https://www.data.gouv.fr/fr/datasets/r/c13d05e5-9e55-4d03-bf7e-042a2ade7e49" 12 | TARIF_HPHC_URL="https://www.data.gouv.fr/fr/datasets/r/f7303b3a-93c7-4242-813d-84919034c416" 13 | TARIF_TEMPO_URL="https://www.data.gouv.fr/fr/datasets/r/0c3d1d36-c412-4620-8566-e5cbb4fa2b5a" 14 | 15 | TEMPO_COLOR_API_URL="https://www.api-couleur-tempo.fr/api/jourTempo" 16 | TEMPO_COLORS_MAPPING={ 17 | 0: "indéterminé", 18 | 1: "bleu", 19 | 2: "blanc", 20 | 3: "rouge" 21 | } 22 | TEMPO_DAY_START_AT="06:00" 23 | TEMPO_TOMRROW_AVAILABLE_AT="11:00" 24 | TEMPO_OFFPEAK_HOURS="22:00-06:00" 25 | 26 | DEFAULT_REFRESH_INTERVAL=1 27 | 28 | PLATFORMS = [Platform.SENSOR] 29 | -------------------------------------------------------------------------------- /custom_components/tarif_edf/coordinator.py: -------------------------------------------------------------------------------- 1 | """Data update coordinator for the Tarif EDF integration.""" 2 | from __future__ import annotations 3 | 4 | from datetime import timedelta, datetime, date 5 | import time 6 | import re 7 | from typing import Any 8 | import json 9 | import logging 10 | import requests 11 | import csv 12 | 13 | from homeassistant.config_entries import ConfigEntry 14 | from homeassistant.const import Platform 15 | from homeassistant.core import HomeAssistant 16 | from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator 17 | 18 | from .const import ( 19 | DEFAULT_REFRESH_INTERVAL, 20 | CONTRACT_TYPE_BASE, 21 | CONTRACT_TYPE_HPHC, 22 | CONTRACT_TYPE_TEMPO, 23 | TARIF_BASE_URL, 24 | TARIF_HPHC_URL, 25 | TARIF_TEMPO_URL, 26 | TEMPO_COLOR_API_URL, 27 | TEMPO_COLORS_MAPPING, 28 | TEMPO_DAY_START_AT, 29 | TEMPO_TOMRROW_AVAILABLE_AT, 30 | TEMPO_OFFPEAK_HOURS 31 | ) 32 | 33 | _LOGGER = logging.getLogger(__name__) 34 | 35 | def get_remote_file(url: str): 36 | return requests.get( 37 | url, 38 | stream=True, 39 | headers={ 40 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" 41 | }, 42 | ) 43 | 44 | def str_to_time(str): 45 | return datetime.strptime(str, '%H:%M').time() 46 | 47 | def time_in_between(now, start, end): 48 | if start <= end: 49 | return start <= now < end 50 | else: 51 | return start <= now or now < end 52 | 53 | def get_tempo_color_from_code(code): 54 | return TEMPO_COLORS_MAPPING[code] 55 | 56 | 57 | class TarifEdfDataUpdateCoordinator(TimestampDataUpdateCoordinator): 58 | """Data update coordinator for the Tarif EDF integration.""" 59 | 60 | config_entry: ConfigEntry 61 | tempo_prices = [] 62 | 63 | def __init__( 64 | self, hass: HomeAssistant, entry: ConfigEntry 65 | ) -> None: 66 | """Initialize the coordinator.""" 67 | super().__init__( 68 | hass=hass, 69 | logger=_LOGGER, 70 | name=entry.title, 71 | update_interval=timedelta(minutes=1), 72 | ) 73 | self.config_entry = entry 74 | 75 | async def get_tempo_day(self, date): 76 | date_str = date.strftime('%Y-%m-%d') 77 | now = datetime.now().time() 78 | check_limit = str_to_time(TEMPO_TOMRROW_AVAILABLE_AT) 79 | 80 | for price in self.tempo_prices: 81 | codeJour = price['codeJour'] 82 | if "dateJour" in price and price['dateJour'] == date_str and \ 83 | (codeJour in [1,2,3] or (codeJour == 0 and now < check_limit)): 84 | return price 85 | 86 | url = f"{TEMPO_COLOR_API_URL}/{date_str}" 87 | response = await self.hass.async_add_executor_job(get_remote_file, url) 88 | response_json = response.json() 89 | 90 | self.tempo_prices.append(response_json) 91 | 92 | return response_json 93 | 94 | async def _async_update_data(self) -> dict[Platform, dict[str, Any]]: 95 | """Get the latest data from Tarif EDF and updates the state.""" 96 | data = self.config_entry.data 97 | previous_data = None if self.data is None else self.data.copy() 98 | 99 | if previous_data is None: 100 | self.data = { 101 | "contract_power": data['contract_power'], 102 | "contract_type": data['contract_type'], 103 | "last_refresh_at": None, 104 | "tarif_actuel_ttc": None 105 | } 106 | 107 | fresh_data_limit = datetime.now() - timedelta(days=self.config_entry.options.get("refresh_interval", DEFAULT_REFRESH_INTERVAL)) 108 | 109 | tarif_needs_update = self.data['last_refresh_at'] is None or self.data['last_refresh_at'] < fresh_data_limit 110 | 111 | self.logger.info('EDF tarif_needs_update '+('yes' if tarif_needs_update else 'no')) 112 | 113 | if tarif_needs_update: 114 | if data['contract_type'] == CONTRACT_TYPE_BASE: 115 | url = TARIF_BASE_URL 116 | elif data['contract_type'] == CONTRACT_TYPE_HPHC: 117 | url = TARIF_HPHC_URL 118 | elif data['contract_type'] == CONTRACT_TYPE_TEMPO: 119 | url = TARIF_TEMPO_URL 120 | 121 | response = await self.hass.async_add_executor_job(get_remote_file, url) 122 | parsed_content = csv.reader(response.content.decode('utf-8').splitlines(), delimiter=';') 123 | rows = list(parsed_content) 124 | 125 | for row in rows: 126 | if row[1] == '' and row[2] == data['contract_power']: 127 | if data['contract_type'] == CONTRACT_TYPE_BASE: 128 | self.data['base_fixe_ttc'] = float(row[4].replace(",", "." )) 129 | self.data['base_variable_ttc'] = float(row[6].replace(",", "." )) 130 | elif data['contract_type'] == CONTRACT_TYPE_HPHC: 131 | self.data['hphc_fixe_ttc'] = float(row[4].replace(",", "." )) 132 | self.data['hphc_variable_hc_ttc'] = float(row[6].replace(",", "." )) 133 | self.data['hphc_variable_hp_ttc'] = float(row[8].replace(",", "." )) 134 | elif data['contract_type'] == CONTRACT_TYPE_TEMPO: 135 | self.data['tempo_fixe_ttc'] = float(row[4].replace(",", "." )) 136 | self.data['tempo_variable_hc_bleu_ttc'] = float(row[6].replace(",", "." )) 137 | self.data['tempo_variable_hp_bleu_ttc'] = float(row[8].replace(",", "." )) 138 | self.data['tempo_variable_hc_blanc_ttc'] = float(row[10].replace(",", "." )) 139 | self.data['tempo_variable_hp_blanc_ttc'] = float(row[12].replace(",", "." )) 140 | self.data['tempo_variable_hc_rouge_ttc'] = float(row[14].replace(",", "." )) 141 | self.data['tempo_variable_hp_rouge_ttc'] = float(row[16].replace(",", "." )) 142 | 143 | self.data['last_refresh_at'] = datetime.now() 144 | 145 | break 146 | response.close 147 | 148 | if data['contract_type'] == CONTRACT_TYPE_TEMPO: 149 | today = date.today() 150 | yesterday = today - timedelta(days=1) 151 | tomorrow = today + timedelta(days=1) 152 | 153 | tempo_yesterday = await self.get_tempo_day(yesterday) 154 | tempo_today = await self.get_tempo_day(today) 155 | tempo_tomorrow = await self.get_tempo_day(tomorrow) 156 | 157 | yesterday_color = get_tempo_color_from_code(tempo_yesterday['codeJour']) 158 | today_color = get_tempo_color_from_code(tempo_today['codeJour']) 159 | tomorrow_color = get_tempo_color_from_code(tempo_tomorrow['codeJour']) 160 | 161 | self.data['tempo_couleur_hier'] = yesterday_color 162 | self.data['tempo_couleur_aujourdhui'] = today_color 163 | self.data['tempo_couleur_demain'] = tomorrow_color 164 | 165 | currentColorCode = tempo_yesterday['codeJour'] 166 | 167 | if datetime.now().time() >= str_to_time(TEMPO_DAY_START_AT): 168 | self.logger.info("Using today's tempo prices") 169 | currentColorCode = tempo_today['codeJour'] 170 | else: 171 | self.logger.info("Using yesterday's tempo prices") 172 | 173 | if currentColorCode in [1, 2, 3]: 174 | color = get_tempo_color_from_code(currentColorCode) 175 | self.data['tempo_couleur'] = color 176 | self.data['tempo_variable_hp_ttc'] = self.data[f"tempo_variable_hp_{color}_ttc"] 177 | self.data['tempo_variable_hc_ttc'] = self.data[f"tempo_variable_hc_{color}_ttc"] 178 | self.data['last_refresh_at'] = datetime.now() 179 | 180 | default_offpeak_hours = None 181 | if data['contract_type'] == CONTRACT_TYPE_TEMPO: 182 | default_offpeak_hours = TEMPO_OFFPEAK_HOURS 183 | off_peak_hours_ranges = self.config_entry.options.get("off_peak_hours_ranges", default_offpeak_hours) 184 | 185 | if data['contract_type'] == CONTRACT_TYPE_BASE: 186 | self.data['tarif_actuel_ttc'] = self.data['base_variable_ttc'] 187 | elif data['contract_type'] in [CONTRACT_TYPE_HPHC, CONTRACT_TYPE_TEMPO] and off_peak_hours_ranges is not None: 188 | contract_type_key = 'hphc' if data['contract_type'] == CONTRACT_TYPE_HPHC else 'tempo' 189 | tarif_actuel = self.data[contract_type_key+'_variable_hp_ttc'] 190 | now = datetime.now().time() 191 | for range in off_peak_hours_ranges.split(','): 192 | if not re.match(r'([0-1]?[0-9]|2[0-3]):[0-5][0-9]-([0-1]?[0-9]|2[0-3]):[0-5][0-9]', range): 193 | continue 194 | 195 | hours = range.split('-') 196 | start_at = str_to_time(hours[0]) 197 | end_at = str_to_time(hours[1]) 198 | 199 | if time_in_between(now, start_at, end_at): 200 | tarif_actuel = self.data[contract_type_key+'_variable_hc_ttc'] 201 | break 202 | 203 | self.data['tarif_actuel_ttc'] = tarif_actuel 204 | 205 | self.logger.info('EDF Tarif') 206 | self.logger.info(self.data) 207 | 208 | return self.data 209 | -------------------------------------------------------------------------------- /custom_components/tarif_edf/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "tarif_edf", 3 | "name": "Tarif EDF", 4 | "codeowners": [ 5 | "@delphiki" 6 | ], 7 | "config_flow": true, 8 | "documentation": "https://github.com/delphiki/hass-tarif-edf", 9 | "iot_class": "cloud_polling", 10 | "issue_tracker": "https://github.com/delphiki/hass-tarif-edf/issues", 11 | "version": "2.0.1" 12 | } 13 | -------------------------------------------------------------------------------- /custom_components/tarif_edf/sensor.py: -------------------------------------------------------------------------------- 1 | from homeassistant.core import HomeAssistant 2 | from homeassistant.config_entries import ConfigEntry 3 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 4 | from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo 5 | from homeassistant.components.sensor import ( 6 | SensorEntity, 7 | ) 8 | 9 | from homeassistant.helpers.update_coordinator import ( 10 | CoordinatorEntity, 11 | ) 12 | 13 | 14 | from .coordinator import TarifEdfDataUpdateCoordinator 15 | 16 | from .const import ( 17 | DOMAIN, 18 | CONTRACT_TYPE_BASE, 19 | CONTRACT_TYPE_HPHC, 20 | CONTRACT_TYPE_TEMPO, 21 | ) 22 | 23 | async def async_setup_entry( 24 | hass: HomeAssistant, 25 | config_entry: ConfigEntry, 26 | async_add_entities: AddEntitiesCallback 27 | ) -> None: 28 | coordinator: TarifEdfDataUpdateCoordinator = hass.data[DOMAIN][ 29 | config_entry.entry_id 30 | ]["coordinator"] 31 | 32 | sensors = [ 33 | TarifEdfSensor(coordinator, 'contract_power', f"Puissance souscrite {coordinator.data['contract_type']} {coordinator.data['contract_power']}kVA", 'kVA'), 34 | ] 35 | 36 | if coordinator.data['contract_type'] == CONTRACT_TYPE_BASE: 37 | sensors.extend([ 38 | TarifEdfSensor(coordinator, 'base_variable_ttc', 'Tarif Base TTC', 'EUR/kWh'), 39 | ]) 40 | elif coordinator.data['contract_type'] == CONTRACT_TYPE_HPHC: 41 | sensors.extend([ 42 | TarifEdfSensor(coordinator, 'hphc_variable_hc_ttc', 'Tarif Heures creuses TTC', 'EUR/kWh'), 43 | TarifEdfSensor(coordinator, 'hphc_variable_hp_ttc', 'Tarif Heures pleines TTC', 'EUR/kWh'), 44 | ]) 45 | elif coordinator.data['contract_type'] == CONTRACT_TYPE_TEMPO: 46 | sensors.extend([ 47 | TarifEdfSensor(coordinator, 'tempo_couleur', 'Tarif Tempo Couleur'), 48 | TarifEdfSensor(coordinator, 'tempo_couleur_hier', 'Tarif Tempo Couleur Hier'), 49 | TarifEdfSensor(coordinator, 'tempo_couleur_aujourdhui', "Tarif Tempo Couleur Aujourd'hui"), 50 | TarifEdfSensor(coordinator, 'tempo_couleur_demain', 'Tarif Tempo Couleur Demain'), 51 | TarifEdfSensor(coordinator, 'tempo_variable_hc_ttc', 'Tarif Tempo Heures creuses TTC', 'EUR/kWh'), 52 | TarifEdfSensor(coordinator, 'tempo_variable_hp_ttc', 'Tarif Tempo Heures pleines TTC', 'EUR/kWh'), 53 | TarifEdfSensor(coordinator, 'tempo_variable_hc_bleu_ttc', 'Tarif Bleu Tempo Heures creuses TTC', 'EUR/kWh'), 54 | TarifEdfSensor(coordinator, 'tempo_variable_hp_bleu_ttc', 'Tarif Bleu Tempo Heures pleines TTC', 'EUR/kWh'), 55 | TarifEdfSensor(coordinator, 'tempo_variable_hc_rouge_ttc', 'Tarif Rouge Tempo Heures creuses TTC', 'EUR/kWh'), 56 | TarifEdfSensor(coordinator, 'tempo_variable_hp_rouge_ttc', 'Tarif Rouge Tempo Heures pleines TTC', 'EUR/kWh'), 57 | TarifEdfSensor(coordinator, 'tempo_variable_hc_blanc_ttc', 'Tarif Blanc Tempo Heures creuses TTC', 'EUR/kWh'), 58 | TarifEdfSensor(coordinator, 'tempo_variable_hp_blanc_ttc', 'Tarif Blanc Tempo Heures pleines TTC', 'EUR/kWh'), 59 | ]) 60 | 61 | if coordinator.data['tarif_actuel_ttc'] is not None: 62 | sensors.append( 63 | TarifEdfSensor(coordinator, 'tarif_actuel_ttc', f"Tarif actuel {coordinator.data['contract_type']} {coordinator.data['contract_power']}kVA TTC", 'EUR/kWh') 64 | ) 65 | 66 | async_add_entities(sensors, False) 67 | 68 | class TarifEdfSensor(CoordinatorEntity, SensorEntity): 69 | """Representation of a Tarif EDF sensor.""" 70 | 71 | def __init__(self, coordinator, coordinator_key: str, name: str, unit_of_measurement: str = None) -> None: 72 | """Initialize the Tarif EDF sensor.""" 73 | super().__init__(coordinator) 74 | contract_name = str.upper(self.coordinator.data['contract_type']) + " " + self.coordinator.data['contract_power'] + "kVA" 75 | 76 | self._coordinator_key = coordinator_key 77 | self._name = name 78 | self._attr_unique_id = f"tarif_edf_{self._name}" 79 | self._attr_name = name 80 | self._attr_device_info = DeviceInfo( 81 | name=f"Tarif EDF - {contract_name}", 82 | entry_type=DeviceEntryType.SERVICE, 83 | identifiers={ 84 | (DOMAIN, f"Tarif EDF - {contract_name}") 85 | }, 86 | manufacturer="Tarif EDF", 87 | model=contract_name, 88 | ) 89 | if (unit_of_measurement is not None): 90 | self._attr_unit_of_measurement = unit_of_measurement 91 | 92 | @property 93 | def native_value(self): 94 | """Return the state of the sensor.""" 95 | if self.coordinator.data[self._coordinator_key] is None: 96 | return 'unavailable' 97 | else: 98 | return self.coordinator.data[self._coordinator_key] 99 | 100 | @property 101 | def extra_state_attributes(self): 102 | """Return the state attributes.""" 103 | return { 104 | 'updated_at': self.coordinator.last_update_success_time, 105 | 'unit_of_measurement': self._attr_unit_of_measurement, 106 | } 107 | 108 | @property 109 | def available(self) -> bool: 110 | """Return if entity is available.""" 111 | return self.coordinator.last_update_success and self.coordinator.data[self._coordinator_key] is not None 112 | -------------------------------------------------------------------------------- /custom_components/tarif_edf/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "contract_power": "Puissance (kVA)", 7 | "contract_type": "Contrat" 8 | } 9 | } 10 | }, 11 | "error": { 12 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 13 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 14 | "unknown": "[%key:common::config_flow::error::unknown%]" 15 | }, 16 | "abort": { 17 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 18 | } 19 | }, 20 | "options": { 21 | "step": { 22 | "init": { 23 | "description": "Customize the way the integration works", 24 | "data": { 25 | "refresh_interval": "Data refresh interval (in days)", 26 | "off_peaks_hours_ranges": "Off peak hours ranges (HH:MM-HH:MM,HH:MM-HH:MM,...)" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /custom_components/tarif_edf/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured" 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect", 8 | "invalid_auth": "Invalid authentication", 9 | "unknown": "Unexpected error" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "contract_type": "Option", 15 | "power": "Power (kVA)" 16 | } 17 | } 18 | } 19 | }, 20 | "options": { 21 | "step": { 22 | "init": { 23 | "data": { 24 | "refresh_interval": "Data refresh interval (in days)", 25 | "off_peak_hours_ranges": "Off peak hours ranges (HH:MM-HH:MM,HH:MM-HH:MM,...)" 26 | }, 27 | "description": "Customize the way the integration works" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /custom_components/tarif_edf/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "L'appareil est déjà configuré" 5 | }, 6 | "error": { 7 | "cannot_connect": "Échec de connexion", 8 | "invalid_auth": "Authentification invalide", 9 | "unknown": "Erreur inattendue" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "contract_type": "Option", 15 | "contract_power": "Puissance (kVA)" 16 | } 17 | } 18 | } 19 | }, 20 | "options": { 21 | "step": { 22 | "init": { 23 | "data": { 24 | "refresh_interval": "Intervalle de mise à jour (en jours)", 25 | "off_peak_hours_ranges": "Créneaux heures creuses (HH:MM-HH:MM,HH:MM-HH:MM,...)" 26 | }, 27 | "description": "Personnalisez le fonctionnement de l'intégration" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tarif EDF", 3 | "country": "FR", 4 | "render_readme": true, 5 | "zip_release": true, 6 | "filename": "tarif_edf.zip" 7 | } 8 | --------------------------------------------------------------------------------