├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config-example.yaml ├── docker-compose.yaml ├── helper_common.py ├── helper_ha.py ├── images └── home_assistant.jpg ├── main.py ├── smt_reader.py └── version /.dockerignore: -------------------------------------------------------------------------------- 1 | logs/* 2 | -------------------------------------------------------------------------------- /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Ignore for PyCharm 107 | .idea 108 | 109 | # Ignore for erxsyslog 110 | logs/ 111 | 112 | # Ignore config.yaml 113 | config.yaml 114 | settings.json 115 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | 3 | RUN pip3 install requests PyYAML 4 | 5 | COPY ./*.py ./*.yaml /bin/ 6 | WORKDIR /bin 7 | 8 | CMD ["python", "-u", "./main.py"] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 scadaguru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pysmtreader (Python based SMT Reader): 2 | 3 | A Python application to poll your smart electric meter, this application uses an undocumented API for [Smart Meter Texas](https://www.smartmetertexas.com). 4 | 5 | **Huge thanks** to [u/keatontaylor for the API Details](https://github.com/keatontaylor/smartmetertexas-api), without his documentation none of this would have been possible. 6 | 7 | # What it does 8 | It polls [Smart Meter Texas](https://www.smartmetertexas.com) to get electric meter reading every hour (can be configured to read maximum twice per hour as restricted by SmartMeterTexas). Once read it will send read data to [Home Assistant](https://www.home-assistant.io/) 9 | 10 | # Update config.yaml file 11 | Update default config.yaml with your information mentioned below: 12 | Please update username, password, esiid, meter_number, base_url and access_token lines in the default config.yaml by removing "__REPLACE__" and providing your values otherwise application will not run! 13 | 14 | ~~~ 15 | logs: 16 | level: debug # debug, info(default), warning, error, critical 17 | log_file_name: pysmt # without extension, log extension will be added automatically 18 | 19 | health_check: 20 | log_info_line_at: 30 # in minutes, 0: disable 21 | 22 | smartmetertexas: # smartmetertexas.com 23 | base_url: https://smartmetertexas.com/api 24 | username: _REPLACE_ # Update with your username to access smartmetertexas.com 25 | password: _REPLACE_ # Update with your password to access smartmetertexas.com 26 | esiid: _REPLACE_ # Update with your ESSID, you can find from your electric bill or once you login to smartmetertexas.com 27 | meter_number: _REPLACE_ # Update with your Meter Number, you can find in your electric bill or once you login to smartmetertexas.com 28 | poll_interval_minutes: 60 # 0: disable, do set below 30 as smartmetertexas.com will not allow reading more than twice in an hour 29 | wait_interval_before_ondemand_read_minutes: 5 30 | force_first_read: False # if true it will attempt to read Smart Meter Texas, otherwise at poll_interval 31 | 32 | home_assistant: # Home Assistant access details 33 | base_url: _REPLACE_ # your Home Assistant URL/IP, no slash (/) at the end for example: http://192.168.1.149:8123 34 | access_token: _REPLACE_ # your Home Assistant access token 35 | ha_entity: sensor.smt_reading # home assistnat entity name to be created 36 | ~~~ 37 | 38 | # How to use 39 | Applicaton is available at Docker Hub and can be used in one of the two ways: 40 | 1. Docker 41 | 42 | Run the following command: 43 | ~~~ 44 | docker run -d -v ${PWD}/:/config scadaguru/pysmtreader 45 | ~~~ 46 | 47 | 2. Docker Compose 48 | 49 | ~~~ 50 | version: "3" 51 | services: 52 | pysmtreader: 53 | container_name: pysmtreader 54 | image: scadaguru/pysmtreader 55 | volumes: 56 | - ./:/config 57 | restart: unless-stopped 58 | ~~~ 59 | 60 | # Data Sent to Home Assistant 61 | ~~~ 62 | current_state: Latest reading value 63 | prev_state: Previous reading value 64 | difference: This is computed difference from latest and previoud reading, so if you polling once an hour it is hourly usage 65 | unit_of_measurement: KW 66 | odrusage: This is provided by smartmetertexas.com and most likely it is total since last day 67 | last_timestamp: Last read time in the format of 2020-05-27 14:30:01.674 68 | ~~~ 69 | 70 | ![Home Assistant](images/home_assistant.jpg) 71 | -------------------------------------------------------------------------------- /config-example.yaml: -------------------------------------------------------------------------------- 1 | logs: 2 | level: debug # debug, info(default), warning, error, critical 3 | log_file_name: pysmt # without extension, log extension will be added automatically 4 | 5 | health_check: 6 | log_info_line_at: 30 # in minutes, 0: disable 7 | 8 | smartmetertexas: # smartmetertexas.com 9 | base_url: https://smartmetertexas.com/api 10 | username: _REPLACE_ # Update with your username to access smartmetertexas.com 11 | password: _REPLACE_ # Update with your password to access smartmetertexas.com 12 | esiid: _REPLACE_ # Update with your ESSID, you can find from your electric bill or once you login to smartmetertexas.com 13 | meter_number: _REPLACE_ # Update with your Meter Number, you can find in your electric bill or once you login to smartmetertexas.com 14 | poll_interval_minutes: 60 # 0: disable, do set below 30 as smartmetertexas.com will not allow reading more than twice in an hour 15 | wait_interval_before_ondemand_read_minutes: 5 16 | force_first_read: False # if true it will attempt to read Smart Meter Texas, otherwise at poll_interval 17 | 18 | home_assistant: # Home Assistant access details 19 | base_url: _REPLACE_ # your Home Assistant URL/IP, no slash (/) at the end for example: http://192.168.1.2:8123 20 | access_token: _REPLACE_ # your Home Assistant access token 21 | ha_entity: sensor.smt_reading # home assistnat entity name to be created 22 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | pysmtreader: 4 | build: 5 | context: . 6 | dockerfile: ./dockerfile 7 | image: pysmtreader:0.1 8 | container_name: pysmtreader 9 | volumes: 10 | - ./:/config 11 | - /etc/localtime:/etc/localtime:ro 12 | restart: unless-stopped 13 | -------------------------------------------------------------------------------- /helper_common.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import traceback 4 | import yaml 5 | 6 | 7 | class CommonHelper: 8 | log_level_debug = 1 9 | log_level_info = 2 10 | log_level_warning = 3 11 | log_level_error = 4 12 | log_level_critical = 5 13 | 14 | def __init__(self, config_folder): 15 | self.config_folder = config_folder 16 | self.config = yaml.safe_load(open(self.config_folder + 'config.yaml')) 17 | 18 | self.log_level = 2 19 | if self.config["logs"]["level"] == "debug": 20 | self.log_level = 1 21 | elif self.config["logs"]["level"] == "info": 22 | self.log_level = 2 23 | elif self.config["logs"]["level"] == "warning": 24 | self.log_level = 3 25 | elif self.config["logs"]["level"] == "error": 26 | self.log_level = 4 27 | elif self.config["logs"]["level"] == "critical": 28 | self.log_level = 5 29 | 30 | self.log_folder = self.config_folder + "logs/" 31 | self.log_file_name = self.log_folder + \ 32 | self.config["logs"]["log_file_name"] 33 | 34 | if not os.path.exists(self.log_folder): 35 | os.makedirs(self.config_folder + "logs") 36 | self.log_info( 37 | "CommonHelper:__init__(): Creating log folder: " + self.log_folder) 38 | 39 | def get_seconds_till_next_minute(self): 40 | cur_time = datetime.datetime.now() 41 | next_minute = cur_time + \ 42 | datetime.timedelta(seconds=59 - cur_time.second, 43 | microseconds=999999 - cur_time.microsecond) 44 | diff = next_minute - cur_time 45 | diff_ms = (diff.seconds * 1000000 + diff.microseconds) / 1000000.0 46 | return diff_ms 47 | 48 | def log_debug(self, str_print): 49 | self._log(self.log_level_debug, str_print) 50 | 51 | def log_info(self, str_print): 52 | self._log(self.log_level_info, str_print) 53 | 54 | def log_warning(self, str_print): 55 | self._log(self.log_level_warning, str_print) 56 | 57 | def log_error(self, str_print): 58 | self._log(self.log_level_error, str_print) 59 | 60 | def log_critical(self, str_print): 61 | self._log(self.log_level_critical, str_print) 62 | 63 | def log_data(self, str_print): 64 | self._log(self.log_level_critical, str_print) 65 | 66 | def _log(self, log_level, str_print): 67 | if log_level >= self.log_level: 68 | try: 69 | log_file_name = self.log_file_name + "-" + \ 70 | datetime.datetime.now().strftime('%Y-%m-%d') + ".log" 71 | log_str = datetime.datetime.now().strftime('%H:%M:%S.%f')[ 72 | :-3] + self._get_log_level_to_string(log_level) + str_print 73 | print(log_str) 74 | with open(log_file_name, "a") as log_file: 75 | log_file.write(log_str + "\n") 76 | except Exception as e: 77 | print(datetime.datetime.now().strftime('%H:%M:%S.%f')[ 78 | :-3] + " : CommonHelper:log():Exception: " + str(e)) 79 | 80 | def _get_log_level_to_string(self, log_level): 81 | log_level_str = ": " 82 | if log_level == self.log_level_debug: 83 | log_level_str = ":debug: " 84 | elif log_level == self.log_level_info: 85 | log_level_str = ":info: " 86 | elif log_level == self.log_level_warning: 87 | log_level_str = ":warn: " 88 | elif log_level == self.log_level_error: 89 | log_level_str = ":error: " 90 | elif log_level == self.log_level_critical: 91 | log_level_str = ":critical: " 92 | return log_level_str 93 | -------------------------------------------------------------------------------- /helper_ha.py: -------------------------------------------------------------------------------- 1 | import json 2 | from requests import post, get 3 | import yaml 4 | import traceback 5 | 6 | 7 | class CustomHAHelper: 8 | def __init__(self, config_folder): 9 | try: 10 | self.config = yaml.safe_load(open(config_folder + 'config.yaml')) 11 | self.base_url = self.config["home_assistant"]["base_url"] 12 | self.access_token = self.config["home_assistant"]["access_token"] 13 | except Exception as e: 14 | exception_info = "\nException: {}\n Call Stack: {}".format( 15 | str(e), str(traceback.format_exc())) 16 | print("CustomHAHelper:__init__():Exception: " + exception_info) 17 | 18 | def ha_get_entity_state(self, entity_name): 19 | state = None 20 | response = self.ha_get_sensor(entity_name) 21 | if response: 22 | state = response.json()['state'] 23 | return state 24 | 25 | def ha_get_entity_attribute(self, entity_name, attribute_name): 26 | attribute_value = None 27 | response = self.ha_get_sensor(entity_name) 28 | if response: 29 | attribute_value = response.json()['attributes'][attribute_name] 30 | return attribute_value 31 | 32 | def ha_set_entity_state(self, entity_name, state_str=None, attributes=None, payload=None): 33 | if payload == None: 34 | payload = {"state": state_str} 35 | if attributes: 36 | payload['attributes'] = attributes 37 | return self.ha_update_sensor(entity_name, payload) 38 | 39 | def ha_get_sensor(self, entity_name): 40 | get_url = "{}/api/states/{}".format(self.base_url, entity_name) 41 | headers = { 42 | 'Authorization': "Bearer " + self.access_token 43 | } 44 | response = get(get_url, headers=headers) 45 | return response 46 | 47 | def ha_update_sensor(self, entity_name, payload): 48 | post_url = "{}/api/states/{}".format(self.base_url, entity_name) 49 | headers = { 50 | 'Authorization': "Bearer " + self.access_token 51 | } 52 | response = post(post_url, data=json.dumps(payload), headers=headers) 53 | return response 54 | 55 | def ha_service_notify(self, message, whom): 56 | post_url = "{}/api/services/notify/{}".format(self.base_url, whom) 57 | headers = { 58 | 'Authorization': "Bearer " + self.access_token 59 | } 60 | payload = { 61 | "message": message 62 | } 63 | response = post(post_url, data=json.dumps(payload), headers=headers) 64 | return response 65 | 66 | def ha_service_update_device_tracker(self, mac_address=None, status_str=None, payload=None): 67 | post_url = "{}/api/services/device_tracker/see".format(self.base_url) 68 | headers = { 69 | 'Authorization': "Bearer " + self.access_token 70 | } 71 | if payload == None: 72 | payload = { 73 | "mac": mac_address, 74 | "location_name": status_str, 75 | "attributes": {"source_type": "script"} 76 | } 77 | response = post(post_url, data=json.dumps(payload), headers=headers) 78 | return response 79 | -------------------------------------------------------------------------------- /images/home_assistant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scadaguru/pysmtreader/0cb32a9779cd3e22252c677ebdcd25590339ecaa/images/home_assistant.jpg -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | import json 4 | import traceback 5 | from smt_reader import SMTReader 6 | from helper_common import CommonHelper 7 | from helper_ha import CustomHAHelper 8 | 9 | 10 | class MeterReadHelper: 11 | 12 | def __init__(self, host_mapped_folder): 13 | self.__commonHelper = CommonHelper(host_mapped_folder) 14 | self.__customHAHelper = CustomHAHelper(host_mapped_folder) 15 | 16 | self.__log_info_line_at = self.__commonHelper.config["health_check"]["log_info_line_at"] 17 | self.__smt_poll_interval = int( 18 | self.__commonHelper.config["smartmetertexas"]["poll_interval_minutes"]) 19 | self.__smt_force_first_read = self.__commonHelper.config[ 20 | "smartmetertexas"]["force_first_read"] 21 | self.__smtreader = SMTReader(self.__commonHelper) 22 | self.__commonHelper.log_info("SMTReader meter needs to be polled every {} minutes with force first is {}".format( 23 | self.__smt_poll_interval, self.__smt_force_first_read)) 24 | self.__ha_entity = self.__commonHelper.config["home_assistant"]["ha_entity"] 25 | 26 | def validate_config(self): 27 | config_status = 0 28 | value = self.__commonHelper.config["smartmetertexas"]["username"] 29 | if value == "_REPLACE_" or value == "": 30 | config_status = 1 31 | self.__commonHelper.log_error( 32 | "Config error: please make sure smartmetertexas username {} is valid".format(value)) 33 | value = self.__commonHelper.config["smartmetertexas"]["password"] 34 | if value == "_REPLACE_" or value == "": 35 | config_status = 1 36 | self.__commonHelper.log_error( 37 | "Config error: please make sure smartmetertexas password {} is valid".format(value)) 38 | value = self.__commonHelper.config["smartmetertexas"]["esiid"] 39 | if value == "_REPLACE_" or value == "": 40 | config_status = 1 41 | self.__commonHelper.log_error( 42 | "Config error: please make sure smartmetertexas esiid {} is valid".format(value)) 43 | value = self.__commonHelper.config["smartmetertexas"]["meter_number"] 44 | if value == "_REPLACE_" or value == "": 45 | config_status = 1 46 | self.__commonHelper.log_error( 47 | "Config error: please make sure smartmetertexas meter_number {} is valid".format(value)) 48 | value = self.__commonHelper.config["home_assistant"]["base_url"] 49 | if value == "_REPLACE_" or value == "": 50 | config_status = 1 51 | self.__commonHelper.log_error( 52 | "Config error: please make sure home_assistant base_url {} is valid".format(value)) 53 | value = self.__commonHelper.config["home_assistant"]["access_token"] 54 | if value == "_REPLACE_" or value == "": 55 | config_status = 1 56 | self.__commonHelper.log_error( 57 | "Config error: please make sure home_assistant access_token {} is valid".format(value)) 58 | return config_status 59 | 60 | def start(self): 61 | if self.validate_config() == 0: 62 | if self.__smt_force_first_read: 63 | self.__read_smt_meter() 64 | 65 | self.__commonHelper.log_info( 66 | "Going to sleep until exact minute starts") 67 | time.sleep(self.__commonHelper.get_seconds_till_next_minute()) 68 | 69 | self.__commonHelper.log_info( 70 | "Now doing regular polling from config file interval") 71 | while True: 72 | cur_time = datetime.datetime.now() 73 | minutes_since_day_start = cur_time.hour * 60 + cur_time.minute 74 | 75 | if self.__log_info_line_at != 0 and minutes_since_day_start % self.__log_info_line_at == 0: 76 | self.__commonHelper.log_info( 77 | "Health check info line, still active and working!") 78 | 79 | if self.__smt_poll_interval != 0 and minutes_since_day_start % self.__smt_poll_interval == 0: 80 | self.__read_smt_meter() 81 | 82 | time.sleep(self.__commonHelper.get_seconds_till_next_minute()) 83 | 84 | def __read_smt_meter(self): 85 | try: 86 | status_code_read, meter_reading, odrusage = self.__smtreader.read_meter() 87 | if status_code_read == 0: 88 | self.__update_hass(meter_reading, odrusage) 89 | self.__commonHelper.log_info("SMT reading: " + meter_reading) 90 | except Exception as e: 91 | error_msg = "__read_smt_meter(): Exception: {}\n Call Stack: {}".format( 92 | str(e), traceback.format_exc()) 93 | self.__commonHelper.log_critical(error_msg) 94 | 95 | def __update_hass(self, meter_reading, odrusage): 96 | self.__commonHelper.log_debug("Updating homeAssistant") 97 | response = self.__customHAHelper.ha_get_sensor(self.__ha_entity) 98 | if response.text.find("attributes") != -1 and response.text.find("current_state") != -1: 99 | prev_reading = response.json()['attributes']['current_state'] 100 | self.__commonHelper.log_debug( 101 | "Found previous reading on homeAssistant: {}".format(str(prev_reading))) 102 | else: 103 | prev_reading = meter_reading 104 | 105 | attributes = dict() 106 | attributes['unit_of_measurement'] = "kWh" 107 | attributes['device_class'] = "energy" 108 | attributes['state_class'] = "total_increasing" 109 | attributes['prev_state'] = prev_reading 110 | attributes['current_state'] = meter_reading 111 | attributes['odrusage'] = odrusage 112 | attributes['last_timestamp'] = str( 113 | datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]) 114 | attributes['last_reset'] = attributes['last_timestamp'] 115 | try: 116 | attributes['difference'] = str( 117 | round(float(meter_reading) - float(prev_reading), 3)) 118 | except Exception as e: 119 | error_msg = "update_hass(): Exception: {}\n Call Stack: {}".format( 120 | str(e), traceback.format_exc()) 121 | self.__commonHelper.log_critical(error_msg) 122 | 123 | payload = {"state": meter_reading, "attributes": attributes} 124 | self.__commonHelper.log_debug( 125 | "Updating homeAssistant: payload: {}".format(str(payload))) 126 | response = self.__customHAHelper.ha_update_sensor( 127 | self.__ha_entity, payload) 128 | self.__commonHelper.log_debug(str(response.text)) 129 | 130 | 131 | # This is mapped volume from docker compose file, under this folder everything will be persisted on host 132 | host_mapped_folder = "/config/" 133 | #host_mapped_folder = "./" 134 | meter_read_helper = MeterReadHelper(host_mapped_folder) 135 | meter_read_helper.start() 136 | -------------------------------------------------------------------------------- /smt_reader.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import time 4 | import datetime 5 | import traceback 6 | 7 | 8 | class SMTReader: 9 | 10 | def __init__(self, commonHelper): 11 | self.__commonHelper = commonHelper 12 | 13 | self.__base_url = self.__commonHelper.config["smartmetertexas"]["base_url"] 14 | self.__username = self.__commonHelper.config["smartmetertexas"]["username"] 15 | self.__password = self.__commonHelper.config["smartmetertexas"]["password"] 16 | self.__esiid = self.__commonHelper.config["smartmetertexas"]["esiid"] 17 | self.__meter_number = self.__commonHelper.config["smartmetertexas"]["meter_number"] 18 | self.__wait_interval = self.__commonHelper.config["smartmetertexas"][ 19 | "wait_interval_before_ondemand_read_minutes"] 20 | 21 | def read_meter(self): 22 | status_code_read = -1 23 | meter_reading = "" 24 | odrusage = "" 25 | 26 | self.__commonHelper.log_debug("About to read SMT meter") 27 | status_code_auth, auth_token = self.__get_auth_token() 28 | if status_code_auth == 0: 29 | status_code_ondemand = self.__request_ondemand_read(auth_token) 30 | 31 | if status_code_ondemand == "0": 32 | self.__commonHelper.log_debug( 33 | "Starting timer for {} minutes".format(str(self.__wait_interval))) 34 | time.sleep(self.__wait_interval * 60) 35 | status_code_read, meter_reading, odrusage = self.__process_read_request( 36 | auth_token) 37 | 38 | # if an error occured when getting the reading, wait another 5 minutes and then try to get the reading 39 | if status_code_read == 1: 40 | self.__commonHelper.log_error( 41 | "Still pending, starting another timer for {} minutes".format(str(self.__wait_interval))) 42 | time.sleep(self.__wait_interval * 60) 43 | status_code_read, meter_reading, odrusage = self.__process_read_request( 44 | auth_token) 45 | elif status_code_ondemand == "5031": # 5031 represents too many requests in an hour 46 | self.__commonHelper.log_error( 47 | "Looks like too many requests have been sent, can't get the reading this hour") 48 | else: # some other error occured calling the api 49 | self.__commonHelper.log_error( 50 | "There was a problem calling the rest api") 51 | return status_code_read, meter_reading, odrusage 52 | 53 | # Will return 0 if a request was successfully 54 | # Will return -1 if there was an error making a request to the server 55 | def __get_auth_token(self): 56 | self.__commonHelper.log_debug('Getting auth token') 57 | status_code = -1 58 | auth_token = "" 59 | 60 | session = requests.Session() 61 | payload = {'username': self.__username, 62 | 'password': self.__password, 'rememberMe': True} 63 | response = session.post(self.__base_url + '/user/authenticate', 64 | data=payload, verify=False) 65 | self.__commonHelper.log_debug( 66 | "Authorization request response: {}".format(str(response.text))) 67 | if response.ok: 68 | json_data = json.loads(response.text) 69 | auth_token = json_data['token'] 70 | status_code = 0 71 | self.__commonHelper.log_info("Authorization successful") 72 | else: 73 | self.__commonHelper.log_error( 74 | "Authorization request failed, response: {}".format(str(response.text))) 75 | return status_code, auth_token 76 | 77 | # Will return 0 if a request was successfully sent and currently pending 78 | # Will return 5031 if the request is not allowed due to the fact it is too soon 79 | # Will return -1 if there was an error making a request to the server 80 | def __request_ondemand_read(self, auth_token): 81 | self.__commonHelper.log_debug('Requesting ondemand reading') 82 | status_code = -1 83 | 84 | header = {'Authorization': 'Bearer ' + auth_token} 85 | payload = {'ESIID': self.__esiid, 'MeterNumber': self.__meter_number} 86 | 87 | session = requests.Session() 88 | response = session.post(self.__base_url + '/ondemandread', data=payload, 89 | headers=header, verify=False) 90 | self.__commonHelper.log_debug( 91 | "Ondemand request response: {}".format(str(response.text))) 92 | if response.ok: 93 | json_data = json.loads(response.text) 94 | status_code = json_data['data']['statusCode'] 95 | self.__commonHelper.log_info("Ondemand request sent successfully") 96 | else: 97 | self.__commonHelper.log_error( 98 | "Ondemand request send failed, response: {}".format(str(response.text))) 99 | return status_code 100 | 101 | # Will return 0 if a request was successfully sent and updated on home assistant 102 | # Will return 1 if the request is still not ready, will try again in 5 minutes 103 | # Will return -1 if there was an error making a request to the server 104 | def __process_read_request(self, auth_token): 105 | self.__commonHelper.log_debug("Starting read of the processed data") 106 | status_code = -1 107 | meter_reading = "" 108 | odrusage = "" 109 | 110 | header = {'Authorization': 'Bearer ' + auth_token} 111 | payload = {'ESIID': self.__esiid} 112 | 113 | session = requests.Session() 114 | response = session.post(self.__base_url + '/usage/latestodrread', data=payload, 115 | headers=header, verify=False) 116 | self.__commonHelper.log_debug( 117 | "Read request response: {}".format(str(response.text))) 118 | if response.ok: 119 | json_data = json.loads(response.text) 120 | status_string = json_data['data']['odrstatus'] 121 | 122 | if status_string == 'COMPLETED': 123 | self.__commonHelper.log_info("Read request successful") 124 | meter_reading = json_data['data']['odrread'] 125 | odrusage = json_data['data']['odrusage'] 126 | status_code = 0 127 | elif status_string == 'PENDING': 128 | self.__commonHelper.log_info("Read request is still pending") 129 | status_code = 1 130 | else: 131 | self.__commonHelper.log_error( 132 | "Read request failed: {}".format(str(response.text))) 133 | status_code = 1 134 | return status_code, meter_reading, odrusage 135 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 0.1 --------------------------------------------------------------------------------