pax_global_header00006660000000000000000000000064137561716420014526gustar00rootroot0000000000000052 comment=095e8928e606f4caf2613fa48f94fc829a7ecf34 shenxn-rpi-bad-power-095e892/000077500000000000000000000000001375617164200157665ustar00rootroot00000000000000shenxn-rpi-bad-power-095e892/.github/000077500000000000000000000000001375617164200173265ustar00rootroot00000000000000shenxn-rpi-bad-power-095e892/.github/workflows/000077500000000000000000000000001375617164200213635ustar00rootroot00000000000000shenxn-rpi-bad-power-095e892/.github/workflows/code-style.yml000066400000000000000000000010601375617164200241530ustar00rootroot00000000000000name: Code Style # yamllint disable-line rule:truthy on: push: branches: master pull_request: jobs: code-style: name: Code Style runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: 3.7 - name: Install dependencies run: | python -m pip install --upgrade pip pip install pre-commit - name: Run pre-commit run: | pre-commit run --all-files shenxn-rpi-bad-power-095e892/.github/workflows/codeql-analysis.yml000066400000000000000000000040201375617164200251720ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" # yamllint disable-line rule:truthy on: push: branches: master pull_request: # The branches below must be a subset of the branches above branches: master schedule: - cron: '29 1 * * 0' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: ['python'] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 shenxn-rpi-bad-power-095e892/.github/workflows/publish.yml000066400000000000000000000015051375617164200235550ustar00rootroot00000000000000name: Build and Publish # yamllint disable-line rule:truthy on: release: types: [created] push: branches: master pull_request: jobs: deploy: name: Build and Publish runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: 3.7 - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build run: | python setup.py sdist bdist_wheel - name: Publish if: ${{ github.event_name == 'release' }} # Only publish on release env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | twine upload dist/* shenxn-rpi-bad-power-095e892/.github/workflows/pytest.yml000066400000000000000000000016731375617164200234450ustar00rootroot00000000000000name: PyTest # yamllint disable-line rule:truthy on: push: branches: master pull_request: jobs: pytest: name: PyTest runs-on: ubuntu-latest strategy: matrix: python: ["3.7", "3.8", "3.9"] env: PYTHON: ${{ matrix.python }} steps: - name: Checkout uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest pytest-cov - name: PyTest run: | pytest --cov=./rpi_bad_power --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: file: ./coverage.xml env_vars: PYTHON fail_ci_if_error: true path_to_write_report: ./codecov_report.gz verbose: true shenxn-rpi-bad-power-095e892/.gitignore000066400000000000000000000034201375617164200177550ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # 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/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ .vscode shenxn-rpi-bad-power-095e892/.pre-commit-config.yaml000066400000000000000000000034361375617164200222550ustar00rootroot00000000000000repos: - repo: https://github.com/asottile/pyupgrade rev: v2.7.2 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black rev: 20.8b1 hooks: - id: black args: - --safe - --quiet files: ^.*\.py$ - repo: https://github.com/codespell-project/codespell rev: v1.17.1 hooks: - id: codespell args: - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - pydocstyle==5.1.1 files: ^.*\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.6.2 hooks: - id: bandit args: - --quiet - --format=custom files: ^rpi_bad_power.*\.py$ - repo: https://github.com/PyCQA/isort rev: 5.5.3 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.2.0 hooks: - id: check-executables-have-shebangs stages: [manual] - repo: https://github.com/adrienverge/yamllint.git rev: v1.24.2 hooks: - id: yamllint - repo: https://github.com/prettier/prettier rev: 2.0.4 hooks: - id: prettier stages: [manual] - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.790 hooks: - id: mypy - repo: https://github.com/pre-commit/mirrors-pylint rev: v2.6.0 hooks: - id: pylint additional_dependencies: [pylint-strict-informational] shenxn-rpi-bad-power-095e892/.yamllint000066400000000000000000000022011375617164200176130ustar00rootroot00000000000000rules: braces: level: error min-spaces-inside: 0 max-spaces-inside: 1 min-spaces-inside-empty: -1 max-spaces-inside-empty: -1 brackets: level: error min-spaces-inside: 0 max-spaces-inside: 0 min-spaces-inside-empty: -1 max-spaces-inside-empty: -1 colons: level: error max-spaces-before: 0 max-spaces-after: 1 commas: level: error max-spaces-before: 0 min-spaces-after: 1 max-spaces-after: 1 comments: level: error require-starting-space: true min-spaces-from-content: 2 comments-indentation: level: error document-end: level: error present: false document-start: level: error present: false empty-lines: level: error max: 1 max-start: 0 max-end: 1 hyphens: level: error max-spaces-after: 1 indentation: level: error spaces: 2 indent-sequences: true check-multi-line-strings: false key-duplicates: level: error line-length: disable new-line-at-end-of-file: level: error new-lines: level: error type: unix trailing-spaces: level: error truthy: level: error shenxn-rpi-bad-power-095e892/LICENSE000066400000000000000000000020551375617164200167750ustar00rootroot00000000000000MIT License Copyright (c) 2020 Xiaonan Shen 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. shenxn-rpi-bad-power-095e892/README.md000066400000000000000000000022731375617164200172510ustar00rootroot00000000000000# rpi-bad-power A Python library to detect bad power supply on Raspberry Pi. This library is mainly built for the [Raspberry Pi Power Supply Checker](https://www.home-assistant.io/integrations/rpi_power/) integration of [HomeAssistant](https://github.com/home-assistant/core). It should also work for other purpose. ## Compatibility This library only works on kernel 4.14+. It supports getting the under voltage bit from different entries. Related PRs: - [raspberrypi/linux#2397](https://github.com/raspberrypi/linux/pull/2397): `/sys/devices/platform/soc/soc:firmware/get_trottled` - [raspberrypi/linux#2706](https://github.com/raspberrypi/linux/pull/2706): `/sys/class/hwmon/hwmon0/in0_lcrit_alarm` ## Usage Here is an example on how to use this library. ```python from rpi_bad_power import new_under_voltage under_voltage = new_under_voltage() if under_voltage is None: print("System not supported.") elif under_voltage.get(): print("Under voltage detected.") else: print("Voltage is normal.") ``` ## Credits Some of the code are based on [custom-components/sensor.rpi_power](https://github.com/custom-components/sensor.rpi_power) maintained by [@swetoast](https://github.com/swetoast). shenxn-rpi-bad-power-095e892/pyproject.toml000066400000000000000000000051471375617164200207110ustar00rootroot00000000000000[build-system] requires = [ "setuptools >= 40.9.0", "wheel", ] build-backend = "setuptools.build_meta" [tool.black] target-version = ["py37", "py38"] exclude = 'generated' [tool.isort] # https://github.com/PyCQA/isort/wiki/isort-Settings profile = "black" # will group `import x` and `from x import` of the same module. force_sort_within_sections = true known_first_party = [ "homeassistant", "tests", ] forced_separate = [ "tests", ] combine_as_imports = true [tool.pylint.MASTER] ignore = [ "tests", ] # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. # Disabled for now: https://github.com/PyCQA/pylint/issues/3584 #jobs = 2 load-plugins = [ "pylint_strict_informational", ] persistent = false extension-pkg-whitelist = [ "ciso8601", "cv2", ] [tool.pylint.BASIC] good-names = [ "_", "ev", "ex", "fp", "i", "id", "j", "k", "Run", "T", ] [tool.pylint."MESSAGES CONTROL"] # Reasons disabled: # format - handled by black # locally-disabled - it spams too much # duplicate-code - unavoidable # cyclic-import - doesn't test if both import on load # abstract-class-little-used - prevents from setting right foundation # unused-argument - generic callbacks and setup methods create a lot of warnings # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise # too-many-ancestors - it's too strict. # wrong-import-order - isort guards this disable = [ "format", "abstract-class-little-used", "abstract-method", "cyclic-import", "duplicate-code", "inconsistent-return-statements", "locally-disabled", "not-context-manager", "too-few-public-methods", "too-many-ancestors", "too-many-arguments", "too-many-branches", "too-many-instance-attributes", "too-many-lines", "too-many-locals", "too-many-public-methods", "too-many-return-statements", "too-many-statements", "too-many-boolean-expressions", "unused-argument", "wrong-import-order", ] enable = [ #"useless-suppression", # temporarily every now and then to clean them up "use-symbolic-message-instead", ] [tool.pylint.REPORTS] score = false [tool.pylint.TYPECHECK] ignored-classes = [ "_CountingAttr", # for attrs ] [tool.pylint.FORMAT] expected-line-ending-format = "LF" [tool.pylint.EXCEPTIONS] overgeneral-exceptions = [ "BaseException", "Exception", "HomeAssistantError", ] shenxn-rpi-bad-power-095e892/rpi_bad_power/000077500000000000000000000000001375617164200206025ustar00rootroot00000000000000shenxn-rpi-bad-power-095e892/rpi_bad_power/__init__.py000066400000000000000000000044511375617164200227170ustar00rootroot00000000000000""" A library reading under voltage bit from the official Raspberry Pi Kernel. Minimal Kernel needed is 4.14+ """ import logging import os from typing import Optional, Text _LOGGER = logging.getLogger(__name__) HWMON_NAME = "rpi_volt" SYSFILE_HWMON_DIR = "/sys/class/hwmon" SYSFILE_HWMON_FILE = "in0_lcrit_alarm" SYSFILE_LEGACY = "/sys/devices/platform/soc/soc:firmware/get_throttled" UNDERVOLTAGE_STICKY_BIT = 1 << 16 def get_rpi_volt_hwmon() -> Optional[Text]: """Find rpi_volt hwmon device.""" try: hwmons = os.listdir(SYSFILE_HWMON_DIR) except FileNotFoundError: return None for hwmon in hwmons: name_file = os.path.join(SYSFILE_HWMON_DIR, hwmon, "name") if os.path.isfile(name_file): with open(name_file) as file: hwmon_name = file.read().strip() if hwmon_name == HWMON_NAME: return os.path.join(SYSFILE_HWMON_DIR, hwmon) return None class UnderVoltage: """Read under voltage status.""" def get(self) -> bool: """Get under voltage status.""" class UnderVoltageNew(UnderVoltage): """Read under voltage status from new entry.""" def __init__(self, hwmon: Text): """Initialize the under voltage class.""" self._hwmon = hwmon def get(self) -> bool: """Get under voltage status.""" # Use new hwmon entry with open(os.path.join(self._hwmon, SYSFILE_HWMON_FILE)) as file: bit = file.read()[:-1] _LOGGER.debug("Get under voltage status: %s", bit) return bit == "1" class UnderVoltageLegacy(UnderVoltage): """Read under voltage status from legacy entry.""" def get(self) -> bool: """Get under voltage status.""" # Using legacy get_throttled entry with open(SYSFILE_LEGACY) as file: throttled = file.read()[:-1] _LOGGER.debug("Get throttled value: %s", throttled) return ( int(throttled, base=16) & UNDERVOLTAGE_STICKY_BIT == UNDERVOLTAGE_STICKY_BIT ) def new_under_voltage() -> Optional[UnderVoltage]: """Create new UnderVoltage object.""" hwmon = get_rpi_volt_hwmon() if hwmon: return UnderVoltageNew(hwmon) if os.path.isfile(SYSFILE_LEGACY): # support older kernel return UnderVoltageLegacy() return None shenxn-rpi-bad-power-095e892/setup.cfg000066400000000000000000000025471375617164200176170ustar00rootroot00000000000000[metadata] name = rpi-bad-power version = 0.1.0 author = Xiaonan Shen author_email = s@sxn.dev license = MIT License license_file = LICENSE platforms = any description = A Python library to detect bad power supply on Raspberry Pi . long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/shenxn/rpi-bad-power keywords = rpi, raspberry-pi classifier = Programming Language :: Python :: 3 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: POSIX :: Linux Programming Language :: Python :: 3.7 Topic :: Software Development :: Embedded Systems [options] packages = find: [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build doctests = True # To work with Black # E501: line too long # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # D202 No blank lines allowed after function docstring # W504 line break after binary operator ignore = E501, W503, E203, D202, W504 [mypy] python_version = 3.7 show_error_codes = true follow_imports = silent ignore_missing_imports = true warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true strict = true ignore_errors = false warn_unreachable = true # TODO: turn these off, address issues allow_any_generics = true implicit_reexport = true shenxn-rpi-bad-power-095e892/setup.py000066400000000000000000000000721375617164200174770ustar00rootroot00000000000000"""Setup script.""" import setuptools setuptools.setup() shenxn-rpi-bad-power-095e892/test_rpi_bad_power.py000066400000000000000000000136761375617164200222300ustar00rootroot00000000000000"""Testcases for rpi bad power.""" from typing import Any, List, Mapping, Text from unittest.mock import MagicMock, patch from rpi_bad_power import ( SYSFILE_HWMON_DIR, SYSFILE_LEGACY, UnderVoltageLegacy, UnderVoltageNew, new_under_voltage, ) class MockFile: """Mocked file.""" def __init__(self, content: Text): """Initialize the mocked file.""" self.read = MagicMock(return_value=f"{content}\n") self.close = MagicMock() class PatchFile: """Patch a file.""" def __init__(self, content: Text): """Initialize the mocked file.""" self.file = MockFile(content) def __enter__(self) -> MockFile: """Enter a with statement doing nothing.""" return self.file def __exit__(self, *exc_info: Any) -> bool: """Exit a with statement by closing the file.""" self.file.close() return True class MockSysFiles: """Mocking the system files.""" def __init__( self, multi: bool, new: bool, legacy: bool, no_hwmon: bool, is_under_voltage: bool, ): """Initialize the mocked system files.""" self.multi = multi # multiple entries self.new = new # single new entry self.legacy = legacy # legacy entry self.no_hwmon = no_hwmon # hwmon dir does not exist self.is_under_voltage = is_under_voltage self.listdir = MagicMock(side_effect=self._listdir) self.open = MagicMock(side_effect=self._open) self.isfile = MagicMock(side_effect=self._isfile) self.opened_files: List[MockFile] = [] self.files: Mapping[Text, Text] = {} if multi: self.files[f"{SYSFILE_HWMON_DIR}/hwmon0/name"] = "cpu_thermal" self.files[f"{SYSFILE_HWMON_DIR}/hwmon2/name"] = "rpi_volt" self.files[f"{SYSFILE_HWMON_DIR}/hwmon2/in0_lcrit_alarm"] = ( "1" if is_under_voltage else "0" ) elif new: self.files[f"{SYSFILE_HWMON_DIR}/hwmon0/name"] = "rpi_volt" self.files[f"{SYSFILE_HWMON_DIR}/hwmon0/in0_lcrit_alarm"] = ( "1" if is_under_voltage else "0" ) elif legacy: self.files[SYSFILE_LEGACY] = "50005" if self.is_under_voltage else "0" def _listdir(self, path: Text) -> List[Text]: assert path == SYSFILE_HWMON_DIR if self.no_hwmon: raise FileNotFoundError() if self.multi: return ["hwmon0", "hwmon1", "hwmon2"] if self.new: return ["hwmon0"] return [] def _open(self, path: Text) -> PatchFile: try: patch_file = PatchFile(self.files[path]) except KeyError: raise FileNotFoundError() # pylint: disable=raise-missing-from self.opened_files.append(patch_file.file) return patch_file def _isfile(self, path: Text) -> bool: return path in self.files def assert_all_files_closed(self) -> None: """Assert all opened files are closed.""" for file in self.opened_files: file.close.assert_called_once() class PatchSysFiles: """Patch the system files.""" def __init__( self, multi: bool = False, new: bool = False, legacy: bool = False, no_hwmon: bool = False, is_under_voltage: bool = False, ): """Initialize the patch helper class.""" self.mock = MockSysFiles(multi, new, legacy, no_hwmon, is_under_voltage) self.listdir_patch = patch("rpi_bad_power.os.listdir", self.mock.listdir) self.open_patch = patch("rpi_bad_power.open", self.mock.open) self.isfile_patch = patch("rpi_bad_power.os.path.isfile", self.mock.isfile) def __enter__(self) -> MockSysFiles: """Enter the with statement by patching functions.""" self.listdir_patch.__enter__() self.open_patch.__enter__() self.isfile_patch.__enter__() return self.mock def __exit__(self, *exc_info: Any) -> bool: """Exit the with statement by un-patching functions.""" if exc_info[0] is not None: return False self.listdir_patch.__exit__(*exc_info) self.open_patch.__exit__(*exc_info) self.isfile_patch.__exit__(*exc_info) self.mock.assert_all_files_closed() return True def test_non_rpi() -> None: """Test running on non raspberry pi environment.""" with PatchSysFiles() as mock_sys_files: assert new_under_voltage() is None mock_sys_files.listdir.assert_called_once_with(SYSFILE_HWMON_DIR) def test_no_hwmon() -> None: """Test running on a system without hwmon directory.""" with PatchSysFiles(no_hwmon=True): assert new_under_voltage() is None def test_multi() -> None: """Test running on a rpi kernel with multiple hwmon entries.""" with PatchSysFiles(True): under_voltage = new_under_voltage() assert isinstance(under_voltage, UnderVoltageNew) assert under_voltage.get() is False with PatchSysFiles(True, is_under_voltage=True): assert new_under_voltage().get() is True # type: ignore def test_new() -> None: """Test running on a rpi kernel with one new hwmon entry.""" with PatchSysFiles(new=True): under_voltage = new_under_voltage() assert isinstance(under_voltage, UnderVoltageNew) assert under_voltage.get() is False with PatchSysFiles(new=True, is_under_voltage=True): assert new_under_voltage().get() is True # type: ignore def test_legacy() -> None: """Test running on a legacy rpi kernel.""" with PatchSysFiles(legacy=True) as mock_sys_files: under_voltage = new_under_voltage() assert isinstance(under_voltage, UnderVoltageLegacy) assert under_voltage.get() is False mock_sys_files.isfile.assert_called_once_with(SYSFILE_LEGACY) with PatchSysFiles(legacy=True, is_under_voltage=True): assert new_under_voltage().get() is True # type: ignore