pax_global_header00006660000000000000000000000064147541264250014524gustar00rootroot0000000000000052 comment=01f7fcf6e950acebb5d8387421f283a92bfdca35 pyeconet-0.1.28/000077500000000000000000000000001475412642500134425ustar00rootroot00000000000000pyeconet-0.1.28/.github/000077500000000000000000000000001475412642500150025ustar00rootroot00000000000000pyeconet-0.1.28/.github/CODE_OF_CONDUCT.md000066400000000000000000000063611475412642500176070ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [safety@home-assistant.io][email]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available [here][version]. ## Adoption This Code of Conduct was first adopted December 15th, 2021. [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ [email]: mailto:dahl.brendan@gmail.com pyeconet-0.1.28/.github/ISSUE_TEMPLATE/000077500000000000000000000000001475412642500171655ustar00rootroot00000000000000pyeconet-0.1.28/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000010321475412642500216530ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: 'Bug' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. pyeconet-0.1.28/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011231475412642500227070ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. pyeconet-0.1.28/.github/pre-commit-config.yaml000066400000000000000000000017421475412642500212110ustar00rootroot00000000000000repos: - repo: https://github.com/asottile/pyupgrade rev: v3.2.0 hooks: - id: pyupgrade stages: [manual] args: - "--py38-plus" - repo: https://github.com/psf/black rev: 22.10.0 hooks: - id: black stages: [manual] args: - --safe files: ^((custom_components|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell rev: v2.2.2 hooks: - id: codespell stages: [manual] args: - --quiet-level=2 - --ignore-words-list=hass,ba,fo - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: - id: check-executables-have-shebangs stages: [manual] - id: check-json stages: [manual] - id: requirements-txt-fixer stages: [manual] - id: check-ast stages: [manual] - id: mixed-line-ending stages: [manual] args: - --fix=lf pyeconet-0.1.28/.github/workflows/000077500000000000000000000000001475412642500170375ustar00rootroot00000000000000pyeconet-0.1.28/.github/workflows/lint.yaml000066400000000000000000000025001475412642500206660ustar00rootroot00000000000000name: Lint on: pull_request_target: branches: - master - externalAPI jobs: matrix: runs-on: ubuntu-latest name: Run ${{ matrix.checks }} strategy: matrix: checks: - pyupgrade - black - codespell - check-executables-have-shebangs - check-json - requirements-txt-fixer - check-ast - mixed-line-ending steps: - name: Check out repository uses: actions/checkout@v3 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Python uses: actions/setup-python@v4 id: python with: python-version: "3.x" - name: Install pre-commit run: | python3 -m pip install pre-commit pre-commit install-hooks --config .github/pre-commit-config.yaml - name: Run the check (${{ matrix.checks }}) run: pre-commit run --hook-stage manual ${{ matrix.checks }} --all-files --config .github/pre-commit-config.yaml - name: Commit Changes if: failure() uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: "fix: ${{ matrix.checks }} action" pyeconet-0.1.28/.github/workflows/lintPR.yaml000066400000000000000000000006171475412642500211370ustar00rootroot00000000000000name: "Lint PR" on: pull_request_target: types: - opened - edited - synchronize jobs: main: runs-on: ubuntu-latest steps: # Please look up the latest version from # https://github.com/amannn/action-semantic-pull-request/releases - uses: amannn/action-semantic-pull-request@v5.0.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} pyeconet-0.1.28/.github/workflows/release.yml000066400000000000000000000044241475412642500212060ustar00rootroot00000000000000name: Release on: push: branches: - master jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Gets semantic release info id: semantic_release_info uses: jossef/action-semantic-release-info@v1 env: GITHUB_TOKEN: ${{ github.token }} - name: Update Version and Commit if: ${{steps.semantic_release_info.outputs.version != ''}} run: | echo "Version: ${{steps.semantic_release_info.outputs.version}}" sed -i "s/VERSION = \".*\"/VERSION = \"${{steps.semantic_release_info.outputs.version}}\"/g" setup.py git config --local user.email "6432770+w1ll1am23@users.noreply.github.com" git config --local user.name "William Scanlon" git add -A git commit -m "chore: bumping version to ${{steps.semantic_release_info.outputs.version}}" git tag ${{ steps.semantic_release_info.outputs.git_tag }} - name: Push changes if: ${{steps.semantic_release_info.outputs.version != ''}} uses: ad-m/github-push-action@v0.6.0 with: github_token: ${{ github.token }} tags: true - name: Create GitHub Release if: ${{steps.semantic_release_info.outputs.version != ''}} uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ github.token }} with: tag_name: ${{ steps.semantic_release_info.outputs.git_tag }} release_name: ${{ steps.semantic_release_info.outputs.git_tag }} body: ${{ steps.semantic_release_info.outputs.notes }} draft: false prerelease: false - name: Set up Python 3.9 uses: actions/setup-python@v1 with: python-version: 3.9 - name: Install pypa/build run: >- python -m pip install build --user - name: Build a binary wheel and a source tarball run: >- python -m build --sdist --wheel --outdir dist/ . - name: Publish distribution 📦 to PyPI if: ${{steps.semantic_release_info.outputs.version != ''}} uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }} pyeconet-0.1.28/.gitignore000066400000000000000000000020431475412642500154310ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # IDE .idea/ pyeconet-0.1.28/LICENSE000066400000000000000000000020601475412642500144450ustar00rootroot00000000000000MIT License Copyright (c) 2016 William Scanlon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyeconet-0.1.28/MANIFEST.in000066400000000000000000000000671475412642500152030ustar00rootroot00000000000000include CHANGELOG.md include LICENSE include README.md pyeconet-0.1.28/README.md000066400000000000000000000034201475412642500147200ustar00rootroot00000000000000# pyeconet Python3 interface to the unofficial EcoNet API. > [!NOTE] > I no longer have a device connect to the Rheem cloud due to migrating to a fully local control option > https://github.com/esphome-econet/esphome-econet > If anyone is interested in taking over ownership for this project please open an issue to discuss. > [!NOTE] > This isn't using an official EcoNet API therefore this library could stop working at any time, without warning. ```python import asyncio import logging import time import getpass from pyeconet import EcoNetApiInterface from pyeconet.equipment import EquipmentType from pyeconet.equipment.water_heater import WaterHeaterOperationMode logging.basicConfig() logging.getLogger().setLevel(logging.DEBUG) async def main(): email = input("Enter your email: ").strip() password = "" #getpass.getpass(prompt='Enter your password: ') api = await EcoNetApiInterface.login(email, password=password) all_equipment = await api.get_equipment_by_type([EquipmentType.WATER_HEATER, EquipmentType.THERMOSTAT]) #api.subscribe() #await asyncio.sleep(5) for equip_list in all_equipment.values(): for equipment in equip_list: print(f"Name: {equipment.device_name}") # print(f"Set point: {equipment.set_point}") # print(f"Supports modes: {equipment._supports_modes()}") # print(f"Operation modes: {equipment.modes}") # print(f"Operation mode: {equipment.mode}") #await equipment._get_energy_usage() #equipment.set_set_point(equipment.set_point + 1) #equipment.set_mode(OperationMode.ELECTRIC_MODE) #await asyncio.sleep(300000) #api.unsubscribe() if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) ```pyeconet-0.1.28/pylintrc000066400000000000000000000003431475412642500152310ustar00rootroot00000000000000[MASTER] max-line-length=120 max-args=10 ignore=test # Reasons disabled: # locally-disabled - Because that's the whole point! disable= missing-docstring , global-statement , invalid-name , fixme , locally-disabled pyeconet-0.1.28/pyproject.toml000066400000000000000000000001501475412642500163520ustar00rootroot00000000000000[build-system] requires = [ "setuptools>=42", "wheel" ] build-backend = "setuptools.build_meta" pyeconet-0.1.28/setup.cfg000066400000000000000000000000501475412642500152560ustar00rootroot00000000000000[metadata] description-file = README.md pyeconet-0.1.28/setup.py000066400000000000000000000020361475412642500151550ustar00rootroot00000000000000from setuptools import setup, find_packages import pathlib here = pathlib.Path(__file__).parent.resolve() # Get the long description from the README file LONG_DESCRIPTION = (here / "README.md").read_text(encoding="utf-8") VERSION = "0.1.28" # Setting up setup( name="pyeconet", version=VERSION, author="William Scanlon", description="Interface to the unofficial EcoNet API", long_description=LONG_DESCRIPTION, long_description_content_type="text/markdown", package_dir={"": "src"}, packages=find_packages( where="src", exclude=["dist", "*.test", "*.test.*", "test.*", "test"] ), install_requires=["aiohttp>=3.11.11, <4", "paho-mqtt>=2.1.0, <3"], keywords=["econet", "rheem", "api"], python_requires=">=3.8, <4", url="https://github.com/w1ll1am23/pyeconet", project_urls={ "Bug Reports": "https://github.com/w1ll1am23/pyeconet/issues", "Source": "https://github.com/w1ll1am23/pyeconet", }, classifiers=[ "License :: OSI Approved :: MIT License", ], ) pyeconet-0.1.28/src/000077500000000000000000000000001475412642500142315ustar00rootroot00000000000000pyeconet-0.1.28/src/pyeconet/000077500000000000000000000000001475412642500160575ustar00rootroot00000000000000pyeconet-0.1.28/src/pyeconet/__init__.py000066400000000000000000000001631475412642500201700ustar00rootroot00000000000000"""Define module-level imports""" from .api import EcoNetApiInterface # noqa from .equipment import EquipmentType pyeconet-0.1.28/src/pyeconet/api.py000066400000000000000000000310461475412642500172060ustar00rootroot00000000000000from datetime import datetime import time import ssl import json from typing import Type, TypeVar, List, Dict, Optional import logging from pyeconet.errors import ( PyeconetError, InvalidCredentialsError, GenericHTTPError, InvalidResponseFormat, ) from pyeconet.equipment import Equipment, EquipmentType from pyeconet.equipment.water_heater import WaterHeater from pyeconet.equipment.thermostat import Thermostat from aiohttp import ClientSession, ClientTimeout from aiohttp.client_exceptions import ClientError import paho.mqtt.client as mqtt HOST = "rheem.clearblade.com" REST_URL = f"https://{HOST}/api/v/1" CLEAR_BLADE_SYSTEM_KEY = "e2e699cb0bb0bbb88fc8858cb5a401" CLEAR_BLADE_SYSTEM_SECRET = "E2E699CB0BE6C6FADDB1B0BC9A20" HEADERS = { "ClearBlade-SystemKey": CLEAR_BLADE_SYSTEM_KEY, "ClearBlade-SystemSecret": CLEAR_BLADE_SYSTEM_SECRET, "Content-Type": "application/json; charset=UTF-8", } _LOGGER = logging.getLogger(__name__) ApiType = TypeVar("ApiType", bound="EcoNetApiInterface") def _create_ssl_context() -> ssl.SSLContext: """Create a SSL context for the MQTT connection.""" context = ssl.SSLContext(ssl.PROTOCOL_TLS) context.load_default_certs() return context _SSL_CONTEXT = _create_ssl_context() class EcoNetApiInterface: """ API interface object. """ def __init__( self, email: str, password: str, account_id: str = None, user_token: str = None ) -> None: """ Create the EcoNet API interface object. Args: email (str): EcoNet account email address. password (str): EcoNet account password. """ self.email: str = email self.password: str = password self._user_token: str = user_token self._account_id: str = account_id self._locations: List = [] self._equipment: Dict = {} self._mqtt_client = None @property def user_token(self) -> str: """Return the current user token""" return self._user_token @property def account_id(self) -> str: """Return the current user token""" return self._account_id @classmethod async def login(cls: Type[ApiType], email: str, password: str) -> ApiType: """Create an EcoNetApiInterface object using email and password Args: email (str): EcoNet account email address. password (str): EcoNet account password. """ this_class = cls(email, password) await this_class._authenticate({"email": email, "password": password}) return this_class def check_mode_enum(self, equip, enumtext=False): # Fix enumeration of Emergency Heat in Thermostat, maybe others? if "@MODE" in equip and isinstance(equip["@MODE"], Dict): if 'constraints' in equip["@MODE"]: enumtext = equip["@MODE"]['constraints']['enumText'] if enumtext: value = equip["@MODE"]['value'] status = equip["@MODE"]['status'] if (value != enumtext.index(status)): _LOGGER.debug("Enum value mismatch: " f"{enumtext[value]} != " f"{equip["@MODE"]['status']}") equip["@MODE"]['value'] = enumtext.index(status) return equip, enumtext def check_update_enum(self, equip, update): # Update messages only have the status and value, so get the enumtext equip, enumtext = self.check_mode_enum(equip) # Fix the update update, __ = self.check_mode_enum(update, enumtext) return equip, update def subscribe(self): """Subscribe to the MQTT updates""" if not self._equipment: _LOGGER.error( "Equipment list is empty, did you call get_equipment before subscribing?" ) return False self._mqtt_client = mqtt.Client( callback_api_version=mqtt.CallbackAPIVersion.VERSION1, client_id=self._get_client_id(), clean_session=True, userdata=None, protocol=mqtt.MQTTv311, ) self._mqtt_client.username_pw_set( self._user_token, password=CLEAR_BLADE_SYSTEM_KEY ) self._mqtt_client.enable_logger() self._mqtt_client.tls_set_context(_SSL_CONTEXT) self._mqtt_client.tls_insecure_set(False) self._mqtt_client.on_connect = self._on_connect self._mqtt_client.on_message = self._on_message self._mqtt_client.on_disconnect = self._on_disconnect self._mqtt_client.connect_async(HOST, 1884, 60) self._mqtt_client.loop_start() def publish(self, payload: Dict, device_id: str, serial_number: str): """Publish payload to the specified topic""" date_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") transaction_id = f"ANDROID_{date_time}" publish_payload = { "transactionId": transaction_id, "device_name": device_id, "serial_number": serial_number, } publish_payload.update(payload) self._mqtt_client.publish( f"user/{self._account_id}/device/desired", payload=json.dumps(publish_payload), ) def unsubscribe(self) -> None: self._mqtt_client.loop_stop() def _get_client_id(self) -> str: time_string = str(time.time()).replace(".", "")[:13] return f"{self.email}{time_string}_android" async def _get_equipment(self) -> None: """Get a list of all the equipment for this user""" _locations: List = await self._get_location() for _location in _locations: # They spelled it wrong... for _equip in _location.get("equiptments"): # Early exit if server returned error code if "error" in _equip: _LOGGER.debug("EcoNet equipment error message" f": {_equip.get('error')}") continue _equip, __ = self.check_mode_enum(_equip) _equip_obj: Equipment = None if ( Equipment._coerce_type_from_string(_equip.get("device_type")) == EquipmentType.WATER_HEATER ): _equip_obj = WaterHeater(_equip, self) self._equipment[_equip_obj.serial_number] = _equip_obj elif ( Equipment._coerce_type_from_string(_equip.get("device_type")) == EquipmentType.THERMOSTAT ): _equip_obj = Thermostat(_equip, self) self._equipment[_equip_obj.serial_number] = _equip_obj for zoning_device in _equip.get("zoning_devices", []): _equip_obj = Thermostat(zoning_device, self) self._equipment[_equip_obj.serial_number] = _equip_obj async def refresh_equipment(self) -> None: """Get a list of all the equipment for this user""" _locations: List = await self._get_location() for _location in _locations: # They spelled it wrong... for _equip in _location.get("equiptments"): serial = _equip.get("serial_number") equipment = self._equipment.get(serial) if equipment: _equip, __ = self.check_mode_enum(_equip) equipment.update_equipment_info(_equip) async def get_equipment_by_type(self, equipment_type: List) -> Dict: """Get a list of equipment by the equipment EquipmentType""" if not self._equipment: await self._get_equipment() _equipment = {} for _equip_type in equipment_type: _equipment[_equip_type] = [] for value in self._equipment.values(): if value.type in equipment_type: _equipment[value.type].append(value) return _equipment async def _get_location(self) -> List[Dict]: _headers = HEADERS.copy() _headers["ClearBlade-UserToken"] = self._user_token payload = {"location_only": False, "type": "com.econet.econetconsumerandroid", "version": "6.0.0-375-01b4870e"} _session = ClientSession() try: async with _session.post( f"{REST_URL}/code/{CLEAR_BLADE_SYSTEM_KEY}/getUserDataForApp", json=payload, headers=_headers ) as resp: if resp.status == 200: _json = await resp.json() _LOGGER.debug(json.dumps(_json, indent=2)) if _json.get("success"): self._locations = _json["results"]["locations"] return self._locations else: raise InvalidResponseFormat() else: raise GenericHTTPError(resp.status) except ClientError as err: raise err finally: await _session.close() async def get_dynamic_action(self, payload: Dict) -> Dict: _headers = HEADERS.copy() _headers["ClearBlade-UserToken"] = self._user_token _session = ClientSession() try: async with _session.post( f"{REST_URL}/code/{CLEAR_BLADE_SYSTEM_KEY}/dynamicAction", json=payload, headers=_headers, ) as resp: if resp.status == 200: _json = await resp.json() _LOGGER.debug(json.dumps(_json, indent=2)) if _json.get("success"): return _json raise InvalidResponseFormat() raise GenericHTTPError(resp.status) except ClientError as err: raise err finally: await _session.close() async def _authenticate(self, payload: dict) -> None: _session = ClientSession() try: async with _session.post( f"{REST_URL}/user/auth", json=payload, headers=HEADERS ) as resp: if resp.status == 200: _json = await resp.json() _LOGGER.debug(json.dumps(_json, indent=2)) if _json.get("options")["success"]: self._user_token = _json.get("user_token") self._account_id = _json.get("options").get("account_id") else: raise InvalidCredentialsError(_json.get("options")["message"]) else: raise GenericHTTPError(resp.status) finally: await _session.close() def _on_connect(self, client, userdata, flags, rc): _LOGGER.debug(f"Connected with result code: {str(rc)}") client.subscribe(f"user/{self._account_id}/device/reported") client.subscribe(f"user/{self._account_id}/device/desired") def _on_disconnect(self, client, userdata, rc): _LOGGER.debug(f"Disconnected with result code: {str(rc)}") if rc != 0: _LOGGER.error("EcoNet MQTT unexpected disconnect. Attempting to reconnect.") client.reconnect() def _on_message(self, client, userdata, msg): """When a MQTT message comes in push that update to the specified equipment""" try: unpacked_json = json.loads(msg.payload) _LOGGER.debug("MQTT message from topic: %s", msg.topic) _LOGGER.debug(json.dumps(unpacked_json, indent=2)) _name = unpacked_json.get("device_name") _serial = unpacked_json.get("serial_number") key = _serial _equipment = self._equipment.get(key) if _equipment is not None: _equipment._equipment_info, unpacked_json = self.check_update_enum(_equipment._equipment_info, unpacked_json) _equipment.update_equipment_info(unpacked_json) # Nasty hack to push signal updates to the device it belongs to elif "@SIGNAL" in str(unpacked_json): for _equipment in self._equipment.values(): if _equipment.device_id == _name: # Don't break after update for multi zone HVAC systems _equipment.update_equipment_info(unpacked_json) else: _LOGGER.debug( "Received update for non-existent equipment with device name: %s and serial number %s", _name, _serial, ) except Exception as e: _LOGGER.exception(e) _LOGGER.error("Failed to parse the following MQTT message: %s", msg.payload) pyeconet-0.1.28/src/pyeconet/equipment/000077500000000000000000000000001475412642500200665ustar00rootroot00000000000000pyeconet-0.1.28/src/pyeconet/equipment/__init__.py000066400000000000000000000147131475412642500222050ustar00rootroot00000000000000"""Define an EcoNet equipment""" import logging from enum import Enum from typing import Dict, Tuple, Union _LOGGER = logging.getLogger(__name__) WATER_HEATER = "WH" THERMOSTAT = "HVAC" class EquipmentType(Enum): """Define the equipment type""" WATER_HEATER = 1 THERMOSTAT = 2 UNKNOWN = 99 class Equipment: """Define an equipment""" def __init__(self, equipment_info: dict, api_interface) -> None: self._api = api_interface self._equipment_info = equipment_info self._update_callback = None def set_update_callback(self, callback): self._update_callback = callback def update_equipment_info(self, update: dict): """Take a dictionary and update the stored _equipment_info based on the present dict fields""" # Make sure this update is for this device, should probably check this before sending updates however _set = False if update.get("device_name") == self.device_id: for key, value in update.items(): if key[0] == "@": _LOGGER.debug( "Before update %s : %s", key, self._equipment_info.get(key) ) try: if isinstance(value, Dict): for _key, _value in value.items(): self._equipment_info[key][_key] = _value _LOGGER.debug( "Updating [%s][%s] = %s", key, _key, _value ) else: if isinstance(self._equipment_info.get(key), Dict): if self._equipment_info[key].get("value") is not None: self._equipment_info[key]["value"] = value _LOGGER.debug( "Updating [%s][value] = %s", key, value ) else: self._equipment_info[key] = value _LOGGER.debug("Updating [%s] = %s", key, value) except Exception: _LOGGER.error("Failed to update with message: %s", update) _LOGGER.debug( "After update %s : %s", key, self._equipment_info.get(key) ) _set = True else: _LOGGER.debug( "Not updating field because it isn't editable: %s, %s", key, value, ) pass else: _LOGGER.debug("Invalid update for device: %s", update) if self._update_callback is not None and _set: _LOGGER.debug("Calling the call back to notify updates have occurred") self._update_callback() @staticmethod def _coerce_type_from_string(value: str) -> EquipmentType: """Return a proper type from a string input.""" if value == WATER_HEATER: return EquipmentType.WATER_HEATER elif value == THERMOSTAT: return EquipmentType.THERMOSTAT else: _LOGGER.error("Unknown equipment type state: %s", value) return EquipmentType.UNKNOWN @property def active(self) -> bool: """Return equipment active state""" return self._equipment_info.get("@ACTIVE", True) @property def supports_away(self) -> bool: """Return if the user has enabled away mode functionality for this equipment.""" return self._equipment_info.get("@AWAYCONFIG", False) @property def away(self) -> bool: """Return if equipment has been set to away mode""" return self._equipment_info.get("@AWAY", False) @property def connected(self) -> bool: """Return if the equipment is connected or not""" return self._equipment_info.get("@CONNECTED", True) @property def device_name(self) -> str: """Return the generic name of the equipment""" return self._equipment_info.get("@NAME")["value"] @property def device_id(self) -> str: """Return the number name of the equipment""" return self._equipment_info.get("device_name") @property def generic_type(self) -> str: """Return the string type of the equipment""" return self._equipment_info.get("@TYPE") @property def vacation(self) -> bool: """Return if this equipment has been set up for vacation mode""" return self._equipment_info.get("@VACATION") @property def type(self) -> EquipmentType: """Return the EquipmentType of the equipment""" return self._coerce_type_from_string(self._equipment_info.get("device_type")) @property def serial_number(self) -> str: """Return the equipment serial number""" return self._equipment_info.get("serial_number") @property def alert_count(self) -> int: """Return the number of active alerts""" return self._equipment_info.get("@ALERTCOUNT") @property def set_point(self) -> int: """Return the water heaters temperature set point""" return self._equipment_info.get("@SETPOINT")["value"] @property def set_point_limits(self) -> Tuple: """Returns a tuple of the lower limit and upper limit for the set point""" set_point = self._equipment_info.get("@SETPOINT")["constraints"] return set_point["lowerLimit"], set_point["upperLimit"] @property def wifi_signal(self) -> Union[int, None]: """Return the Wifi signal in db. Note: this field isn't present in the REST API and only comes back on the devices MQTT topic. That means this field will be None until an update comes through over MQTT. """ signal = self._equipment_info.get("@SIGNAL") try: if signal: signal = int(signal) except TypeError: if signal: signal = self._equipment_info.get("@SIGNAL")["value"] return signal def force_update_from_api(self): self._api.refresh_equipment(self) def set_away_mode(self, away): """Set the away mode for the equipment""" if self.supports_away: self._api.publish({"@AWAY": away}, self.device_id, self.serial_number) else: _LOGGER.error("Unit isn't set up for away mode") pyeconet-0.1.28/src/pyeconet/equipment/thermostat.py000066400000000000000000000266251475412642500226450ustar00rootroot00000000000000"""EcoNet thermostat""" import logging from enum import Enum from typing import Tuple, Union, List, Optional from . import Equipment _LOGGER = logging.getLogger(__name__) class ThermostatOperationMode(Enum): """Define the operation mode""" OFF = 1 HEATING = 2 COOLING = 3 AUTO = 4 FAN_ONLY = 5 EMERGENCY_HEAT = 6 UNKNOWN = 99 @staticmethod def by_string(str_value: str): """Convert a string to a supported OperationMode""" _cleaned_string = str_value.rstrip().replace(" ", "").upper() if _cleaned_string == ThermostatOperationMode.OFF.name.upper(): return ThermostatOperationMode.OFF elif _cleaned_string == ThermostatOperationMode.HEATING.name.upper(): return ThermostatOperationMode.HEATING elif _cleaned_string == ThermostatOperationMode.COOLING.name.upper(): return ThermostatOperationMode.COOLING elif _cleaned_string == ThermostatOperationMode.AUTO.name.upper(): return ThermostatOperationMode.AUTO elif ( _cleaned_string == ThermostatOperationMode.FAN_ONLY.name.replace("_", "").upper() ): return ThermostatOperationMode.FAN_ONLY elif ( _cleaned_string == ThermostatOperationMode.EMERGENCY_HEAT.name.replace("_", "").upper() ): return ThermostatOperationMode.EMERGENCY_HEAT else: _LOGGER.error("Unknown mode: [%s]", str_value) return ThermostatOperationMode.UNKNOWN class ThermostatFanMode(Enum): """Define the operation mode""" AUTO = 1 LOW = 2 MEDLO = 3 MEDIUM = 4 MEDHI = 5 HIGH = 6 UNKNOWN = 99 @staticmethod def by_string(str_value: str): """Convert a string to a supported OperationMode""" _cleaned_string = str_value.rstrip().replace(" ", "_").replace(".", "").upper() if _cleaned_string == ThermostatFanMode.AUTO.name.upper(): return ThermostatFanMode.AUTO elif _cleaned_string == ThermostatFanMode.LOW.name.upper(): return ThermostatFanMode.LOW elif _cleaned_string == ThermostatFanMode.MEDLO.name.upper(): return ThermostatFanMode.MEDLO elif _cleaned_string == ThermostatFanMode.MEDIUM.name.upper(): return ThermostatFanMode.MEDIUM elif _cleaned_string == ThermostatFanMode.MEDHI.name.upper(): return ThermostatFanMode.MEDHI elif _cleaned_string == ThermostatFanMode.HIGH.name.upper(): return ThermostatFanMode.HIGH else: _LOGGER.error("Unknown fan mode: [%s]", str_value) return ThermostatFanMode.UNKNOWN class Thermostat(Equipment): @property def running(self) -> bool: """Return if the thermostat is running or not""" return self._equipment_info.get("@RUNNINGSTATUS") != "" @property def running_state(self) -> str: """Return the raw running status value""" return self._equipment_info.get("@RUNNINGSTATUS") @property def beep_enabled(self) -> bool: """Return if thermostat beep is enabled or not""" beep = self._equipment_info.get("@BEEP") if beep is not None: return beep["value"] == 1 else: return False @property def supports_humidifier(self) -> bool: return self._equipment_info.get("@DEHUMENABLE").get("constraints") is not None @property def cool_set_point(self) -> int: """Return the current cool set point""" return self._equipment_info.get("@COOLSETPOINT")["value"] @property def cool_set_point_limits(self) -> Tuple: """Returns a tuple of the lower limit and upper limit for the cool set point""" set_point = self._equipment_info.get("@COOLSETPOINT")["constraints"] return set_point["lowerLimit"], set_point["upperLimit"] @property def heat_set_point(self) -> int: return self._equipment_info.get("@HEATSETPOINT")["value"] @property def heat_set_point_limits(self) -> Tuple: """Returns a tuple of the lower limit and upper limit for the heat set point""" set_point = self._equipment_info.get("@HEATSETPOINT")["constraints"] return set_point["lowerLimit"], set_point["upperLimit"] @property def deadband(self) -> int: return self._equipment_info.get("@DEADBAND")["value"] @property def deadband_set_point_limits(self) -> Tuple: """Returns a tuple of the lower limit and upper limit for the cool set point""" set_point = self._equipment_info.get("@DEADBAND")["constraints"] return set_point["lowerLimit"], set_point["upperLimit"] @property def dehumidifier_set_point(self) -> int: return self._equipment_info.get("@DEHUMSETPOINT")["value"] @property def dehumidifier_set_point_limits(self) -> Tuple[int, int]: """Returns a tuple of the lower limit and upper limit for the dehumidifier set point""" set_point = self._equipment_info.get("@DEHUMSETPOINT")["constraints"] return set_point["lowerLimit"], set_point["upperLimit"] @property def dehumidifier_enabled(self) -> bool: return self._equipment_info.get("@DEHUMENABLE")["value"] == 1 def set_dehumidifier_set_point(self, humidity): """Set the provided humidity""" lower, upper = self.dehumidifier_set_point_limits if lower <= humidity <= upper: payload = {"@DEHUMSETPOINT": humidity} self._api.publish(payload, self.device_id, self.serial_number) else: _LOGGER.error( "Set point out of range. Lower: %s Upper: %s Humidity set point: %s", lower, upper, humidity, ) @property def zone_id(self) -> Union[str, None]: """Return the zode id""" return self._equipment_info.get("@ZONE_ID_NAME") @property def humidity(self) -> int: """Returns the current humidity""" return self._equipment_info.get("@HUMIDITY")["value"] @property def screen_locked(self) -> bool: return self._equipment_info.get("@SCREENLOCK")["value"] == 1 @property def modes(self) -> List[ThermostatOperationMode]: """Return a list of supported operation modes""" _supported_modes = [] _modes = self._equipment_info.get("@MODE")["constraints"]["enumText"] for _mode in _modes: _op_mode = ThermostatOperationMode.by_string(_mode) if _op_mode is not ThermostatOperationMode.UNKNOWN: _supported_modes.append(_op_mode) return _supported_modes @property def mode(self) -> Union[ThermostatOperationMode, None]: """Return the current mode""" return self.modes[self._equipment_info.get("@MODE")["value"]] @property def set_point_limits(self) -> Tuple: """ Returns a tuple of the lower limit and upper limit for the set point. Thermostat set points are too extreme -100 - 200 setting reasonable limits. """ return 40, 95 def set_mode(self, mode: ThermostatOperationMode): """Set the provided mode""" payload = {} text_modes = self._equipment_info["@MODE"]["constraints"]["enumText"] count = 0 for text_mode in text_modes: if mode == ThermostatOperationMode.by_string(text_mode): payload["@MODE"] = count count = count + 1 self._api.publish(payload, self.device_id, self.serial_number) @property def fan_modes(self) -> List[ThermostatFanMode]: """Return a list of supported operation modes""" _supported_modes = [] _modes = self._equipment_info.get("@FANSPEED")["constraints"]["enumText"] for _mode in _modes: _op_mode = ThermostatFanMode.by_string(_mode) if _op_mode is not ThermostatFanMode.UNKNOWN: _supported_modes.append(_op_mode) return _supported_modes @property def fan_mode(self) -> Union[ThermostatFanMode, None]: """Return the current mode""" return self.fan_modes[self._equipment_info.get("@FANSPEED")["value"]] def set_fan_mode(self, mode: ThermostatFanMode): """Set the provided mode""" payload = {} text_modes = self._equipment_info["@FANSPEED"]["constraints"]["enumText"] count = 0 for text_mode in text_modes: if mode == ThermostatFanMode.by_string(text_mode): payload["@FANSPEED"] = count count = count + 1 self._api.publish(payload, self.device_id, self.serial_number) def set_set_point(self, target_temp, target_temp_cool, target_temp_heat): """Set the provided set points based on mode. if just target temp is passed the temp of the current mode will be set to target_temp, this isn't valid for auto If target_temp_cool or target_temp_heat are passed target_temp will be ignored. """ cool_payload = {} heat_payload = {} if ( target_temp_cool or target_temp and self.mode == ThermostatOperationMode.COOLING ): _temp = target_temp if target_temp else target_temp_cool if self.cool_set_point_limits[0] <= _temp <= self.cool_set_point_limits[1]: cool_payload["@COOLSETPOINT"] = _temp else: _LOGGER.error( "Cool set point out of range. Lower: %s Upper: %s Cool set point: %s", self.cool_set_point_limits[0], self.cool_set_point_limits[1], _temp, ) if ( target_temp_heat or target_temp and self.mode in [ThermostatOperationMode.HEATING, ThermostatOperationMode.EMERGENCY_HEAT] ): _temp = target_temp if target_temp else target_temp_heat if self.heat_set_point_limits[0] <= _temp <= self.heat_set_point_limits[1]: heat_payload["@HEATSETPOINT"] = _temp else: _LOGGER.error( "Heat set point out of range. Lower: %s Upper: %s Heat set point: %s", self.heat_set_point_limits[0], self.heat_set_point_limits[1], _temp, ) has_set_temp = False if cool_payload and self.mode in [ ThermostatOperationMode.AUTO, ThermostatOperationMode.COOLING, ]: self._api.publish(cool_payload, self.device_id, self.serial_number) has_set_temp = True if heat_payload and self.mode in [ ThermostatOperationMode.AUTO, ThermostatOperationMode.HEATING, ThermostatOperationMode.EMERGENCY_HEAT, ]: self._api.publish(heat_payload, self.device_id, self.serial_number) has_set_temp = True if target_temp and not has_set_temp: payload = {} if self.mode == ThermostatOperationMode.COOLING: payload = cool_payload elif self.mode in [ ThermostatOperationMode.HEATING, ThermostatOperationMode.EMERGENCY_HEAT, ]: payload = heat_payload else: _LOGGER.error( "Can't auto determine set point to set when mode is: %s", self.mode ) self._api.publish(payload, self.device_id, self.serial_number) pyeconet-0.1.28/src/pyeconet/equipment/water_heater.py000066400000000000000000000357301475412642500231220ustar00rootroot00000000000000"""EcoNet water heater""" from datetime import datetime, timedelta import logging import enum from typing import List, Union, Optional, Dict from pyeconet.errors import InvalidResponseFormat from pyeconet.equipment import Equipment _LOGGER = logging.getLogger(__name__) try: from enum import StrEnum except ImportError: class StrEnum(str, enum.Enum): pass @enum.unique class WaterHeaterOperationMode(enum.IntEnum): """Define the operation mode""" OFF = 1 ELECTRIC_MODE = 2 ENERGY_SAVING = 3 HEAT_PUMP_ONLY = 4 HIGH_DEMAND = 5 GAS = 6 ENERGY_SAVER = 7 PERFORMANCE = 8 VACATION = 9 ELECTRIC = 10 HEAT_PUMP = 11 ELECTRIC_GAS = 12 UNKNOWN = 99 @staticmethod def by_string(str_value: str): """Convert a string to a supported OperationMode""" _cleaned_string = str_value.rstrip().replace(" ", "_").replace("/", "_").upper() if _cleaned_string == WaterHeaterOperationMode.OFF.name.upper(): return WaterHeaterOperationMode.OFF elif _cleaned_string == WaterHeaterOperationMode.ELECTRIC_MODE.name.upper(): return WaterHeaterOperationMode.ELECTRIC_MODE elif _cleaned_string == WaterHeaterOperationMode.ENERGY_SAVING.name.upper(): return WaterHeaterOperationMode.ENERGY_SAVING elif _cleaned_string == WaterHeaterOperationMode.HEAT_PUMP_ONLY.name.upper(): return WaterHeaterOperationMode.HEAT_PUMP_ONLY elif _cleaned_string == WaterHeaterOperationMode.HIGH_DEMAND.name.upper(): return WaterHeaterOperationMode.HIGH_DEMAND elif _cleaned_string == WaterHeaterOperationMode.GAS.name.upper(): return WaterHeaterOperationMode.GAS elif _cleaned_string == WaterHeaterOperationMode.ENERGY_SAVER.name.upper(): # Treat ENERGY SAVER and ENERGY SAVING modes the same return WaterHeaterOperationMode.ENERGY_SAVING elif _cleaned_string == WaterHeaterOperationMode.PERFORMANCE.name.upper(): return WaterHeaterOperationMode.PERFORMANCE elif _cleaned_string == WaterHeaterOperationMode.VACATION.name.upper(): return WaterHeaterOperationMode.VACATION elif _cleaned_string == WaterHeaterOperationMode.ELECTRIC.name.upper(): # Treat ELECTRIC MODE and ELECTRIC modes the same return WaterHeaterOperationMode.ELECTRIC_MODE elif _cleaned_string == WaterHeaterOperationMode.HEAT_PUMP.name.upper(): # Treat HEAT PUMP ONLY and HEAT PUMP modes the same return WaterHeaterOperationMode.HEAT_PUMP_ONLY elif _cleaned_string == WaterHeaterOperationMode.ELECTRIC_GAS.name.upper(): return WaterHeaterOperationMode.ELECTRIC_GAS else: _LOGGER.error("Unknown mode: [%s]", str_value) return WaterHeaterOperationMode.UNKNOWN @enum.unique class UsageFormat(StrEnum): DAILY = "daily" WEEKLY = "weekly" YEARLY = "yearly" MONTHLY = "monthly" class WaterHeater(Equipment): def __init__(self, equipment_info: dict, api_interface) -> None: """Initialize.""" super().__init__(equipment_info, api_interface) self._energy_usage = None self._historical_energy_usage = None self._energy_type = None self.water_usage = None @property def leak_installed(self) -> bool: """Return if heater has leak detection or not""" leak = self._equipment_info.get("@LEAKINSTALLED") if leak is not None: return leak["value"] == 1 else: return False @property def has_shutoff_valve(self) -> bool: return self._equipment_info.get("@VALVESTATUS", {}).get('title', "").startswith("Shut-OFF Valve - ") @property def running(self) -> bool: """Return if the water heater is running or not""" return self._equipment_info.get("@RUNNING") != "" @property def running_state(self) -> str: """Return the raw running value""" return self._equipment_info.get("@RUNNING") @property def tank_hot_water_availability(self) -> Union[int, None]: """Return the hot water availability""" icon = self._equipment_info.get("@HOTWATER") value = 100 if icon is None: _LOGGER.debug("Tank does not support hot water capacity") return None if "ic_tank_hundread_percent" in icon: value = 100 elif "ic_tank_fourty_percent" in icon: value = 40 elif "ic_tank_ten_percent" in icon: value = 10 elif "ic_tank_empty" in icon or "ic_tank_zero_percent" in icon: # Tank is empty when shutoff valve is closed value = 0 else: _LOGGER.error("Invalid tank level: %s", icon) return value @property def shutoff_valve_open(self) -> Union[bool, None]: """Return if the shutoff valve is open or not""" if self.has_shutoff_valve: status = self._equipment_info.get("@VALVESTATUS")["title"] if status == "Shut-OFF Valve - Open": return True elif status == "Shut-OFF Valve - Closed": return False return None @property def tank_health(self) -> Union[int, None]: """Return the value 0-100? of the tank/heating element health""" return self._equipment_info.get("@TANK", {}).get("value") @property def compressor_health(self) -> Union[int, None]: """Return the value 0-100 of the compressor for heat pump units health""" return self._equipment_info.get("@COMBUSTION", {}).get("value") @property def demand_response_over(self) -> bool: """Return if the demand response is running or not""" return self._equipment_info.get("@VALVE")["value"] == 0 def _supports_modes(self) -> bool: """Return if the system supports modes or not""" return self._equipment_info.get("@MODE") is not None def _supports_on_off(self) -> bool: """Return if the system supports on and off""" return self._equipment_info.get("@ENABLED") is not None @property def modes(self) -> List[WaterHeaterOperationMode]: """Return a list of supported operation modes""" _supported_modes = [] if self._supports_modes(): _modes = self._equipment_info.get("@MODE")["constraints"]["enumText"] for _mode in _modes: _op_mode = WaterHeaterOperationMode.by_string(_mode) if _op_mode is not WaterHeaterOperationMode.UNKNOWN: if _op_mode is WaterHeaterOperationMode.ELECTRIC_GAS: if ( self.generic_type == "gasWaterHeater" or self.generic_type == "tanklessWaterHeater" ): _supported_modes.append(WaterHeaterOperationMode.GAS) else: _supported_modes.append( WaterHeaterOperationMode.ELECTRIC_MODE ) else: _supported_modes.append(_op_mode) if self._supports_on_off() and not _supported_modes: _supported_modes.append(WaterHeaterOperationMode.OFF) if ( self.generic_type == "gasWaterHeater" or self.generic_type == "tanklessWaterHeater" ): _supported_modes.append(WaterHeaterOperationMode.GAS) else: _supported_modes.append(WaterHeaterOperationMode.ELECTRIC_MODE) elif self._supports_on_off() and _supported_modes: _supported_modes.append(WaterHeaterOperationMode.OFF) return _supported_modes @property def mode(self) -> Union[WaterHeaterOperationMode, None]: """Return the current mode""" if self._supports_on_off(): if not self.enabled: return WaterHeaterOperationMode.OFF if self._supports_modes(): return self.modes[self._equipment_info.get("@MODE")["value"]] else: if ( self.generic_type == "gasWaterHeater" or self.generic_type == "tanklessWaterHeater" ): return WaterHeaterOperationMode.GAS else: return WaterHeaterOperationMode.ELECTRIC_MODE @property def enabled(self) -> Union[bool, None]: """Return the the water heater is enabled or not""" if self._supports_modes(): return self.modes[self._equipment_info.get("@MODE")["value"]] != "OFF" elif self._supports_on_off(): return self._equipment_info.get("@ENABLED")["value"] == 1 else: # Unit doesn't support on/off or modes return None @property def override_status(self) -> str: """Return the alert override status""" return self._equipment_info.get("@OVERRIDESTATUS") @property def energy_usage(self) -> Dict[int, float]: return self._energy_usage @property def historical_energy_usage(self) -> Dict[int, float]: return self._historical_energy_usage @property def energy_type(self) -> str: """Return the energy type returned from the energy usage response.""" return self._energy_type @property def todays_energy_usage(self) -> Union[float, None]: _total = 0 if self._energy_usage: for value in self._energy_usage.values(): _total += value return _total else: return None @property def todays_water_usage(self) -> Union[float, None]: return self.water_usage async def get_energy_usage( self, start: Optional[datetime] = None, end: Optional[datetime] = None, ): """Call dynamic action for energy usage.""" current_date = datetime.now() end_datetime_formatted = current_date.strftime("%Y-%m-%dT23:59:59.999") start_datetime_formatted = current_date.strftime("%Y-%m-%dT00:00:00.999") if end: end_datetime_formatted = current_date.strftime("%Y-%m-%dT%H:%M:%S.000") if start: start_datetime_formatted = current_date.strftime("%Y-%m-%dT%H:%M:%S.000") payload = { "ACTION": "waterheaterUsageReportView", "device_name": f"{self.device_id}", "serial_number": f"{self.serial_number}", "start_date": start_datetime_formatted, "end_date": end_datetime_formatted, "usage_type": "energyUsage", } try: _response = await self._api.get_dynamic_action(payload) except InvalidResponseFormat: _LOGGER.debug("Tried to get energy usage, but unit doesn't support it.") return self._energy_usage = { int(item["name"]): item["value"] for item in _response["results"]["energy_usage"]["data"] } self._historical_energy_usage = { int(item["name"]): item["value"] for item in _response["results"]["energy_usage"]["historyData"] } try: self._energy_type = ( _response["results"]["energy_usage"]["message"].split(" ")[3].upper() ) except (KeyError, IndexError): _LOGGER.error("Failed to determine energy type from response.") if self.generic_type == "gasWaterHeater": self._energy_type = "KBTU" else: self._energy_type = "KWH" _LOGGER.debug(self._energy_usage) async def get_water_usage(self, start: Optional[datetime] = None, end: Optional[datetime] = None ): """Call dynamic action for water usage.""" current_date = datetime.now() end_datetime_formatted = current_date.strftime("%Y-%m-%dT23:59:59.999") start_datetime_formatted = current_date.strftime("%Y-%m-%dT00:00:00.999") if end: end_datetime_formatted = current_date.strftime("%Y-%m-%dT%H:%M:%S.000") if start: start_datetime_formatted = current_date.strftime("%Y-%m-%dT%H:%M:%S.000") payload = { "ACTION": "waterheaterUsageReportView", "device_name": f"{self.device_id}", "serial_number": f"{self.serial_number}", "start_date": start_datetime_formatted, "end_date": end_datetime_formatted, "usage_type": "waterUsage", } try: _response = await self._api.get_dynamic_action(payload) except InvalidResponseFormat: _LOGGER.debug("Tried to get water usage, but unit doesn't support it.") return _todays_usage = 0 for value in _response["results"]["water_usage"]["data"]: _todays_usage += value["value"] self.water_usage = _todays_usage _LOGGER.debug(_todays_usage) def set_mode(self, new_mode: WaterHeaterOperationMode): """Set the provided mode or enable/disable if mode isn't support.""" payload = {} if not (self._supports_on_off() or self._supports_modes()): _LOGGER.error( "Unit doesn't support on off or modes, shouldn't be trying to set a mode." ) return if self._supports_on_off(): if new_mode == WaterHeaterOperationMode.OFF: payload["@ENABLED"] = 0 else: payload["@ENABLED"] = 1 # Traverse the supported mode strings returned from the water heater # and set the payload to the index value of the one we want if self._supports_modes(): text_modes = self._equipment_info["@MODE"]["constraints"]["enumText"] count = 0 for text_mode in text_modes: candidate_mode = WaterHeaterOperationMode.by_string(text_mode) if new_mode == candidate_mode: payload["@MODE"] = count elif candidate_mode == WaterHeaterOperationMode.ELECTRIC_GAS: if new_mode == WaterHeaterOperationMode.ELECTRIC_MODE: payload["@MODE"] = count elif new_mode == WaterHeaterOperationMode.GAS: payload["@MODE"] = count count = count + 1 if not "@MODE" in payload: _LOGGER.error("Could not find a matching mode string to set.") if payload: self._api.publish(payload, self.device_id, self.serial_number) def set_set_point(self, set_point: int): """Set the equipment set point to set_point.""" lower, upper = self.set_point_limits if lower <= set_point <= upper: payload = {"@SETPOINT": set_point} self._api.publish(payload, self.device_id, self.serial_number) else: _LOGGER.error( "Set point out of range. Lower: %s Upper: %s Set point: %s", lower, upper, set_point, ) pyeconet-0.1.28/src/pyeconet/errors.py000066400000000000000000000006261475412642500177510ustar00rootroot00000000000000"""Define package errors.""" class PyeconetError(Exception): """A base error.""" pass class InvalidCredentialsError(PyeconetError): """An error related to invalid requests.""" pass class InvalidResponseFormat(PyeconetError): """An error related to invalid requests.""" pass class GenericHTTPError(PyeconetError): """An error related to invalid requests.""" pass pyeconet-0.1.28/src/pyeconet/example_responses/000077500000000000000000000000001475412642500216135ustar00rootroot00000000000000pyeconet-0.1.28/src/pyeconet/example_responses/dynamic_action_energy_usage.json000066400000000000000000000120311475412642500302210ustar00rootroot00000000000000{ "results": { "energy_usage": { "data": [ { "name": "1", "value": 0 }, { "name": "2", "value": 0 }, { "name": "3", "value": 0 }, { "name": "4", "value": 0 }, { "name": "5", "value": 0 }, { "name": "6", "value": 0 }, { "name": "7", "value": 0 }, { "name": "8", "value": 0 }, { "name": "9", "value": 0.63 }, { "name": "10", "value": 0 }, { "name": "11", "value": 0.66 }, { "name": "12", "value": 0 }, { "name": "13", "value": 0.55 }, { "name": "14", "value": 0.58 }, { "name": "15", "value": 0 }, { "name": "16", "value": 0 }, { "name": "17", "value": 0 }, { "name": "18", "value": 0 }, { "name": "19", "value": 0 }, { "name": "20", "value": 0 }, { "name": "21", "value": 1.37 }, { "name": "22", "value": 1.93 }, { "name": "23", "value": 0 }, { "name": "24", "value": 0 } ], "historyData": [ { "name": "1", "value": 0 }, { "name": "2", "value": 0 }, { "name": "3", "value": 0 }, { "name": "4", "value": 0 }, { "name": "5", "value": 0 }, { "name": "6", "value": 0 }, { "name": "7", "value": 0 }, { "name": "8", "value": 0 }, { "name": "9", "value": 0 }, { "name": "10", "value": 0.67 }, { "name": "11", "value": 0 }, { "name": "12", "value": 0.56 }, { "name": "13", "value": 0 }, { "name": "14", "value": 0 }, { "name": "15", "value": 0 }, { "name": "16", "value": 0.5 }, { "name": "17", "value": 0 }, { "name": "18", "value": 0 }, { "name": "19", "value": 0 }, { "name": "20", "value": 0.54 }, { "name": "21", "value": 0 }, { "name": "22", "value": 0 }, { "name": "23", "value": 0 }, { "name": "24", "value": 0 } ], "message": "

