├── custom_components └── currentcost │ ├── __init__.py │ ├── repository.json │ ├── manifest.json │ └── sensor.py ├── icon.png ├── logo.png ├── hacs.json ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── CHANGELOG.md ├── info.md └── README.md /custom_components/currentcost/__init__.py: -------------------------------------------------------------------------------- 1 | """The Current Cost component.""" -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lolouk44/CurrentCost_HA_CC/HEAD/icon.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lolouk44/CurrentCost_HA_CC/HEAD/logo.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Current Cost", 3 | "domains": ["sensor"], 4 | "iot_class": "local push" 5 | } -------------------------------------------------------------------------------- /custom_components/currentcost/repository.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CurrentCost_HA_CC", 3 | "url": "https://github.com/lolouk44/CurrentCost_HA_CC/", 4 | "maintainer": "lolouk44" 5 | } -------------------------------------------------------------------------------- /custom_components/currentcost/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "currentcost", 3 | "name": "Current Cost", 4 | "version": "0.2.4", 5 | "documentation": "https://github.com/lolouk44/CurrentCost_HA_CC", 6 | "requirements": [ 7 | "pyserial-asyncio==0.4", 8 | "xmltodict==0.12.0" 9 | ], 10 | "dependencies": [], 11 | "issue_tracker": "https://github.com/lolouk44/CurrentCost_HA_CC/issues", 12 | "codeowners": [ 13 | "@lolouk44" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | --- 11 | name: Bug report 12 | about: Create a report to help us improve 13 | title: '' 14 | labels: '' 15 | assignees: '' 16 | 17 | --- 18 | 19 | --- 20 | name: Bug report 21 | about: Create a report to help us improve 22 | title: '' 23 | labels: bug 24 | assignees: '' 25 | 26 | --- 27 | 28 | **Describe the bug** 29 | A clear and concise description of what the bug is. 30 | 31 | **To Reproduce** 32 | Steps to reproduce the behaviour, including error message if any. 33 | 34 | **Expected behaviour** 35 | A clear and concise description of what you expected to happen. 36 | 37 | **Screenshots** 38 | If applicable, add screenshots to help explain your problem. 39 | 40 | **Desktop/Server/Device (please complete the following information):** 41 | - Device used to run the Custom Component [e.g. Raspberry Pi, NUC] 42 | - Method of installation [e.g. via HACS or manual] 43 | - Version of the Custom Component installed? 44 | - Model of the Current Cost Device 45 | 46 | **Additional context** 47 | Add any other context about the problem here. 48 | 49 | **Logs** 50 | Please set the Current Cost Custom Component's logging level to debug and provide relevant logs section from Home-Assistant. 51 | To Enable debug logging level, add this to your configuration.yaml and restart: 52 | 53 | ``` 54 | logger: 55 | default: error 56 | logs: 57 | custom_components.currentcost: debug 58 | ``` 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.2.4] - 2023-01-14 2 | ### Added 3 | - Support for unique_id to help sensor customisation 4 | ### Changed 5 | - Replaced deprecated DEVICE_CLASS_* Constants ([fixes #20](https://github.com/lolouk44/CurrentCost_HA_CC/issues/20)) 6 | - Fixed default icon (flash-circle -> lightning-bolt-circle) 7 | 8 | ## [0.2.3] - 2022-09-18 9 | ### Changed 10 | - Gracefully handle when unable to read serial port ([fixes #17](https://github.com/lolouk44/CurrentCost_HA_CC/issues/17)) 11 | 12 | ## [0.2.2] - 2021-12-14 13 | ### Changed 14 | - Updated Sensor definition: replace deprecated device_state_attributes with extra_state_attributes 15 | 16 | ## [0.2.1] - 2021-08-19 17 | ### Added 18 | - Added handling of temperature in Fahrenheit 19 | 20 | ## [0.2.0] - 2021-08-11 21 | ### Added 22 | - Added last 24h and last 30 days energy used in KWh to the attributes 23 | 24 | ## [0.1.10] - 2021-08-10 25 | ### Changed 26 | - Removed "Setting up State Class" error message used for testing 27 | 28 | ## [0.1.8] - 2021-08-09 29 | ### Added 30 | - Updated support for [Long Term Statistics](https://www.home-assistant.io/blog/2021/08/04/release-20218/#long-term-statistics) 31 | ### Changed 32 | - Updated README and INFO to use "modern" Template Sensors and allow support for Long Term Statistics for templated sensors 33 | 34 | ## [0.1.7] - 2021-07-28 35 | ### Breaking Change 36 | - Standardized the way sensor data is reported (removed the unit of measurement) - Please update your templates (see README) 37 | 38 | ## [0.1.6] - 2021-07-27 39 | ### Breaking Change 40 | - Standardized the way sensor data is reported (removed the unit of measurement) - Please update your templates (see README) 41 | 42 | ## [0.1.5] - 2021-03-30 43 | ### Changed 44 | - Added version to manifest 45 | 46 | ## [0.1.4] - 2020-09-10 47 | ### Changed 48 | - repo completely overhauled for a PR to make it ready for publishing in HACS 49 | 50 | ## [0.1.3] - 2020-09-08 51 | ### Changed 52 | - Repo structure changed to be HACS Compliant 53 | 54 | ## [0.1.2] - 2020-09-08 55 | ### Changed 56 | - Better error handling: Stop any data processing when error parsing data 57 | 58 | ## [0.1.1] - 2020-04-03 59 | ### Added 60 | - First release (but not tagged on Github) 61 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | ##Usage: 2 | 3 | **Please note: Home Assistant 2021.12 or higher is required for this custom component to work** 4 | 5 | Add the following code to your `configuration.yaml` file under the existing `sensor` and `template` headers. (do not copy/paste the `sensor` or `template` headers). 6 | If `sensor` or `template` do not already exist, add the code block including the `sensor` or `template` header. 7 | 8 | ```yaml 9 | sensor: 10 | - platform: currentcost 11 | serial_port: /dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller-if00-port0 12 | name: Current Cost 13 | baudrate: 57600 14 | devices: 15 | - 0 16 | - 2 17 | - 9 18 | # Add this sensor if you want to see data in the energy tab 19 | - platform: integration 20 | source: sensor.current_cost 21 | name: Total Energy 22 | unit_prefix: k 23 | round: 2 24 | 25 | template: 26 | - sensor: 27 | - name: "CurrentCost Temperature" 28 | unit_of_measurement: '°C' # or °F for Fahrenheits 29 | state: '{{ state_attr("sensor.current_cost", "Temperature") | float -3 }}' # Manual adjustment of -3°C in case the temp sensor is higher than real temperature 30 | device_class: temperature 31 | state_class: measurement # Add state_class: measurement for long term statistics are required 32 | - sensor: 33 | - name: "CurrentCost Power" 34 | unit_of_measurement: 'W' 35 | state: '{{ state_attr("sensor.current_cost", "Appliance 0") }}' 36 | device_class: power 37 | state_class: measurement 38 | - sensor: 39 | - name: "Dehumidifier Power" 40 | unit_of_measurement: 'W' 41 | state: '{{ state_attr("sensor.current_cost", "Appliance 2") }}' 42 | device_class: power 43 | state_class: measurement 44 | - sensor: 45 | - name: "Total Energy Used Last 24h" # Note: this data is published by the Current Cost device every 2h 46 | unit_of_measurement: 'KWh' 47 | state: '{{ state_attr("sensor.current_cost", "Appliance 0 Last 24h") }}' 48 | device_class: energy 49 | ``` 50 | 51 | {% if installed %} 52 | 53 | ## Changes as compared to your installed version: 54 | The sensors unit of measurement is no longer returned with the sensor 55 | Please update your template sensors (see README) 56 | 57 | ### Breaking Changes 58 | The sensors unit of measurement is no longer returned with the sensor 59 | Please update your template sensors (see README) 60 | 61 | ### Changes 62 | 63 | ### Features 64 | 65 | {% if version_installed.replace(".","") | int < 13 %} 66 | - Added HACS 67 | {% endif %} 68 | 69 | ### Bugfixes 70 | 71 | --- 72 | {% endif %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![currentcost logo](logo.png) 2 | 3 | [![version](https://img.shields.io/github/v/release/lolouk44/CurrentCost_HA_CC)](https://github.com/lolouk44/CurrentCost_HA_CC/releases) 4 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) 5 | # CurrentCost Custom Component for Home Assistant 6 | 7 | **Please note: Home Assistant 2021.12 or higher is required for this custom component to work** 8 | 9 | This repo is a custom component for [Home Assistant](https://www.home-assistant.io/) 10 | 11 | The `currentcost` sensor platform is using the data provided by a [CurrentCost](http://www.currentcost.com/) device connected to the serial port via a [data cable](http://www.currentcost.com/product-datacable.html). 12 | 13 | The sensor returns the Total Power (usually on Appliance 0) as a state, and the temperature as an attribute. 14 | It is also possible to list additional appliances by listing the appliance number that CurrentCost devices are paired with 15 | 16 | Confirmed working devices: 17 | - Currentcost Envi (aka CC128) 18 | - Currentcost EnviR 19 | 20 | ## HACS Installation 21 | 22 | The easiest way to install this custom component is via [HACS](https://hacs.xyz/) 23 | 1) Follow the [installation instructions](https://hacs.xyz/docs/installation/prerequisites) to install HACS 24 | 2) Click on the HACS icon in the left side bar, click on the elipsis on the right handside and select `Custom Repositories` 25 | 3) enter `https://github.com/lolouk44/CurrentCost_HA_CC` in the URL box, select `Integration` as a category 26 | 27 | 28 | ## Manual Installation 29 | To install the CurrentCost custom component: 30 | 1) Create a folder called `custom_components` in your config folder (same folder where configuration.yaml is locate, if that folder does not already exist) 31 | 2) Create a folder called `currentcost` (no spaces, lowercase) 32 | 3) Copy the files inside the [CurrentCost_HA_CC](https://github.com/lolouk44/CurrentCost_HA_CC/tree/master/custom_components/CurrentCost_HA_CC) folder into the `currentcost` folder 33 | 34 | 35 | ## Configuration 36 | 37 | To setup a CurrentCost sensor to your installation: 38 | 1) Add the following code to your `configuration.yaml` file under the existing `sensor` and `template` headers. (do not copy/paste the `sensor` or `template` headers). 39 | If `sensor` or `template` do not already exist, add the code block including the `sensor` or `template` header. 40 | 41 | ```yaml 42 | # Example configuration.yaml entry 43 | sensor: 44 | - platform: currentcost 45 | serial_port: /dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller-if00-port0 46 | name: Current Cost 47 | baudrate: 57600 48 | devices: 49 | - 0 50 | - 2 51 | - 9 52 | # Add this sensor if you want to see data in the energy tab 53 | - platform: integration 54 | source: sensor.current_cost 55 | name: Total Energy 56 | unit_prefix: k 57 | round: 2 58 | 59 | template: 60 | - sensor: 61 | - name: "CurrentCost Temperature" 62 | unit_of_measurement: '°C' 63 | state: '{{ state_attr("sensor.current_cost", "Temperature") | float -3 }}' # Manual adjustment of -3°C in case the temp sensor is high than real temperature 64 | device_class: temperature 65 | state_class: measurement # Add state_class: measurement for long term statistics are required 66 | - sensor: 67 | - name: "CurrentCost Power" 68 | unit_of_measurement: 'W' 69 | state: '{{ state_attr("sensor.current_cost", "Appliance 0") }}' 70 | device_class: power 71 | state_class: measurement 72 | - sensor: 73 | - name: "Dehumidifier Power" 74 | unit_of_measurement: 'W' 75 | state: '{{ state_attr("sensor.current_cost", "Appliance 2") }}' 76 | device_class: power 77 | state_class: measurement 78 | - sensor: 79 | - name: "Total Energy Used Last 24h" # Note: this data is published by the Current Cost device every 2h 80 | unit_of_measurement: 'KWh' 81 | state: '{{ state_attr("sensor.current_cost", "Appliance 0 Last 24h") }}' 82 | device_class: energy 83 | ``` 84 | 85 | 86 | ### serial_port: 87 | **description**: Local serial port where the sensor is connected and access is granted. 88 | **required**: true 89 | **type**: string 90 | note: when using HA in a docker environment, make sure you assign a name to the device when mounting it, e.g. `--device=/dev/ttyUSB0:/dev/ttyUSB0` as opposed to just `--device=/dev/ttyUSB0` 91 | ### name: 92 | **description**: Friendly name to use for the frontend. Default to "Current Cost". 93 | **required**: false 94 | **type**: string 95 | ### baudrate: 96 | **description**: Baudrate of the serial port. 57600 is the value needed for EnviR devices. 97 | **required**: false 98 | **default**: 57600 Bps 99 | **type**: integer 100 | ### devices: 101 | **description**: List of appliance numbers paired with a CurrentCost sensor 102 | **required**: false 103 | **default**: 0 104 | **type**: integer 105 | -------------------------------------------------------------------------------- /custom_components/currentcost/sensor.py: -------------------------------------------------------------------------------- 1 | """Support for reading Current Cost data from a serial port.""" 2 | import json 3 | import logging 4 | import xmltodict 5 | import sys 6 | 7 | import serial_asyncio 8 | import voluptuous as vol 9 | 10 | from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity, SensorDeviceClass, SensorStateClass 11 | from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_DEVICES, EVENT_HOMEASSISTANT_STOP, POWER_WATT 12 | import homeassistant.helpers.config_validation as cv 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | CONF_SERIAL_PORT = "serial_port" 18 | CONF_BAUDRATE = "baudrate" 19 | CONF_DEVICES = "devices" 20 | 21 | DEFAULT_NAME = "Current Cost" 22 | DEFAULT_ID = "abaaa250-fd59-46e1-abd8-07545fb2b297" 23 | DEFAULT_BAUDRATE = 57600 24 | DEFAULT_DEVICES = [0,1,2,3,4,5,6,7,8,9] 25 | 26 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 27 | { 28 | vol.Required(CONF_SERIAL_PORT): cv.string, 29 | vol.Optional(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): cv.positive_int, 30 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 31 | vol.Optional(CONF_UNIQUE_ID, default=DEFAULT_ID): cv.string, 32 | vol.Optional(CONF_DEVICES, default=DEFAULT_DEVICES): vol.All(cv.ensure_list, [vol.Range(min=0, max=9)]), 33 | } 34 | ) 35 | 36 | 37 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 38 | """Set up the Current Cost sensor platform.""" 39 | name = config.get(CONF_NAME) 40 | unique_id = config.get(CONF_UNIQUE_ID) 41 | port = config.get(CONF_SERIAL_PORT) 42 | baudrate = config.get(CONF_BAUDRATE) 43 | devices = config.get(CONF_DEVICES) 44 | _LOGGER.debug("devices: %s", config.get(CONF_DEVICES)) 45 | #sensor = [] 46 | sensor = CurrentCostSensor(name, f"current-cost-{unique_id}", port, baudrate, devices) 47 | #for variable in devices: 48 | # sensor.append(CurrentCostSensor(f"{name}_appliance_{variable}", port, baudrate)) 49 | #sensor.append(CurrentCostSensor(f"{name}_temperature", port, baudrate)) 50 | 51 | hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, sensor.stop_serial_read()) 52 | async_add_entities([sensor], True) 53 | 54 | 55 | class CurrentCostSensor(SensorEntity): 56 | """Representation of a Current Cost sensor.""" 57 | 58 | def __init__(self, name, unique_id, port, baudrate, devices): 59 | """Initialize the Current Cost sensor.""" 60 | self._name = name 61 | self._attr_unique_id = unique_id 62 | self._unit = POWER_WATT 63 | self._icon = "mdi:flash-outline" 64 | self._device_class = SensorDeviceClass.POWER 65 | self._state_class = SensorStateClass.MEASUREMENT 66 | self._state = None 67 | self._port = port 68 | self._baudrate = baudrate 69 | self._serial_loop_task = None 70 | self._attributes = {"Temperature": None} 71 | self._devices = devices 72 | for variable in devices: 73 | self._attributes[f"Appliance {variable}"] = None 74 | self._attributes[f"Appliance {variable} Last 24h"] = None 75 | self._attributes[f"Appliance {variable} Last 30 days"] = None 76 | 77 | async def async_added_to_hass(self): 78 | """Handle when an entity is about to be added to Home Assistant.""" 79 | self._serial_loop_task = self.hass.loop.create_task( 80 | self.serial_read(self._port, self._baudrate) 81 | ) 82 | 83 | async def serial_read(self, device, rate, **kwargs): 84 | """Read the data from the port.""" 85 | reader, _ = await serial_asyncio.open_serial_connection( 86 | url=device, baudrate=rate, **kwargs 87 | ) 88 | while True: 89 | try: 90 | line = await reader.readline() 91 | line = line.decode("utf-8").strip() 92 | _LOGGER.debug("Line Received: %s", line) 93 | except Exception as error: 94 | _LOGGER.error("Error Reading From Serial Port: %s", error) 95 | pass 96 | try: 97 | data = xmltodict.parse(line) 98 | # Data can be parsed from line, continuing 99 | # First read real time data 100 | try: 101 | appliance = int(data['msg']['sensor']) 102 | except: 103 | appliance = None 104 | pass 105 | 106 | temperature = None 107 | try: 108 | temperature = float(data['msg']['tmpr']) 109 | except: 110 | pass 111 | try: 112 | temperature = float(data['msg']['tmprF']) 113 | except: 114 | pass 115 | try: 116 | imp = int(data['msg']['imp']) 117 | ipu = int(data['msg']['ipu']) 118 | except: 119 | imp = None 120 | ipu = None 121 | pass 122 | try: 123 | wattsch1 = int(data['msg']['ch1']['watts']) 124 | except: 125 | wattsch1 = 0 126 | pass 127 | try: 128 | wattsch2 = int(data['msg']['ch2']['watts']) 129 | except: 130 | wattsch2 = 0 131 | pass 132 | try: 133 | wattsch3 = int(data['msg']['ch3']['watts']) 134 | except: 135 | wattsch3 = 0 136 | pass 137 | total_watts = wattsch1 + wattsch2 + wattsch3 138 | if appliance == 0: 139 | self._state = total_watts 140 | self._attributes[f"Channel 1"] = wattsch1 141 | self._attributes[f"Channel 2"] = wattsch2 142 | self._attributes[f"Channel 3"] = wattsch3 143 | if appliance is not None: 144 | if imp is not None: 145 | self._attributes[f"Impulses {appliance}"] = imp 146 | self._attributes[f"Impulses/Unit {appliance}"] = ipu 147 | else: 148 | self._attributes[f"Appliance {appliance}"] = total_watts 149 | if temperature is not None: 150 | self._attributes["Temperature"] = temperature 151 | 152 | # Then read history data 153 | 154 | for variable in self._devices: 155 | #self._attributes[f"Appliance {variable}"] = None 156 | try: 157 | if int(data['msg']['hist']['data'][int(variable)]['sensor']) == int(variable): 158 | applianceHist = int(data['msg']['hist']['data'][int(variable)]['sensor']) 159 | else: 160 | applianceHist = None 161 | except: 162 | applianceHist = None 163 | pass 164 | if applianceHist is not None: 165 | try: 166 | last24h = float(data['msg']['hist']['data'][applianceHist]['d001']) 167 | self._attributes[f"Appliance {applianceHist} Last 24h"] = last24h 168 | except: 169 | pass 170 | try: 171 | last30d = float(data['msg']['hist']['data'][applianceHist]['m001']) 172 | self._attributes[f"Appliance {applianceHist} Last 30 days"] = last30d 173 | except: 174 | pass 175 | 176 | # Then update HA Sensor info 177 | self.async_schedule_update_ha_state() 178 | 179 | # Data can not be parsed from line, raising exception 180 | except: 181 | _LOGGER.error(f"Error parsing data from serial port:\n {sys.exc_info()[1]}\n line received:\n {line}") 182 | pass 183 | 184 | async def stop_serial_read(self): 185 | """Close resources.""" 186 | if self._serial_loop_task: 187 | self._serial_loop_task.cancel() 188 | 189 | @property 190 | def name(self): 191 | """Return the name of the sensor.""" 192 | return self._name 193 | 194 | @property 195 | def should_poll(self): 196 | """No polling needed.""" 197 | return False 198 | 199 | @property 200 | def extra_state_attributes(self): 201 | """Return the attributes of the entity (if any JSON present).""" 202 | return self._attributes 203 | 204 | @property 205 | def state(self): 206 | """Return the state of the sensor.""" 207 | return self._state 208 | 209 | @property 210 | def unit_of_measurement(self): 211 | """Return the units of measurement.""" 212 | return self._unit 213 | 214 | @property 215 | def icon(self): 216 | """Return the icon of the sensor.""" 217 | return self._icon 218 | 219 | @property 220 | def device_class(self): 221 | """Return the device class of the sensor.""" 222 | return self._device_class 223 | 224 | @property 225 | def state_class (self): 226 | """Return the state class of the sensor.""" 227 | return self._state_class 228 | --------------------------------------------------------------------------------