Skip to content

Commit

Permalink
Merge pull request #181 from xaviml/refactor/general
Browse files Browse the repository at this point in the history
Refactor/general
  • Loading branch information
xaviml authored Nov 21, 2020
2 parents 0ec87ff + af99d3f commit 27b46f0
Show file tree
Hide file tree
Showing 61 changed files with 1,310 additions and 992 deletions.
7 changes: 0 additions & 7 deletions .coveragerc

This file was deleted.

3 changes: 0 additions & 3 deletions .flake8

This file was deleted.

3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ dmypy.json
.pyre/

#VSCode
.vscode/
.vscode/*
!.vscode/settings.json
.idea

# Ignoring Pipfile.lock since we support python 3.6, 3.7 and 3.8
Expand Down
39 changes: 18 additions & 21 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: check-json
- id: pretty-format-json
args:
- --autofix
- --indent
- '4'
- repo: local
hooks:
- id: isort
name: isort
entry: pipenv run isort
language: python
types: [python]
args: []
require_serial: false
- id: black
name: black
entry: pipenv run black apps/controllerx tests
language: system
pass_filenames: false
always_run: true
entry: pipenv run black
language: python
types: [python]
- id: flake8
name: flake8
entry: pipenv run flake8 apps/controllerx tests
language: system
pass_filenames: false
always_run: true
entry: pipenv run flake8
language: python
types: [python]
- id: mypy
name: mypy
entry: pipenv run mypy apps/controllerx
language: system
entry: pipenv run mypy apps/controllerx/ tests/
language: python
types: [python]
pass_filenames: false
always_run: true
- id: pytest
name: pytest
entry: pipenv run pytest
language: system
language: python
types: [python]
pass_filenames: false
always_run: true
22 changes: 22 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"files.autoSave": "afterDelay",
"files.autoSaveDelay": 100,
"python.testing.pytestEnabled": true,
"editor.formatOnSave": true,
"python.formatting.provider": "black",
"python.analysis.typeCheckingMode": "basic",
"python.analysis.diagnosticMode": "workspace",
"python.analysis.stubPath": "apps/controllerx",
"python.testing.pytestArgs": [
"tests"
],
"python.languageServer": "Pylance",
"python.linting.mypyEnabled": true,
"python.linting.mypyCategorySeverity.note": "Error",
"python.linting.flake8Enabled": true,
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
}
36 changes: 26 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,57 @@ New controllers need to be added into the `apps/controllerx/devices/` and you wi

Note that this project will only accept the mapping that the original controller would follow with its original hub.

## Imports

Run the following to fix imports order:

```shell
pipenv run isort apps/controllerx/ tests/
```

## Format

Run the following to fix formatting:

```shell
pipenv run black apps/controllerx/ tests/
```

## Typing

Run the following to check consistency in the typings:

```
pipenv run mypy apps/controllerx
```shell
pipenv run mypy apps/controllerx/ tests/
```

## Linting

Run the following to check for stylings:

```
pipenv run flake8 apps/controllerx
```shell
pipenv run flake8 apps/controllerx/ tests/
```

## Test

Run the following command for the tests:

```
```shell
pipenv run pytest --cov=apps
```

or the following to get a report of the missing lines to be tested:

```
```shell
pytest --cov-report term-missing --cov=apps
```

## Pre-commit

Once you have the code ready, pre-commit will run some checks to make sure the code follows the format and the tests did not break. If you want to run the check for all files at any point, run:

```
```shell
pipenv run pre-commit run --all-files
```

Expand All @@ -64,7 +80,7 @@ You can use the tool `commitizen` to commit based in a standard. If you are in t

[Install Jekyll](https://jekyllrb.com/docs/) and run the documentation locally with:

```
```shell
cd docs
bundle install
bundle exec jekyll serve
Expand Down Expand Up @@ -98,13 +114,13 @@ git checkout -b <username>-<remote-branch> <username>/<remote-branch>

Thanks to the Azure Pipelines, we are able to deploy by just creating a new tag on git. So first, we will need to bump version with `commitizen` by running the following line in the `master` branch:

```
```shell
cz bump --no-verify
```

`--prerelease beta` tag can be added to create a pre-release. Note that you can also add `--dry-run` to see which version will bump without commiting anything. Then, we can directly push the tags:

```
```shell
git push origin master --tags
```

Expand Down
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pre-commit = "==2.8.2"
commitizen = "==2.8.0"
mypy = "==0.790"
flake8 = "==3.8.4"
isort = "==5.6.4"
controllerx = {path = ".", editable = true}

[packages]
Expand Down
1 change: 0 additions & 1 deletion apps/controllerx/cx_const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import Any, Awaitable, Callable, Dict, Tuple, Union


ActionFunction = Callable[..., Awaitable[Any]]
TypeAction = Union[ActionFunction, Tuple, str]
ActionEvent = Union[str, int]
Expand Down
61 changes: 10 additions & 51 deletions apps/controllerx/cx_core/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
Awaitable,
Callable,
Counter,
DefaultDict,
Dict,
List,
Optional,
Expand All @@ -24,13 +23,13 @@
from appdaemon.plugins.hass.hassapi import Hass # type: ignore
from appdaemon.plugins.mqtt.mqttapi import Mqtt # type: ignore
from cx_const import ActionEvent, ActionFunction, TypeAction, TypeActionsMapping

from cx_core import integration as integration_module
from cx_core.integration import Integration
from cx_core.integration import EventData, Integration

Service = Tuple[str, Dict]
Services = List[Service]


DEFAULT_DELAY = 350 # In milliseconds
DEFAULT_ACTION_DELTA = 300 # In milliseconds
DEFAULT_MULTIPLE_CLICK_DELAY = 500 # In milliseconds
Expand Down Expand Up @@ -118,15 +117,13 @@ async def initialize(self) -> None:
self.multiple_click_delay: int = self.args.get(
"multiple_click_delay", DEFAULT_MULTIPLE_CLICK_DELAY
)
self.action_times: DefaultDict[str, float] = defaultdict(lambda: 0.0)
self.multiple_click_action_times: DefaultDict[str, float] = defaultdict(
lambda: 0.0
)
self.action_times: Dict[str, float] = defaultdict(lambda: 0.0)
self.multiple_click_action_times: Dict[str, float] = defaultdict(lambda: 0.0)
self.click_counter: Counter[ActionEvent] = Counter()
self.action_delay_handles: DefaultDict[
ActionEvent, Optional[float]
] = defaultdict(lambda: None)
self.multiple_click_action_delay_tasks: DefaultDict[
self.action_delay_handles: Dict[ActionEvent, Optional[float]] = defaultdict(
lambda: None
)
self.multiple_click_action_delay_tasks: Dict[
ActionEvent, Optional[Future]
] = defaultdict(lambda: None)

Expand Down Expand Up @@ -188,7 +185,7 @@ def get_actions_mapping(self, integration: Integration) -> TypeActionsMapping:
raise ValueError(f"This controller does not support {integration.name}.")
return actions_mapping

def get_list(self, entities: Union[List[T], str]) -> Union[List[T], List[str]]:
def get_list(self, entities: Union[List[str], str]) -> List[str]:
if isinstance(entities, str):
return [entities]
return entities
Expand Down Expand Up @@ -387,7 +384,7 @@ def get_zha_actions_mapping(self) -> Optional[TypeActionsMapping]:
"""
return None

def get_zha_action(self, data: Dict[Any, Any]) -> Optional[str]:
def get_zha_action(self, data: EventData) -> Optional[str]:
"""
This method can be override for controllers that do not support
the standard extraction of the actions on cx_core/integration/zha.py
Expand All @@ -398,44 +395,6 @@ def get_type_actions_mapping(self) -> TypeActionsMapping:
return {}


class TypeController(Controller, abc.ABC):
@abc.abstractmethod
def get_domain(self) -> List[str]:
raise NotImplementedError

async def check_domain(self, entity: str) -> None:
domains = self.get_domain()
if entity.startswith("group."):
entities = await self.get_state(entity, attribute="entity_id")
same_domain = all(
(
any(elem.startswith(domain + ".") for domain in domains)
for elem in entities
)
)
if not same_domain:
raise ValueError(
f"All entities from '{entity}' must be from one "
f"of the following domains {domains} (e.g. {domains[0]}.bedroom)"
)
elif not any(entity.startswith(domain + ".") for domain in domains):
raise ValueError(
f"'{entity}' must be from one of the following domains "
f"{domains} (e.g. {domains[0]}.bedroom)"
)

async def get_entity_state(self, entity: str, attribute: str = None) -> Any:
if entity.startswith("group."):
entities = await self.get_state(entity, attribute="entity_id")
if len(entities) == 0:
raise ValueError(
f"The group `{entity}` does not have any entities registered."
)
entity = entities[0]
out = await self.get_state(entity, attribute=attribute)
return out


class ReleaseHoldController(Controller, abc.ABC):
DEFAULT_MAX_LOOPS = 50

Expand Down
44 changes: 30 additions & 14 deletions apps/controllerx/cx_core/feature_support/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
from typing import List, Set, Union
from typing import TYPE_CHECKING, List, Optional, Set, Type, TypeVar

from cx_core.controller import TypeController
if TYPE_CHECKING:
from cx_core.type_controller import TypeController

SupportedFeatureNumber = Union[int, str]
Features = List[int]
SupportedFeatures = Set[int]
FeatureSupportType = TypeVar("FeatureSupportType", bound="FeatureSupport")


class FeatureSupport:

entity_id: str
controller: "TypeController"
features: Features = []
update_supported_features: bool
_supported_features: Optional[SupportedFeatures]

@staticmethod
def encode(supported_features: SupportedFeatures) -> int:
number = 0
Expand All @@ -21,34 +29,42 @@ def decode(number: int, features: Features) -> SupportedFeatures:

def __init__(
self,
entity: str,
controller: TypeController,
features: Features,
update_supported_features: bool,
entity_id: str,
controller: "TypeController",
update_supported_features=False,
) -> None:
self.entity = entity
self.entity_id = entity_id
self.controller = controller
self._supported_features = None
self.features = features
self.update_supported_features = update_supported_features

async def supported_features(self):
@classmethod
def instantiate(
cls: Type[FeatureSupportType],
entity_id: str,
controller: "TypeController",
update_supported_features=False,
) -> FeatureSupportType:
return cls(entity_id, controller, update_supported_features)

@property
async def supported_features(self) -> SupportedFeatures:
if self._supported_features is None or self.update_supported_features:
bitfield: str = await self.controller.get_entity_state(
self.entity, attribute="supported_features"
self.entity_id, attribute="supported_features"
)
if bitfield is not None:
self._supported_features = FeatureSupport.decode(
int(bitfield), self.features
)
else:
raise ValueError(
f"`supported_features` could not be read from `{self.entity}`. Entity might not be available."
f"`supported_features` could not be read from `{self.entity_id}`. Entity might not be available."
)
return self._supported_features

async def is_supported(self, feature: int) -> bool:
return feature in await self.supported_features()
return feature in await self.supported_features

async def not_supported(self, feature: int) -> bool:
return feature not in await self.supported_features()
return feature not in await self.supported_features
Loading

0 comments on commit 27b46f0

Please sign in to comment.