diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 36d6d2a..0000000 --- a/.flake8 +++ /dev/null @@ -1,16 +0,0 @@ -[flake8] -exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build -doctests = True -# To work with Black -max-line-length = 88 -# 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 \ No newline at end of file diff --git a/.github/workflows/pull_request_format.yaml b/.github/workflows/pull_request_format.yaml deleted file mode 100644 index bb596ee..0000000 --- a/.github/workflows/pull_request_format.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: Check formatting - -on: - pull_request: - -jobs: - style: - runs-on: ubuntu-latest - name: Check formatting - steps: - - name: Pull - uses: actions/checkout@v2 - - name: Python - uses: actions/setup-python@v1 - with: - python-version: 3.x - - name: Black - run: python3 -m pip install black - - name: Check - run: black . --check diff --git a/.github/workflows/push_format.yaml b/.github/workflows/push_format.yaml deleted file mode 100644 index 973ff1a..0000000 --- a/.github/workflows/push_format.yaml +++ /dev/null @@ -1,45 +0,0 @@ -name: Format - -on: - push: - branches: - - master - - main - -jobs: - format: - name: Format with black and isort - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Cache - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: pip-format - - name: Install dependencies - run: | - python -m pip install --upgrade pip wheel - python -m pip install --upgrade black isort - - name: Pull again - run: git pull || true - - name: Run formatting - run: | - python -m isort -v --multi-line 3 --trailing-comma -l 88 --recursive . - python -m black -v . - - name: Commit files - run: | - if [ $(git diff HEAD | wc -l) -gt 30 ] - then - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config user.name "GitHub Actions" - git commit -m "Run formatting" -a || true - git push || true - fi diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 24cd0be..06c5e2b 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -9,7 +9,7 @@ jobs: name: HACS runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Validate uses: hacs/action@main with: @@ -19,45 +19,6 @@ jobs: name: Hassfest runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Validate uses: home-assistant/actions/hassfest@master - test_advice_flake8: - name: Test + advice with flake8 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Cache - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: pip-flake8 - - name: Install dependencies - run: | - python -m pip install --upgrade pip wheel - python -m pip install --upgrade flake8 wemake-python-styleguide - - name: Lint with flake8 - run: | - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - - name: Don't mind this - run: | - flake8 . --inline-quotes '"' --count --exit-zero --max-complexity=15 --max-line-length=90 --statistics --select=I,P,WPS305,C812,E203,W503,E800 - - name: Docstrings - run: | - flake8 . --inline-quotes '"' --count --exit-zero --max-complexity=15 --max-line-length=90 --statistics --select=D,DAR - - name: Unused stuff - run: | - echo "Some stuff may not be used, but is used in commented out code." - echo "Make sure you check with the find command before you remove anything!" - flake8 . --inline-quotes '"' --count --exit-zero --max-complexity=15 --max-line-length=90 --statistics --select=F - echo "Some stuff may not be used, but is used in commented out code." - echo "Make sure you check with the find command before you remove anything!" - - name: General stats - run: | - flake8 . --inline-quotes '"' --count --exit-zero --max-complexity=15 --max-line-length=90 --statistics --ignore=I,P,WPS305,C812,E203,W503,E800,D,DAR,F diff --git a/custom_components/lutron_caseta_pro/__init__.py b/custom_components/lutron_caseta_pro/__init__.py index 742ae46..b5acb26 100644 --- a/custom_components/lutron_caseta_pro/__init__.py +++ b/custom_components/lutron_caseta_pro/__init__.py @@ -167,7 +167,7 @@ async def async_setup_bridge(hass, config, fname, bridge): """Initialize a bridge by loading its integration report.""" _LOGGER.debug("Setting up bridge using Integration Report %s", fname) - devices = await casetify.async_load_integration_report(fname) + devices = await hass.async_add_executor_job(casetify.load_integration_report, fname) # Patch up device types from configuration. # All other devices will be treated as lights. @@ -200,7 +200,7 @@ async def async_setup_bridge(hass, config, fname, bridge): for device_type in types: component = device_type _LOGGER.debug("Loading platform %s", component) - hass.async_add_job( + hass.async_create_task( discovery.async_load_platform( hass, component, @@ -416,9 +416,7 @@ def name(self): def unique_id(self) -> str: """Return a unique ID.""" if self._mac is not None: - return "{}_{}_{}_{}".format( - DOMAIN, self._platform_domain, self._mac, self._integration - ) + return f"{DOMAIN}_{self._platform_domain}_{self._mac}_{self._integration}" return None diff --git a/custom_components/lutron_caseta_pro/casetify.py b/custom_components/lutron_caseta_pro/casetify.py index cbb5a40..e73c42b 100644 --- a/custom_components/lutron_caseta_pro/casetify.py +++ b/custom_components/lutron_caseta_pro/casetify.py @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_load_integration_report(fname): +def load_integration_report(fname): """Process a JSON integration report and return a list of devices. Each returned device will have an 'id', 'name', 'type' and optionally diff --git a/custom_components/lutron_caseta_pro/cover.py b/custom_components/lutron_caseta_pro/cover.py index 6ee971d..f99d541 100644 --- a/custom_components/lutron_caseta_pro/cover.py +++ b/custom_components/lutron_caseta_pro/cover.py @@ -3,16 +3,15 @@ Provides shade functionality for Home Assistant. """ + import logging +from typing import Any from homeassistant.components.cover import ( ATTR_POSITION, DOMAIN, - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, - SUPPORT_STOP, CoverEntity, + CoverEntityFeature, ) from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_ID, CONF_MAC, CONF_NAME @@ -25,6 +24,13 @@ CasetaEntity, ) +COVER_SUPPORT = ( + CoverEntityFeature.CLOSE + | CoverEntityFeature.OPEN + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.STOP +) + _LOGGER = logging.getLogger(__name__) @@ -102,9 +108,14 @@ def current_cover_position(self): """Return current position of the cover.""" return self._position - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any): """Open the cover.""" - # Rasing must be used for STOP to work + _LOGGER.debug( + "Writing cover OUTPUT value: %d %d", + self._integration, + Caseta.Action.RAISING, + ) + # Raising must be used for STOP to work await self._data.caseta.write( Caseta.OUTPUT, self._integration, Caseta.Action.RAISING, None ) @@ -114,8 +125,13 @@ async def async_open_cover(self, **kwargs): # will not do this on a Caseta.Action.RAISING self.update_state(100) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any): """Close the cover.""" + _LOGGER.debug( + "Writing cover OUTPUT value: %d %d", + self._integration, + Caseta.Action.LOWERING, + ) # Lowering must be used for STOP to work await self._data.caseta.write( Caseta.OUTPUT, self._integration, Caseta.Action.LOWERING, None @@ -126,7 +142,7 @@ async def async_close_cover(self, **kwargs): # will not do this on a Caseta.Action.LOWERING self.update_state(0) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any): """Move the cover to a specific position.""" if ATTR_POSITION in kwargs: position = kwargs[ATTR_POSITION] @@ -139,6 +155,14 @@ async def async_set_cover_position(self, **kwargs): "Tried to set cover position to greater than maximum value 100." ) position = 100 + _LOGGER.debug( + "Writing cover OUTPUT value: %d %d %d %d %d", + self._integration, + Caseta.Action.SET, + position, + 0, + 0, + ) # Parameters are Level, Fade, Delay # Fade is ignored and Delay set to 0 await self._data.caseta.write( @@ -146,11 +170,11 @@ async def async_set_cover_position(self, **kwargs): ) @property - def supported_features(self): + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + return COVER_SUPPORT - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any): """Stop raising or lowering the shade.""" await self._data.caseta.write( Caseta.OUTPUT, self._integration, Caseta.Action.STOP, None diff --git a/custom_components/lutron_caseta_pro/fan.py b/custom_components/lutron_caseta_pro/fan.py index 2815718..65b9dda 100644 --- a/custom_components/lutron_caseta_pro/fan.py +++ b/custom_components/lutron_caseta_pro/fan.py @@ -3,9 +3,11 @@ Provides fan functionality for Home Assistant. """ + import logging +from typing import Any -from homeassistant.components.fan import DOMAIN, SUPPORT_SET_SPEED, FanEntity +from homeassistant.components.fan import DOMAIN, FanEntity, FanEntityFeature from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_ID, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -20,6 +22,10 @@ CasetaEntity, ) +FAN_SUPPORT = ( + FanEntityFeature.SET_SPEED | FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON +) + _LOGGER = logging.getLogger(__name__) @@ -93,14 +99,14 @@ def is_on(self): return self._percentage and self._percentage > 0 @property - def percentage(self) -> int: + def percentage(self) -> int | None: """Return the current speed.""" return self._percentage @property - def supported_features(self) -> int: + def supported_features(self) -> FanEntityFeature: """Flag supported features.""" - return SUPPORT_SET_SPEED + return FAN_SUPPORT @property def speed_count(self) -> int: @@ -109,10 +115,9 @@ def speed_count(self) -> int: async def async_turn_on( self, - speed: str = None, - percentage: int = None, - preset_mode: str = None, - **kwargs + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Instruct the fan to turn on.""" if percentage is None: @@ -135,7 +140,7 @@ async def async_set_percentage(self, percentage: int) -> None: self._percentage = percentage self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the fan to turn off.""" await self.async_set_percentage(0) diff --git a/custom_components/lutron_caseta_pro/light.py b/custom_components/lutron_caseta_pro/light.py index 87b0e98..05f088e 100644 --- a/custom_components/lutron_caseta_pro/light.py +++ b/custom_components/lutron_caseta_pro/light.py @@ -3,15 +3,17 @@ Provides dimmable light functionality for Home Assistant. """ + import logging +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, DOMAIN, - SUPPORT_BRIGHTNESS, - SUPPORT_TRANSITION, + ColorMode, LightEntity, + LightEntityFeature, ) from homeassistant.const import ( CONF_DEVICES, @@ -77,14 +79,12 @@ def _format_transition(transition) -> str: transition = _MAX_TRANSITION if transition < 60: # format to two decimals for less than 60 seconds - transition = "{:0>.2f}".format(transition) + transition = f"{transition:0>.2f}" else: # else format HH:MM:SS minutes, seconds = divmod(transition, 60) hours, minutes = divmod(minutes, 60) - transition = "{:0>2d}:{:0>2d}:{:0>2d}".format( - int(hours), int(minutes), int(seconds) - ) + transition = f"{int(hours):0>2d}:{int(minutes):0>2d}:{int(seconds):0>2d}" return transition @@ -108,8 +108,13 @@ def __init__(self, light, data, mac, transition): self._mac = mac self._default_transition = transition self._platform_domain = DOMAIN + self._attr_supported_features = ( + LightEntityFeature.TRANSITION if self._is_dimmer else 0 + ) + self._color_mode = ColorMode.BRIGHTNESS + self._color_modes = {self._color_mode} - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Update initial state.""" await self.query() @@ -128,27 +133,37 @@ def extra_state_attributes(self): return attr @property - def brightness(self): + def brightness(self) -> int | None: """Brightness of the light (an integer in the range 1-255).""" return int((self._brightness / 100) * 255) + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + return self._color_mode + @property def is_on(self): """Return true if light is on.""" return self._is_on @property - def supported_features(self): + def supported_features(self) -> int | None: """Flag supported features.""" - return (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) if self._is_dimmer else 0 + return self._attr_supported_features + + @property + def supported_color_modes(self) -> set[ColorMode]: + """Flag supported color modes.""" + return self._color_modes - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" value = 100 transition = None if self._is_dimmer: if ATTR_BRIGHTNESS in kwargs: - value = "{:0>.2f}".format((kwargs[ATTR_BRIGHTNESS] / 255) * 100) + value = f"{(kwargs[ATTR_BRIGHTNESS] / 255) * 100:0>.2f}" if ATTR_TRANSITION in kwargs: transition = _format_transition(float(kwargs[ATTR_TRANSITION])) elif self._default_transition is not None: @@ -164,7 +179,7 @@ async def async_turn_on(self, **kwargs): Caseta.OUTPUT, self._integration, Caseta.Action.SET, value, transition ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" transition = None if self._is_dimmer: @@ -182,7 +197,7 @@ async def async_turn_off(self, **kwargs): Caseta.OUTPUT, self._integration, Caseta.Action.SET, 0, transition ) - def update_state(self, brightness): + def update_state(self, brightness: int) -> None: """Update brightness value.""" if self._is_dimmer: self._brightness = brightness diff --git a/custom_components/lutron_caseta_pro/manifest.json b/custom_components/lutron_caseta_pro/manifest.json index b8ab17e..0329dd7 100644 --- a/custom_components/lutron_caseta_pro/manifest.json +++ b/custom_components/lutron_caseta_pro/manifest.json @@ -1,11 +1,12 @@ { "domain": "lutron_caseta_pro", "name": "Lutron Caséta Smart Bridge PRO / RA2 Select", + "codeowners": ["@upsert"], + "dependencies": ["configurator"], "documentation": "https://github.com/upsert/lutron-caseta-pro", + "integration_type": "hub", + "iot_class": "local_push", "issue_tracker": "https://github.com/upsert/lutron-caseta-pro/issues", - "dependencies": ["configurator"], - "codeowners": ["@upsert"], "requirements": [], - "version": "2022.2", - "iot_class": "local_push" + "version": "2025.1" } diff --git a/custom_components/lutron_caseta_pro/scene.py b/custom_components/lutron_caseta_pro/scene.py index eb7b37e..b5a4c80 100644 --- a/custom_components/lutron_caseta_pro/scene.py +++ b/custom_components/lutron_caseta_pro/scene.py @@ -3,14 +3,14 @@ Provides access to the scenes defined in Lutron system. """ + import logging from homeassistant.components.scene import DOMAIN, Scene from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_ID, CONF_MAC, CONF_NAME -from . import ATTR_SCENE_ID, CONF_SCENE_ID +from . import ATTR_SCENE_ID, CONF_SCENE_ID, Caseta, CasetaData, CasetaEntity from . import DOMAIN as COMPONENT_DOMAIN -from . import Caseta, CasetaData, CasetaEntity _LOGGER = logging.getLogger(__name__) @@ -82,9 +82,7 @@ def scene_id(self): def unique_id(self) -> str: """Return a unique ID.""" if self._mac is not None: - return "{}_{}_{}_{}_{}".format( - COMPONENT_DOMAIN, DOMAIN, self._mac, self._integration, self._scene_id - ) + return f"{COMPONENT_DOMAIN}_{DOMAIN}_{self._mac}_{self._integration}_{self._scene_id}" return None @property diff --git a/custom_components/lutron_caseta_pro/sensor.py b/custom_components/lutron_caseta_pro/sensor.py index 1d262da..b27b151 100755 --- a/custom_components/lutron_caseta_pro/sensor.py +++ b/custom_components/lutron_caseta_pro/sensor.py @@ -4,6 +4,7 @@ Provides a sensor for each Pico remote with a value that changes depending on the button press. """ + import logging from homeassistant.components.sensor import DOMAIN diff --git a/custom_components/lutron_caseta_pro/switch.py b/custom_components/lutron_caseta_pro/switch.py index f9d7ce5..e39d086 100644 --- a/custom_components/lutron_caseta_pro/switch.py +++ b/custom_components/lutron_caseta_pro/switch.py @@ -3,6 +3,7 @@ Provides switch functionality for Home Assistant. """ + import logging from homeassistant.components.switch import DOMAIN, SwitchEntity diff --git a/hacs.json b/hacs.json index 797c93d..958d9c7 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,4 @@ { "name": "Lutron Caséta Smart Bridge PRO/RA2 Select", - "domains": [ "cover", "fan", "light", "scene", "sensor", "switch" ], - "homeassistant": "0.110" + "homeassistant": "2025.1.0" } diff --git a/pylintrc b/pylintrc deleted file mode 100755 index 125062c..0000000 --- a/pylintrc +++ /dev/null @@ -1,71 +0,0 @@ -[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. -jobs=2 -load-plugins=pylint_strict_informational -persistent=no -extension-pkg-whitelist=ciso8601 - -[BASIC] -good-names=id,i,j,k,ex,Run,_,fp - -[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 -# global-statement - used for the on-demand requirement installation -# redefined-variable-type - this is Python, we're duck typing! -# 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 -# unnecessary-pass - readability for functions which only contain pass -# import-outside-toplevel - TODO -# 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, - global-statement, - import-outside-toplevel, - inconsistent-return-statements, - locally-disabled, - not-context-manager, - redefined-variable-type, - 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, - unnecessary-pass, - unused-argument, - wrong-import-order -enable= - use-symbolic-message-instead - -[REPORTS] -score=no - -[TYPECHECK] -# For attrs -ignored-classes=_CountingAttr - -[FORMAT] -expected-line-ending-format=LF - -[EXCEPTIONS] -overgeneral-exceptions=BaseException,Exception,HomeAssistantError