You've used 5.72 kWh units of energy

" } }, "success": true }pyeconet-0.1.28/src/pyeconet/example_responses/get_locations_hvac.json000066400000000000000000000215131475412642500263430ustar00rootroot00000000000000{ "results": { "locations": [ { "@AWAY": false, "@AWAYCONFIG": true, "@LOCATION_INFO": "CITY, ST", "@LOCATION_NAME": "Home", "@LOCATION_STATUS": "I'm Home", "@VACATION": false, "@VACATIONCONFIG": false, "@WEATHER": "66°", "@WEATHER_F": 66, "@WEATHER_I": "", "equiptments": [ { "@ALERTCOUNT": 0, "@AWAY": false, "@AWAYCONFIG": true, "@BCONFIG": [ { "align": "center", "isConversion": true, "name": "@RUNNINGSTATUS", "type": "TEXT_LABEL_VIEW", "value": "" }, { "align": "center", "name": "@SCHEDULESTATUS", "type": "TEXT_LABEL_VIEW", "value": "Following Schedule" }, { "name": "@SCHEDULERESUME", "title": "Resume", "type": "BUTTON_VIEW", "value": "" } ], "@BEEP": { "constraints": { "enumText": [ "No", "Yes" ], "enumTextIcon": [], "lowerLimit": 0, "upperLimit": 1 }, "status": "Yes", "value": 1 }, "@CARDSTATUS": "Heating", "@CONNECTED": true, "@COOLSETPOINT": { "constraints": { "isConversion": true, "lowerLimit": 52, "unit": 1, "upperLimit": 92 }, "value": 70 }, "@DEADBAND": { "constraints": { "lowerLimit": 0, "unit": 6, "upperLimit": 6 }, "value": 2 }, "@DEHUMENABLE": { "constraints": { "enumText": [ "No", "Yes" ], "enumTextIcon": [], "lowerLimit": 0, "upperLimit": 1 }, "status": "No", "value": 0 }, "@DEHUMSETPOINT": { "constraints": { "lowerLimit": 40, "unit": 3, "upperLimit": 60 }, "value": 52 }, "@DRACTIVE": { "constraints": { "dialog": [ { "message": "This should not impact the water temperature in your home. Do you want to opt out for this event ?", "title": "Tank temperature has been changed in response to a Utility Load Control event", "value": 1 } ] }, "value": "" }, "@FANSPEED": { "constraints": { "enumText": [ "Auto", "Low", "Med.Lo", "Medium", "Med.Hi", "High" ], "lowerLimit": 0, "upperLimit": 5 }, "status": "Auto", "value": 0 }, "@HEATSETPOINT": { "constraints": { "isConversion": true, "lowerLimit": 50, "unit": 1, "upperLimit": 90 }, "value": 68 }, "@HUMIDITY": { "constraints": { "lowerLimit": 0, "unit": 3, "upperLimit": 100 }, "value": 48 }, "@MODE": { "constraints": { "enumText": [ "Heating", "Cooling", "Auto", "Fan Only", "Off", "Emergency Heat" ], "enumTextIcon": [ "ic_thermo_heating.png", "ic_thermo_cooling.png", "ic_thermo_auto.png", "ic_fan.png", "ic_device_off.png", "http://heaticon.png" ], "lowerLimit": 0, "upperLimit": 5 }, "status": "Heating", "value": 0 }, "@NAME": { "constraints": { "stringLength": 64 }, "value": "Thermostat" }, "@RESUME": false, "@RUNNINGSTATUS": "", "@SCHEDULE": true, "@SCHEDULERESUME": "", "@SCHEDULESTATUS": "Following Schedule", "@SCREENLOCK": { "constraints": { "enumText": [ "No", "Yes" ], "enumTextIcon": [], "lowerLimit": 0, "upperLimit": 1 }, "status": "No", "value": 0 }, "@SETPOINT": { "constraints": { "isConversion": true, "lowerLimit": -40, "unit": 1, "upperLimit": 99 }, "value": 72 }, "@STATUS": "Heating", "@TYPE": "econetControlCenter", "@VACATION": false, "actions": [ "networkSettings", "thermostatProductSettings", "thermostatScheduleView" ], "device_name": "74901882412345", "device_type": "HVAC", "serial_number": "Q032012345" } ], "location_id": "d89d2c77-ded4-442b-9b40-05f6c12345" } ] }, "success": true }pyeconet-0.1.28/src/pyeconet/example_responses/get_locations_water_heater.json000066400000000000000000000066371475412642500301060ustar00rootroot00000000000000{ "results": { "locations": [{ "@AWAY": false, "@AWAYCONFIG": false, "@LOCATION_INFO": "City, State_Code", "@LOCATION_NAME": "Home", "@LOCATION_STATUS": "I'm Home", "@VACATION": false, "@VACATIONCONFIG": false, "@WEATHER": "71°", "@WEATHER_F": 71, "@WEATHER_I": "", "equiptments": [{ "@ACTIVE": true, "@ALERTCOUNT": 0, "@AWAY": false, "@BCONFIG": [{ "align": "center", "name": "@SCHEDULESTATUS", "type": "TEXT_LABEL_VIEW", "value": "" }, { "name": "@SCHEDULERESUME", "title": "Resume", "type": "BUTTON_VIEW", "value": "" }], "@CONNECTED": true, "@DRACTIVE": { "constraints": { "dialog": [{ "message": "This should not impact the water temperature in your home. Do you want to opt out for this event ?", "title": "Tank temperature has been changed in response to a Utility Load Control event", "value": 1 }] }, "value": "" }, "@MODE": { "constraints": { "enumText": ["OFF", "ENERGY SAVING", "HEAT PUMP ONLY ", "HIGH DEMAND", "ELECTRIC MODE"], "enumTextIcon": ["ic_device_off.png", "ic_energy_saver.png", "ic_heat_pump.png", "ic_high_demand.png", "ic_electric.png"], "lowerLimit": 0, "upperLimit": 4 }, "status": "ENERGY SAVING", "value": 1 }, "@MODECONFIG": { "constraints": { "bgcolor": "#008000", "enumText": ["Off ", "Energy Saver ", "Heat Pump ", "High Demand ", "Electric "], "enumTextIcon": [], "fontcolor": "#FFFFFF", "icon": "ic_energy_saver", "lowerLimit": 0, "upperLimit": 4 }, "status": "", "value": "" }, "@MODEIMAGE": "ic_energy_saver.png", "@NAME": { "constraints": { "stringLength": 64 }, "value": "Water Heater" }, "@RESUME": false, "@RUNNING": "Running", "@SCHEDULE": false, "@SCHEDULERESUME": "", "@SCHEDULESTATUS": "", "@SETPOINT": { "constraints": { "error": [], "formatDecimal": 0, "isConversion": true, "lowerLimit": 110, "units": "deg F", "upperLimit": 140, "warning": [{ "message": "CAUTION HOT WATER. Contact may cause serious burns to skin", "value": 121 }] }, "value": 132 }, "@STATUS": "Enabled ", "@TCONFIG": [{ "align": "center", "name": "@MODEIMAGE", "type": "SINGLE_IMAGE_VIEW", "value": "ic_energy_saver.png" }, { "align": "center", "name": "@RUNNING", "type": "TEXT_LABEL_VIEW", "value": "Running" }], "@TYPE": "heatpumpWaterHeaterGen4", "@VACATION": false, "actions": ["networkSettings", "waterheaterUsageReportView", "waterheaterScheduleView"], "device_name": "74901882412345", "device_type": "WH", "serial_number": "Q032012345" }], "location_id": "d89d2c77-ded4-442b-9b40-05f6c12345" }] }, "success": true } pyeconet-0.1.28/src/pyeconet/example_responses/get_locations_water_heater_electric_gas_mode.json000066400000000000000000000217721475412642500336130ustar00rootroot00000000000000{ "results": { "locations": [ { "@AWAY": false, "@AWAYCONFIG": false, "@LOCATION_INFO": "XXX, XXX", "@LOCATION_NAME": "Home", "@LOCATION_STATUS": "I'm Home", "@VACATION": false, "@VACATIONCONFIG": false, "@WEATHER": "51°", "@WEATHER_F": 51, "@WEATHER_I": "clearn.png", "equiptments": [ { "@ACTIVE": true, "@ALERTCOUNT": 0, "@AWAY": false, "@AWAYCONFIG": false, "@AWAY_MSG": "", "@BCONFIG": [ { "align": "center", "name": "@SCHEDULESTATUS", "type": "TEXT_LABEL_VIEW", "value": "" }, { "name": "@SCHEDULERESUME", "title": "Resume", "type": "BUTTON_VIEW", "value": "" }, { "align": "center", "type": "TEXT_LABEL_VIEW", "value": "Hot Water Availability" }, { "align": "center", "name": "@HOTWATER", "title": "Hot Water Availability", "type": "SINGLE_IMAGE_VIEW", "value": "ic_tank_hundread_percent_v2.png" } ], "@COMBUSTION": { "constraints": { "formatDecimal": 1, "green": 55, "lowerLimit": 0, "orange": 25, "red": 20, "units": "HRS", "upperLimit": 10000000 }, "status": "Compressor life is normal", "value": 100 }, "@CONFIG": [ { "align": "center", "name": "@MODECONFIG", "title": "This Equipment should be set to ENERGY SAVING Mode for maximum efficiency", "type": "IMAGE_TEXT_BUTTON_VIEW", "value": { "constraints": { "bgcolor": "#008000", "enumText": [ "Off ", "Energy Saver ", "Heat Pump ", "High Demand ", "Electric/Gas ", "Vacation " ], "enumTextIcon": [], "fontcolor": "#FFFFFF", "icon": "ic_energy_saver", "lowerLimit": 0, "upperLimit": 5 }, "status": "Enable", "value": 1 } } ], "@CONNECTED": true, "@DRACTIVE": { "constraints": { "dialog": [ { "message": "This should not impact the water temperature in your home. Do you want to opt out for this event ?", "title": "Tank temperature has been changed in response to a Utility Load Control event", "value": 1 } ] }, "value": "" }, "@DRESOVER": { "constraints": { "enumText": ["false", "true "], "enumTextIcon": [], "lowerLimit": 0, "upperLimit": 1 }, "status": "false", "value": 0 }, "@ENABLED": { "constraints": { "enumText": ["Disabled", "Enabled "], "enumTextIcon": ["ic_device_off.png", "ic_enabled.png"], "lowerLimit": 0, "upperLimit": 1 }, "status": "Enabled ", "value": 1 }, "@HOTWATER": "ic_tank_hundread_percent_v2.png", "@JA13STATUS": "", "@LEAKINSTALLED": { "constraints": { "align": "center", "bgcolor": "#bec2bf", "enumText": ["No ", "Yes"], "enumTextIcon": [], "fontcolor": "#000000", "lowerLimit": 0, "upperLimit": 1 }, "status": "", "title": "Leak sensor not installed", "value": 0 }, "@MODE": { "constraints": { "enumText": [ "Off ", "Energy Saver ", "Heat Pump ", "High Demand ", "Electric/Gas ", "Vacation " ], "enumTextIcon": [ "ic_device_off.png", "ic_energy_saver.png", "ic_heat_pump.png", "ic_high_demand.png", "http://heaticon.png", "ic_vacation.png" ], "lowerLimit": 0, "upperLimit": 5 }, "status": "High Demand ", "value": 3 }, "@MODECONFIG": { "constraints": { "bgcolor": "#008000", "enumText": [ "Off ", "Energy Saver ", "Heat Pump ", "High Demand ", "Electric/Gas ", "Vacation " ], "enumTextIcon": [], "fontcolor": "#FFFFFF", "icon": "ic_energy_saver", "lowerLimit": 0, "upperLimit": 5 }, "status": "Enable", "value": 1 }, "@MODEIMAGE": "", "@NAME": { "constraints": { "stringLength": 64 }, "value": "Heat Pump Water Heater" }, "@OVERRIDE": { "constraints": { "bgcolor": "#FFFF00", "fontcolor": "#000000", "icon": "ic_high_level_alert.png" }, "status": "", "value": "" }, "@OVERRIDESTATUS": "", "@RESUME": false, "@RUNNING": "Compressor Running", "@SCHEDULE": false, "@SCHEDULERESUME": "", "@SCHEDULESTATUS": "", "@SETPOINT": { "constraints": { "error": [], "formatDecimal": 0, "isConversion": true, "lowerLimit": 110, "units": "deg F", "upperLimit": 140, "warning": [ { "message": "CAUTION HOT WATER. Contact may cause serious burns to skin", "value": 121 } ] }, "value": 120 }, "@STATUS": "Enabled ", "@TANK": { "constraints": { "green": 40, "orange": 30, "red": 30 }, "status": "Element operating normally", "value": 100 }, "@TCONFIG": [ { "align": "center", "name": "@MODEIMAGE", "type": "SINGLE_IMAGE_VIEW", "value": "" }, { "align": "center", "name": "@RUNNING", "type": "TEXT_LABEL_VIEW", "value": "Compressor Running" }, { "align": "center", "name": "@OVERRIDESTATUS", "type": "TEXT_LABEL_VIEW", "value": "" } ], "@TYPE": "heatpumpWaterHeaterGen5", "@VACATION": false, "@VALVE": { "constraints": { "dialog": [ { "message": "Closing the shut off valve will disable the water heater", "title": "Closing Valve", "value": 0 } ], "error": [] }, "value": "" }, "@VALVESTATUS": { "constraints": { "align": "center", "bgcolor": "#bec2bf", "fontcolor": "#000000" }, "status": "", "title": "Shut-OFF Valve not installed", "value": 1 }, "actions": [ "waterheaterScheduleView", "networkSettings", "waterheaterUsageReportView", "waterheaterHealthView" ], "device_name": "", "device_type": "WH", "mac_address": "", "serial_number": "" } ], "location_id": "" } ] }, "success": true, "logs": "", "stack": "" } pyeconet-0.1.28/src/pyeconet/example_responses/get_locations_water_heater_legacy_modes.json000066400000000000000000000074221475412642500326120ustar00rootroot00000000000000{ "results": { "locations": [ { "@AWAY": false, "@AWAYCONFIG": true, "@LOCATION_INFO": "CITY, ST", "@LOCATION_NAME": "My Home", "@LOCATION_STATUS": "I'm Home", "@VACATION": false, "@VACATIONCONFIG": false, "@WEATHER": "71°", "@WEATHER_F": 71, "@WEATHER_I": "", "equiptments": [ { "@ACTIVE": true, "@ALERTCOUNT": 0, "@AWAY": false, "@AWAYCONFIG": true, "@BCONFIG": [ { "align": "center", "name": "@SCHEDULESTATUS", "type": "TEXT_LABEL_VIEW", "value": "" }, { "name": "@SCHEDULERESUME", "title": "Resume", "type": "BUTTON_VIEW", "value": "" } ], "@CONNECTED": true, "@DRACTIVE": { "constraints": { "dialog": [ { "message": "This should not impact the water temperature in your home. Do you want to opt out for this event ?", "title": "Tank temperature has been changed in response to a Utility Load Control event", "value": 1 } ] }, "value": "" }, "@ENABLED": { "constraints": { "enumText": [ "Disabled", "Enabled" ], "enumTextIcon": [ "ic_device_off.png", "ic_enabled.png" ], "lowerLimit": 0, "upperLimit": 1 }, "status": "Enabled", "value": 1 }, "@MODE": { "constraints": { "enumText": [ "Energy Saver", "Performance" ], "enumTextIcon": [ "ic_energy_saver.png", "ic_high_demand.png" ], "lowerLimit": 0, "upperLimit": 1 }, "status": "Energy Saver", "value": 0 }, "@NAME": { "constraints": { "stringLength": 64 }, "value": "Electric Water Heater" }, "@RESUME": false, "@RUNNING": "", "@SCHEDULE": false, "@SCHEDULERESUME": "", "@SCHEDULESTATUS": "", "@SETPOINT": { "constraints": { "error": [], "isConversion": true, "lowerLimit": 110, "unit": 1, "upperLimit": 130, "warning": [ { "message": "CAUTION HOT WATER. Contact may cause serious burns to skin", "value": 121 } ] }, "value": 120 }, "@STATUS": "Enabled", "@TCONFIG": [ { "align": "center", "name": "@RUNNING", "type": "TEXT_LABEL_VIEW", "value": "" } ], "@TYPE": "electricWaterHeater", "@VACATION": false, "actions": [ "networkSettings", "waterheaterScheduleView", "@RUNNING" ], "device_name": "74901882412345", "device_type": "WH", "serial_number": "Q032012345" } ], "location_id": "d89d2c77-ded4-442b-9b40-05f6c12345" } ] }, "success": true }pyeconet-0.1.28/src/pyeconet/example_responses/get_locations_water_heater_no_modes.json000066400000000000000000000076771475412642500317760ustar00rootroot00000000000000{ "results": { "locations": [ { "@AWAY": false, "@AWAYCONFIG": true, "@LOCATION_INFO": "CITY, ST", "@LOCATION_NAME": "Home", "@LOCATION_STATUS": "I'm Home", "@VACATION": false, "@VACATIONCONFIG": false, "@WEATHER": "83°", "@WEATHER_F": 83, "@WEATHER_I": "", "equiptments": [ { "@ALERTCOUNT": 0, "@AWAY": false, "@AWAYCONFIG": true, "@BCONFIG": [ { "align": "center", "name": "@SCHEDULESTATUS", "type": "TEXT_LABEL_VIEW", "value": "" }, { "name": "@SCHEDULERESUME", "title": "Resume", "type": "BUTTON_VIEW", "value": "" } ], "@CONFIG": [ { "align": "center", "name": "@SETWARNING", "type": "TEXT_LABEL_VIEW", "value": "The temperature control located on the water heater is set to a maximum temperature of 140" } ], "@CONNECTED": true, "@DRACTIVE": { "constraints": { "dialog": [ { "message": "This should not impact the water temperature in your home. Do you want to opt out for this event ?", "title": "Tank temperature has been changed in response to a Utility Load Control event", "value": 1 } ] }, "value": "" }, "@IP_ADDRESS": { "constraints": { "stringLength": 32 }, "value": "192.168.1.2" }, "@MAC_ADDRESS": { "value": "54-23-1E-12-12-12" }, "@NAME": { "constraints": { "stringLength": 64 }, "value": "Gas Water Heater [WiFi]" }, "@RESUME": false, "@RUNNING": "", "@SCHEDULE": false, "@SCHEDULERESUME": "", "@SCHEDULESTATUS": "", "@SERVER_STATUS": "", "@SETPOINT": { "constraints": { "error": [], "isConversion": true, "lowerLimit": 90, "unit": 1, "upperLimit": 140, "warning": [ { "message": "CAUTION HOT WATER. Contact may cause serious burns to skin", "value": 121 } ] }, "value": 120 }, "@SETWARNING": "The temperature control located on the water heater is set to a maximum temperature of 140", "@SIGNAL": { "constraints": { "lowerLimit": -500, "upperLimit": 500 }, "icon": "ic_signalstrength_1.png", "value": -75 }, "@SSID": { "constraints": { "stringLength": 32 }, "value": "wap" }, "@TCONFIG": [ { "align": "center", "name": "@RUNNING", "type": "TEXT_LABEL_VIEW", "value": "" } ], "@TYPE": "gasWaterHeater", "@VACATION": false, "@VERSION": { "value": "RH-WIFI-00-01-06" }, "actions": [ "networkSettings", "waterheaterScheduleView" ], "device_name": "74901882412345", "device_type": "WH", "serial_number": "Q032012345" } ], "location_id": "d89d2c77-ded4-442b-9b40-05f6c12345" } ] }, "success": true }pyeconet-0.1.28/src/pyeconet/example_responses/mqtt_running_reported.json000066400000000000000000000003051475412642500271350ustar00rootroot00000000000000{ "@RUNNING": "Running", "@HOTWATER": "ic_tank_fourty_percent.png", "transactionId": "WIFI_1.0_2020-05-30T14:17:45.282Z", "device_name": "74901882412345", "serial_number": "Q032012345" } pyeconet-0.1.28/src/pyeconet/example_responses/mqtt_temp_desired.json000066400000000000000000000002161475412642500262160ustar00rootroot00000000000000{ "transactionId": "ANDROID_2020-05-31T12:56:42", "device_name": "74901882412345", "serial_number": "Q032012345", "@SETPOINT": 126.0 }pyeconet-0.1.28/src/pyeconet/example_responses/user_auth.json000066400000000000000000000016241475412642500245100ustar00rootroot00000000000000{ "user_token": "QyrHnY-Fn7gTDfES12345", "user_id": "808dfae30bf412345", "options": { "account_id": "571092412345", "allow_email_notifications": false, "allow_product_alert_emails": false, "allow_product_alert_text_msg": false, "allow_push_notifications": true, "allow_special_offers_emails": false, "allow_special_offers_text_msg": false, "allow_text_notifications": true, "cb_service_account": false, "connected": true, "email": "email@email.com", "first_name": "", "is_phone_verified": true, "last_name": "", "phone_number": "5555555555", "receive_marketing_messages": false, "report_state": true, "role": 0, "share_status": 0, "success": true, "temperature_display_unit": "Fahrenheit", "user_id": "808dfae30bf412345" } }pyeconet-0.1.28/src/test.py000066400000000000000000000026641475412642500155720ustar00rootroot00000000000000import asyncio import logging import time import getpass from pyeconet import EcoNetApiInterface from pyeconet.equipment import EquipmentType from pyeconet.equipment.water_heater import WaterHeaterOperationMode logging.basicConfig() logging.getLogger().setLevel(logging.DEBUG) async def main(): email = input("Enter your email: ").strip() password = getpass.getpass(prompt='Enter your password: ') api = await EcoNetApiInterface.login(email, password=password) all_equipment = await api.get_equipment_by_type( [EquipmentType.WATER_HEATER, EquipmentType.THERMOSTAT] ) # api.subscribe() # await asyncio.sleep(5) for equip_list in all_equipment.values(): for equipment in equip_list: print(f"Name: {equipment.device_name}") # print(f"Set point: {equipment.set_point}") # print(f"Supports modes: {equipment._supports_modes()}") # print(f"Operation modes: {equipment.modes}") # print(f"Operation mode: {equipment.mode}") await equipment.get_energy_usage() print(f"{equipment.todays_energy_usage}") await equipment.get_water_usage() print(f"{equipment.todays_water_usage}") # equipment.set_set_point(equipment.set_point + 1) # equipment.set_mode(OperationMode.ELECTRIC_MODE) # await asyncio.sleep(300000) # api.unsubscribe() if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main())