├── CHANGELOG.md ├── hacs.json ├── custom_components └── mopeka_pro_check │ ├── __init__.py │ ├── services.yaml │ ├── manifest.json │ ├── const.py │ └── sensor.py ├── .github └── workflows │ └── hassfest.yaml ├── LICENSE ├── .gitignore └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | See release history in github 4 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mopeka Pro Check Tank Level BT Sensor", 3 | "domains": "sensor", 4 | "render_readme": true 5 | } 6 | -------------------------------------------------------------------------------- /custom_components/mopeka_pro_check/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mopeka Pro Check Propane Tank Level Sensor BLE Home Assistant Component 3 | 4 | Copyright (c) 2021 Sean Brogan 5 | 6 | SPDX-License-Identifier: MIT 7 | """ 8 | -------------------------------------------------------------------------------- /custom_components/mopeka_pro_check/services.yaml: -------------------------------------------------------------------------------- 1 | # Describe services made available by this integration 2 | 3 | discovered: 4 | # Description of the service 5 | description: Scans for 5 seconds looking for any Mopeka pro Check sensor reporting its green sync button pressed. In the state value it will return a CSV of mac addresses for any sensors found. 6 | -------------------------------------------------------------------------------- /custom_components/mopeka_pro_check/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "mopeka_pro_check", 3 | "name": "Mopeka Pro Check Tank Level BLE sensor integration", 4 | "documentation": "https://github.com/spbrogan/sensor.mopeka_pro_check", 5 | "requirements": ["mopeka_pro_check>=0.1.0"], 6 | "version": "1.0.3", 7 | "dependencies": [], 8 | "codeowners": ["@spbrogan"], 9 | "iot_class": "local_push" 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | # Trigger the workflow on push or pull request, 5 | # but only for the main branch 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | validate: 15 | runs-on: "ubuntu-latest" 16 | steps: 17 | - uses: "actions/checkout@v2" 18 | - uses: home-assistant/actions/hassfest@master 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sean Brogan 4 | 5 | SPDX-License-Identifier: MIT 6 | 7 | https://spdx.org/licenses/MIT.html 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 10 | software and associated documentation files (the "Software"), to deal in the Software 11 | without restriction, including without limitation the rights to use, copy, modify, 12 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice (including the next paragraph) 17 | shall be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 20 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 21 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 22 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /custom_components/mopeka_pro_check/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Mopeka Propane Tank Level Sensor BLE Home Assistant Component 2 | 3 | Copyright (c) 2021 Sean Brogan 4 | Copyright (c) 2020 Home-Is-Where-You-Hang-Your-Hack 5 | 6 | SPDX-License-Identifier: MIT 7 | 8 | """ 9 | 10 | """Fixed constants.""" 11 | 12 | DOMAIN = "mopeka_pro_check" 13 | 14 | # Configuration options 15 | CONF_HCI_DEVICE = "hci_device" 16 | CONF_MOPEKA_DEVICES = "mopeka_devices" 17 | CONF_TANK_TYPE = "tank_type" 18 | CONF_TANK_MAX_HEIGHT = "max_height" 19 | CONF_TANK_MIN_HEIGHT = "min_height" 20 | CONF_TANK_FIELD = "tank" 21 | 22 | CONF_TANK_TYPE_CUSTOM = "custom" 23 | CONF_TANK_TYPE_STD = "standard" 24 | 25 | # 26 | # HEIGHT in MM 27 | # 28 | CONF_SUPPORTED_STD_TANKS_MAP = { 29 | # Where to get real answers for what height is full 30 | 31 | # trial and error and compare with Mopeka official app 32 | "20lb_v": {CONF_TANK_MAX_HEIGHT: 254, CONF_TANK_MIN_HEIGHT: 38.1}, 33 | "30lb_v": {CONF_TANK_MAX_HEIGHT: 381, CONF_TANK_MIN_HEIGHT: 38.1}, 34 | "40lb_v": {CONF_TANK_MAX_HEIGHT: 508, CONF_TANK_MIN_HEIGHT: 38.1}, 35 | "100lb_v": {CONF_TANK_MAX_HEIGHT: 812.8, CONF_TANK_MIN_HEIGHT: 38.1} 36 | } 37 | 38 | CONF_SUPPORTED_STD_TANK_NAMES = tuple(CONF_SUPPORTED_STD_TANKS_MAP.keys()) 39 | 40 | # use CONF_MAC, CONF_NAME, CONF_SCAN_INTERVAL 41 | 42 | 43 | # Default values for configuration options 44 | DEFAULT_SCAN_INTERVAL = 60 45 | DEFAULT_HCI_DEVICE = "hci0" 46 | DEFAULT_STD_TANK = CONF_SUPPORTED_STD_TANK_NAMES[0] 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sensor.mopeka_pro_check 2 | 3 | # **THIS INTEGRATION NO LONGER WORKS AS OF SUMMER 2022!!!** 4 | Home Assistant now has native support but the native integration doesn't do tank percentage calculation. Follow this discussion for updates or comments. 5 | 6 | There is a great esphome integration that works well and is easy to use. 7 | Please checkout the esphome integration here 8 | 9 | ---- 10 | # OLD Stuff below 11 | 12 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/hacs/integration) 13 | 14 | Home Assistant custom component to support Mopeka Pro Check Tank Level Sensors 15 | A custom component for [Home Assistant](https://www.home-assistant.io) that listens for the advertisement message broadcast by Mopeka Bluetooth propane tank level sensors. 16 | 17 | ## Supported Devices 18 | 19 | * Mopeka Pro check 20 | 21 | ## Installation 22 | 23 | **1. Install the custom component:** 24 | 25 | * The easiest way is to install it with [HACS](https://hacs.xyz/). First install [HACS](https://hacs.xyz/) if you don't have it yet. After installation, the custom component can be found in the HACS store under integrations. 26 | 27 | * Alternatively, you can install it manually. Just copy paste the content of the `sensor.mopeka_pro_check/custom_components` folder in your `config/custom_components` directory. 28 | As example, you will get the `sensor.py` file in the following path: `/config/custom_components/mopeka_pro_check/sensor.py`. 29 | 30 | **2. Restart Home Assistant:** 31 | 32 | * Restarts just make everything better. 33 | 34 | **3. Add the platform to your configuration.yaml file (see [below](#configuration))** 35 | 36 | **4. Restart Home Assistant:** 37 | 38 | * A second restart may be required to load the configuration. Within a few minutes, the sensors should be added to your home-assistant automatically (at least two `scan_interval` may be required. If the `scan_interval` is set to a time greater than two minutes, at least four `scan_interval` may be required). 39 | 40 | * Make sure you have woken up the sensor by following the directions supplied with your sensor. Sensors 41 | are shipped from the factory in a low power mode and will need to be woken up to start reporting. 42 | 43 | * Physically install the sensor following the supplied directions. 44 | 45 | ## Troubleshooting and help 46 | 47 | This is still a work in progress with very limited testing. 48 | Please use github issues, pull requests, and discussion to provide feedback, open issues, or ask questions. 49 | I am new to developing for HomeAssistant so for general questions or system questions using 50 | the home assistant forums would be better. Eventually it looks like most add a community post 51 | and i will do so once this component works or is in shape for sharing. 52 | 53 | ## Configuration Variables 54 | 55 | Specify the sensor platform `mopeka_pro_check` and a list of devices with unique MAC address. 56 | 57 | *NOTE*: device name is optional. If not provided, devices will be labeled using the MAC address 58 | 59 | ``` yaml 60 | sensor: 61 | - platform: mopeka_pro_check 62 | mopeka_devices: 63 | # Just use default tank type 64 | - mac: "A1:B2:C3:D4:E5:F6" 65 | name: RV Right Tank 66 | # Set the tank type to a standard 20lbs vertical tank 67 | - mac: "A6:B5:C4:D3:E2:F1" 68 | name: RV Left Tank 69 | tank_type: "standard" 70 | tank: "20lb_v" 71 | # Set the max height in MM using custom tank 72 | - mac: "A0:B0:C0:D0:E0:F0" 73 | name: BBQ 74 | tank_type: "custom" 75 | max_height: 452.10 76 | min_height: 23 77 | ``` 78 | 79 | ### Additional configuration options 80 | 81 | | Option | Type |Default Value | Description | 82 | | -- | -- | -- | -- | 83 | | `scan_interval` | positive integer | `60` | The scan_interval in seconds during which the sensor readings are collected and transmitted to Home Assistant. The Mopeka device broadcast rate is configurable using the sensor buttons but this scan_interval helps to limit the amount of mostly duplicate data stored in Home Assistant's database since tank level should not change that quickly | 84 | | `hci_device`| string | `hci0` | HCI device name used for scanning. 85 | 86 | ## Setting up a new sensor 87 | 88 | _New in Version 0.2.0_ 89 | 90 | For convenience a service is implemented `mopeka_pro_check.discovered` that when called will pause scanning for 5 seconds, look for any mopeka sensor 91 | that has its green "sync" button pressed and report the mac in the `mopeka_pro_check.discovered` state. To do this from Home Assistant UI do the following: 92 | 93 | 1. Go to "Developer Tools" 94 | 2. Go to "Service" tab 95 | 3. Press the green button on your sensor 10 times quickly to get it out of mfg mode 96 | 4. Press the "Call Service" button in Home Assistant and then press and hold the green button on your sensor. 97 | 5. After 5 seconds go to the "States" tab 98 | 6. Bring up the state of entity `mopeka_pro_check.discovered` 99 | 7. Look at the "State" and you will see a csv of MAC addresses for any sensor that reported the sync button was pressed. 100 | 8. Now you can go to your configuration.yml and add the sensor. 101 | 9. Reboot and the new entity should show up. 102 | 103 | ## Credits 104 | 105 | This sensor component was copied from [custom-components/sensor.groveetemp_bt_hci](https://github.com/Home-Is-Where-You-Hang-Your-Hack/sensor.goveetemp_bt_hci) at commit [84cd857ec71e5c076fb37f6748d514aed3c0d210](https://github.com/Home-Is-Where-You-Hang-Your-Hack/sensor.goveetemp_bt_hci/commit/84cd857ec71e5c076fb37f6748d514aed3c0d210) 106 | 107 | That repo also mentions 108 | 109 | >This was originally based on/shamelessly copied from [custom-components/sensor.mitemp_bt](https://github.com/custom-components/sensor.mitemp_bt). I want to thank [@tsymbaliuk](https://community.home-assistant.io/u/tsymbaliuk) and [@Magalex](https://community.home-assistant.io/u/Magalex) for providing a blueprint for developing my Home Assistant component. 110 | 111 | So I want to thank [@Thrilleratplay](https://community.home-assistant.io/u/thrilleratplay) as well as those mentioned above. 112 | 113 | Finally, [Mopeka Products](https://mopeka.com/) was extremely helpful. Their support has been great and the short time I have used their sensor I have been happy with its capability. I would recommend their sensor. 114 | 115 | ## Notices 116 | 117 | Since some parts of this were copied from [custom-components/sensor.mitemp_bt](https://github.com/custom-components/sensor.mitemp_bt) some components will retain that projects copyright. 118 | 119 | ``` txt 120 | MIT License 121 | 122 | Copyright (c) 2020 Home-Is-Where-You-Hang-Your-Hack 123 | 124 | Permission is hereby granted, free of charge, to any person obtaining a copy 125 | of this software and associated documentation files (the "Software"), to deal 126 | in the Software without restriction, including without limitation the rights 127 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 128 | copies of the Software, and to permit persons to whom the Software is 129 | furnished to do so, subject to the following conditions: 130 | 131 | The above copyright notice and this permission notice shall be included in all 132 | copies or substantial portions of the Software. 133 | 134 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 135 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 136 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 137 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 138 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 139 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 140 | SOFTWARE. 141 | ``` 142 | -------------------------------------------------------------------------------- /custom_components/mopeka_pro_check/sensor.py: -------------------------------------------------------------------------------- 1 | """Mopeka Propane Tank Level Sensor integration 2 | 3 | Copyright (c) 2021 Sean Brogan 4 | Copyright (c) 2020 Home-Is-Where-You-Hang-Your-Hack 5 | 6 | SPDX-License-Identifier: MIT 7 | 8 | """ 9 | from datetime import timedelta 10 | import logging 11 | import voluptuous as vol 12 | from typing import List, Callable, Optional 13 | 14 | from mopeka_pro_check.service import MopekaService, GetServiceInstance, ServiceScanningMode 15 | from mopeka_pro_check.sensor import MopekaSensor 16 | 17 | from homeassistant.components.sensor import PLATFORM_SCHEMA # type: ignore 18 | import homeassistant.helpers.config_validation as cv # type: ignore 19 | from homeassistant.helpers.entity import Entity # type: ignore 20 | from homeassistant.helpers.event import track_point_in_utc_time # type: ignore 21 | import homeassistant.util.dt as dt_util # type: ignore 22 | from homeassistant.helpers.typing import ( 23 | ConfigType, 24 | DiscoveryInfoType, 25 | HomeAssistantType, 26 | ) 27 | 28 | from homeassistant.const import ( # type: ignore 29 | ATTR_BATTERY_LEVEL, 30 | CONF_MAC, 31 | CONF_NAME, 32 | CONF_SCAN_INTERVAL, 33 | PERCENTAGE, 34 | ) 35 | 36 | from .const import ( 37 | CONF_SUPPORTED_STD_TANK_NAMES, 38 | DEFAULT_SCAN_INTERVAL, 39 | DEFAULT_HCI_DEVICE, 40 | DEFAULT_STD_TANK, 41 | CONF_HCI_DEVICE, 42 | CONF_MOPEKA_DEVICES, 43 | CONF_TANK_TYPE, 44 | CONF_TANK_MAX_HEIGHT, 45 | CONF_TANK_MIN_HEIGHT, 46 | CONF_SUPPORTED_STD_TANKS_MAP, 47 | CONF_TANK_TYPE_CUSTOM, 48 | CONF_TANK_TYPE_STD, 49 | CONF_TANK_FIELD, 50 | DOMAIN, 51 | ) 52 | 53 | ############################################################################### 54 | 55 | DISCOVERY_SERVICE_NAME = "discovered" 56 | 57 | _LOGGER = logging.getLogger(__name__) 58 | 59 | 60 | DEVICES_SCHEMA = vol.Schema( 61 | { 62 | vol.Required(CONF_MAC): cv.string, 63 | vol.Optional(CONF_NAME): cv.string, 64 | vol.Optional(CONF_TANK_TYPE, default=CONF_TANK_TYPE_STD): vol.In( 65 | (CONF_TANK_TYPE_STD, CONF_TANK_TYPE_CUSTOM) 66 | ), 67 | vol.Optional(CONF_TANK_FIELD, default=DEFAULT_STD_TANK): vol.In( 68 | CONF_SUPPORTED_STD_TANK_NAMES 69 | ), 70 | vol.Optional(CONF_TANK_MAX_HEIGHT): cv.positive_float, 71 | vol.Optional(CONF_TANK_MIN_HEIGHT): cv.positive_float, 72 | } 73 | ) 74 | 75 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 76 | { 77 | vol.Optional( 78 | CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL 79 | ): cv.positive_int, 80 | vol.Optional(CONF_MOPEKA_DEVICES): vol.All([DEVICES_SCHEMA]), 81 | vol.Optional(CONF_HCI_DEVICE, default=DEFAULT_HCI_DEVICE): cv.string, 82 | } 83 | ) 84 | 85 | ############################################################################### 86 | 87 | 88 | # 89 | # Configure for Home Assistant 90 | # 91 | def setup_platform( 92 | hass: HomeAssistantType, 93 | config: ConfigType, 94 | add_entities: Callable, 95 | discovery_info: Optional[DiscoveryInfoType] = None, 96 | ) -> None: 97 | """Set up the sensor platform.""" 98 | _LOGGER.debug("Starting Mopeka Tank Level Sensor") 99 | 100 | service: MopekaService = GetServiceInstance() 101 | 102 | 103 | def ReportMopekaDevicesWithButtonPressed(call) -> None: 104 | """ handle service call to discover sensors with their button pressed""" 105 | 106 | ## 107 | ## 108 | ## This is really bad in that it calls sleep 109 | ## 110 | ## But given the infrequent use of this as it really is only 111 | ## a setup helper to get the MAC of a new sensor 112 | ## I am going to leave it until someone complains or has a PR that cleans 113 | ## all this up. 114 | ## 115 | service.DoSensorDiscovery() 116 | service.Start() 117 | from time import sleep 118 | sleep(5) 119 | service.Stop() 120 | 121 | # restore filtered mode scanning for known devices 122 | service._scanning_mode = ServiceScanningMode.FILTERED_MODE 123 | service.Start() 124 | 125 | results = "" 126 | for a in service.SensorDiscoveredList.keys(): 127 | results += a.address + "," 128 | results = results.rstrip(",") 129 | hass.states.set(DOMAIN + "." + DISCOVERY_SERVICE_NAME, results) 130 | 131 | 132 | 133 | def init_configured_devices() -> None: 134 | """Initialize configured mopeka devices.""" 135 | for conf_dev in config[CONF_MOPEKA_DEVICES]: 136 | # Initialize sensor object 137 | mac = conf_dev[CONF_MAC] 138 | device = MopekaSensor(mac) 139 | device.name = conf_dev.get(CONF_NAME, mac) 140 | 141 | # Initialize HA sensors 142 | tank_sensor = TankLevelSensor(device._mac, device.name) 143 | device.ha_sensor = tank_sensor 144 | 145 | # find tank type 146 | tt = conf_dev.get(CONF_TANK_TYPE) 147 | if tt == CONF_TANK_TYPE_CUSTOM: 148 | # if max height or min height missing uses default tank values 149 | device.max_height = float( 150 | conf_dev.get( 151 | CONF_TANK_MAX_HEIGHT, 152 | CONF_SUPPORTED_STD_TANKS_MAP[DEFAULT_STD_TANK].get( 153 | CONF_TANK_MAX_HEIGHT 154 | ), 155 | ) 156 | ) 157 | device.min_height = float( 158 | conf_dev.get( 159 | CONF_TANK_MIN_HEIGHT, 160 | CONF_SUPPORTED_STD_TANKS_MAP[DEFAULT_STD_TANK].get( 161 | CONF_TANK_MIN_HEIGHT 162 | ), 163 | ) 164 | ) 165 | getattr(tank_sensor, "_extra_state_attributes")[ 166 | CONF_TANK_FIELD 167 | ] = "n/a" 168 | else: 169 | std_type = conf_dev.get(CONF_TANK_FIELD) 170 | device.max_height = CONF_SUPPORTED_STD_TANKS_MAP[std_type].get( 171 | CONF_TANK_MAX_HEIGHT 172 | ) 173 | device.min_height = CONF_SUPPORTED_STD_TANKS_MAP[std_type].get( 174 | CONF_TANK_MIN_HEIGHT 175 | ) 176 | getattr(tank_sensor, "_extra_state_attributes")[ 177 | CONF_TANK_FIELD 178 | ] = std_type 179 | 180 | getattr(tank_sensor, "_extra_state_attributes")[CONF_TANK_TYPE] = tt 181 | getattr(tank_sensor, "_extra_state_attributes")[ 182 | "tank_height" 183 | ] = device.max_height 184 | 185 | service.AddSensorToMonitor(device) 186 | add_entities((tank_sensor,)) 187 | 188 | def update_ble_devices(config) -> None: 189 | """Discover Bluetooth LE devices.""" 190 | # _LOGGER.debug("Discovering Bluetooth LE devices") 191 | 192 | ATTR = "_extra_state_attributes" 193 | 194 | for device in service.SensorMonitoredList.values(): 195 | sensor = device.ha_sensor 196 | ma = device.GetReading() 197 | 198 | if ma != None: 199 | if ma.ReadingQualityStars >= 2: 200 | # If the reading quality is decent then compute the level 201 | sensor._tank_level = ((ma.TankLevelInMM - device.min_height) * 100.0) / (device.max_height - device.min_height) 202 | # round it to whole number 203 | sensor._tank_level = round(sensor._tank_level) 204 | # make sure it is greater than zero 205 | sensor._tank_level = max(sensor._tank_level, 0) 206 | # make sure it is less than 100 207 | sensor._tank_level = min(sensor._tank_level, 100) 208 | else: 209 | sensor._tank_level = 0 210 | # reading quality is bad. just mark as zero 211 | 212 | getattr(sensor, ATTR)["rssi"] = ma.rssi 213 | getattr(sensor, ATTR)["confidence_score"] = ma.ReadingQualityStars 214 | getattr(sensor, ATTR)["temp_c"] = ma.TemperatureInCelsius 215 | getattr(sensor, ATTR)["temp_f"] = ma.TemperatureInFahrenheit 216 | getattr(sensor, ATTR)[ATTR_BATTERY_LEVEL] = ma.BatteryPercent 217 | getattr(sensor, ATTR)["tank_level_mm"] = ma.TankLevelInMM 218 | sensor.async_schedule_update_ha_state() 219 | 220 | def update_ble_loop(now) -> None: 221 | """Lookup Bluetooth LE devices and update status.""" 222 | _LOGGER.debug("update_ble_loop called") 223 | 224 | try: 225 | # Time to make the dounuts 226 | update_ble_devices(config) 227 | except RuntimeError as error: 228 | _LOGGER.error("Error during Bluetooth LE scan: %s", error) 229 | 230 | time_offset = dt_util.utcnow() + timedelta(seconds=config[CONF_SCAN_INTERVAL]) 231 | # update_ble_loop() will be called again after time_offset 232 | track_point_in_utc_time(hass, update_ble_loop, time_offset) 233 | 234 | ########################################################################### 235 | 236 | HciIndex = int(config[CONF_HCI_DEVICE][-1]) 237 | service.SetHostControllerIndex(HciIndex) 238 | 239 | hass.bus.listen("homeassistant_stop", service.Stop) 240 | 241 | hass.services.register(DOMAIN, DISCOVERY_SERVICE_NAME, ReportMopekaDevicesWithButtonPressed) 242 | 243 | # Initialize configured Mopeka devices 244 | init_configured_devices() 245 | service.Start() 246 | # Begin sensor update loop 247 | update_ble_loop(dt_util.utcnow()) 248 | 249 | 250 | ############################################################################### 251 | 252 | # 253 | # HomeAssistant Tank Level Sensor 254 | # 255 | class TankLevelSensor(Entity): 256 | """Representation of a sensor.""" 257 | 258 | def __init__(self, mac: str, name: str): 259 | """Initialize the sensor.""" 260 | self._tank_level = None 261 | self._unique_id = "mopeka_" + mac.replace(":", "") 262 | self._name = name 263 | self._extra_state_attributes = {} 264 | 265 | @property 266 | def name(self) -> str: 267 | """Return the name of the sensor.""" 268 | return self._name 269 | 270 | @property 271 | def state(self): 272 | """Return the state of the sensor.""" 273 | return self._tank_level 274 | 275 | @property 276 | def unit_of_measurement(self) -> str: 277 | """Return the unit of measurement.""" 278 | return PERCENTAGE 279 | 280 | @property 281 | def device_class(self): 282 | """There is no Defined Device Class for this tank level""" 283 | return None 284 | 285 | @property 286 | def should_poll(self) -> bool: 287 | """No polling needed.""" 288 | return False 289 | 290 | @property 291 | def extra_state_attributes(self): 292 | """Return the state attributes.""" 293 | return self._extra_state_attributes 294 | 295 | @property 296 | def unique_id(self) -> str: 297 | """Return a unique ID.""" 298 | return self._unique_id 299 | 300 | @property 301 | def available(self) -> bool: 302 | """ is the sensor available """ 303 | return self._tank_level is not None 304 | 305 | @property 306 | def icon(self): 307 | return "mdi:propane-tank" 308 | --------------------------------------------------------------------------------