diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index ac401a14..00000000 --- a/.coveragerc +++ /dev/null @@ -1,3 +0,0 @@ -[report] -show_missing = True -fail_under = 80 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 4dbb28ad..3d261715 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -13,15 +13,15 @@ jobs: strategy: matrix: python-version: - - "3.12" + - "3.13" steps: - name: 📥 Checkout the repository uses: actions/checkout@v4 - - name: 🛠️ Set up Python - uses: actions/setup-python@v5 with: - fetch-depth: 2 + fetch-depth: 2 + - name: 🛠️ Set up Python + uses: actions/setup-python@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: diff --git a/custom_components/keymaster/__init__.py b/custom_components/keymaster/__init__.py index 54797a90..58932d17 100644 --- a/custom_components/keymaster/__init__.py +++ b/custom_components/keymaster/__init__.py @@ -218,7 +218,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) ) - if unload_ok: + if unload_ok and DOMAIN in hass.data and COORDINATOR in hass.data[DOMAIN]: coordinator: KeymasterCoordinator = hass.data[DOMAIN][COORDINATOR] await coordinator.delete_lock_by_config_entry_id(config_entry.entry_id) diff --git a/custom_components/keymaster/config_flow.py b/custom_components/keymaster/config_flow.py index 94bf8e89..13cd27e2 100644 --- a/custom_components/keymaster/config_flow.py +++ b/custom_components/keymaster/config_flow.py @@ -117,16 +117,15 @@ async def async_step_init( def _available_parent_locks(hass: HomeAssistant, entry_id: str = None) -> list: """Find other keymaster configurations and list them as posible - parent locks if they are not a child lock already""" + parent locks if they are not a child lock already + """ data: list[str] = ["(none)"] if DOMAIN not in hass.data: return data for entry in hass.config_entries.async_entries(DOMAIN): - if CONF_PARENT not in entry.data and entry.entry_id != entry_id: - data.append(entry.title) - elif entry.data[CONF_PARENT] is None and entry.entry_id != entry_id: + if CONF_PARENT not in entry.data and entry.entry_id != entry_id or entry.data[CONF_PARENT] is None and entry.entry_id != entry_id: data.append(entry.title) return data @@ -207,7 +206,8 @@ def _get_default(key: str, fallback_default: Any = None) -> Any: script_default: str | None = _get_default(CONF_NOTIFY_SCRIPT_NAME) if isinstance(script_default, str) and not script_default.startswith("script."): script_default = f"script.{script_default}" - return vol.Schema( + _LOGGER.debug("[get_schema] script_default: %s (%s)", script_default, type(script_default)) + schema = vol.Schema( { vol.Required(CONF_LOCK_NAME, default=_get_default(CONF_LOCK_NAME)): str, vol.Required( @@ -267,15 +267,32 @@ def _get_default(key: str, fallback_default: Any = None) -> Any: extra_entities=[DEFAULT_ALARM_TYPE_SENSOR], ) ), - vol.Optional( - CONF_NOTIFY_SCRIPT_NAME, - default=script_default, - ): vol.In( - _get_entities( - hass=hass, - domain=SCRIPT_DOMAIN, - ) - ), + } + ) + if script_default: + schema = schema.extend({ + vol.Optional( + CONF_NOTIFY_SCRIPT_NAME, + default=script_default, + ): vol.In( + _get_entities( + hass=hass, + domain=SCRIPT_DOMAIN, + ) + ), + }) + else: + schema = schema.extend({ + vol.Optional( + CONF_NOTIFY_SCRIPT_NAME + ): vol.In( + _get_entities( + hass=hass, + domain=SCRIPT_DOMAIN, + ) + ), + }) + return schema.extend({ vol.Required( CONF_HIDE_PINS, default=_get_default(CONF_HIDE_PINS, DEFAULT_HIDE_PINS) ): bool, @@ -292,10 +309,12 @@ async def _start_config_flow( entry_id: str = None, ): """Start a config flow""" + _LOGGER.debug("[start_config_flow] step_id: %s, defaults: %s", step_id, defaults) errors = {} description_placeholders = {} if user_input is not None: + _LOGGER.debug("[start_config_flow] step_id: %s, initial user_input: %s, errors: %s", step_id, user_input, errors) user_input[CONF_SLOTS] = int(user_input.get(CONF_SLOTS)) user_input[CONF_START] = int(user_input.get(CONF_START)) @@ -307,6 +326,7 @@ async def _start_config_flow( # Update options if no errors if not errors: + _LOGGER.debug("[start_config_flow] step_id: %s, final user_input: %s", step_id, user_input) if step_id == "user": return cls.async_create_entry(title=title, data=user_input) cls.hass.config_entries.async_update_entry( diff --git a/pylintrc b/pylintrc deleted file mode 100644 index b2b29679..00000000 --- a/pylintrc +++ /dev/null @@ -1,50 +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 -persistent=no - -[BASIC] -good-names=id,i,j,k,ex,Run,_,fp -max-attributes=15 -argument-naming-style=snake_case -attr-naming-style=snake_case - -[MESSAGES CONTROL] -# Reasons disabled: -# locally-disabled - it spams too much -# too-many-* - are not enforced for the sake of readability -# too-few-* - same as too-many-* -# import-outside-toplevel - TODO -disable= - duplicate-code, - fixme, - import-outside-toplevel, - locally-disabled, - too-few-public-methods, - too-many-arguments, - too-many-public-methods, - too-many-instance-attributes, - too-many-branches, - too-many-statements, - broad-except, - too-many-lines, - too-many-locals, - unexpected-keyword-arg, - abstract-method, - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=8 - -[REPORTS] -score=no - -[TYPECHECK] -# For attrs -ignored-classes=_CountingAttr - -[FORMAT] -expected-line-ending-format=LF \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 445f13e8..57dc3527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,59 +1,55 @@ -[tool.black] -target-version = ["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.coverage.report] +show_missing = true +fail_under = 80 -[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 +[tool.mypy] +python_version = "3.13" +show_error_codes = true +ignore_errors = true +follow_imports = "silent" +ignore_missing_imports = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_unused_configs = true + +[tool.pylint] +ignore = ["tests"] +jobs = 2 +persistent = false load-plugins = [ - "pylint_strict_informational", + "pylint.extensions.code_style", + "pylint.extensions.typing", + "pylint_per_file_ignores", ] -persistent = false extension-pkg-whitelist = [ "ciso8601", "cv2", ] -[tool.pylint.BASIC] -good-names = [ - "_", - "ev", - "ex", - "fp", - "i", - "id", - "j", - "k", - "Run", - "T", +[tool.pylint.basic] +max-attributes = 15 +argument-naming-style = "snake_case" +attr-naming-style = "snake_case" + +[tool.pylint.code_style] +max-line-length-suggestions = 72 + +[tool.pylint.exceptions] +overgeneral-exceptions = [ + "builtins.BaseException", + "builtins.Exception", + # "homeassistant.exceptions.HomeAssistantError", ] -[tool.pylint."MESSAGES CONTROL"] +[tool.pylint.format] +expected-line-ending-format = "LF" + +[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-* @@ -61,51 +57,277 @@ good-names = [ # inconsistent-return-statements - doesn't handle raise # too-many-ancestors - it's too strict. # wrong-import-order - isort guards this +# possibly-used-before-assignment - too many errors / not necessarily issues disable = [ - "format", - "abstract-class-little-used", "abstract-method", + "broad-except", "cyclic-import", "duplicate-code", + "fixme", + "format", + "import-outside-toplevel", "inconsistent-return-statements", "locally-disabled", "not-context-manager", + "possibly-used-before-assignment", "too-few-public-methods", "too-many-ancestors", "too-many-arguments", + "too-many-boolean-expressions", "too-many-branches", "too-many-instance-attributes", "too-many-lines", "too-many-locals", + "too-many-positional-arguments", "too-many-public-methods", "too-many-return-statements", "too-many-statements", - "too-many-boolean-expressions", + "unexpected-keyword-arg", "unused-argument", "wrong-import-order", + "consider-using-namedtuple-or-dataclass", + "consider-using-assignment-expr", + + # Handled by ruff + # Ref: + "await-outside-async", # PLE1142 + "bad-str-strip-call", # PLE1310 + "bad-string-format-type", # PLE1307 + "bidirectional-unicode", # PLE2502 + "continue-in-finally", # PLE0116 + "duplicate-bases", # PLE0241 + "misplaced-bare-raise", # PLE0704 + "format-needs-mapping", # F502 + "function-redefined", # F811 + # Needed because ruff does not understand type of __all__ generated by a function + # "invalid-all-format", # PLE0605 + "invalid-all-object", # PLE0604 + "invalid-character-backspace", # PLE2510 + "invalid-character-esc", # PLE2513 + "invalid-character-nul", # PLE2514 + "invalid-character-sub", # PLE2512 + "invalid-character-zero-width-space", # PLE2515 + "logging-too-few-args", # PLE1206 + "logging-too-many-args", # PLE1205 + "missing-format-string-key", # F524 + "mixed-format-string", # F506 + "no-method-argument", # N805 + "no-self-argument", # N805 + "nonexistent-operator", # B002 + "nonlocal-without-binding", # PLE0117 + "not-in-loop", # F701, F702 + "notimplemented-raised", # F901 + "return-in-init", # PLE0101 + "return-outside-function", # F706 + "syntax-error", # E999 + "too-few-format-args", # F524 + "too-many-format-args", # F522 + "too-many-star-expressions", # F622 + "truncated-format-string", # F501 + "undefined-all-variable", # F822 + "undefined-variable", # F821 + "used-prior-global-declaration", # PLE0118 + "yield-inside-async-function", # PLE1700 + "yield-outside-function", # F704 + "anomalous-backslash-in-string", # W605 + "assert-on-string-literal", # PLW0129 + "assert-on-tuple", # F631 + "bad-format-string", # W1302, F + "bad-format-string-key", # W1300, F + "bare-except", # E722 + "binary-op-exception", # PLW0711 + "cell-var-from-loop", # B023 + # "dangerous-default-value", # B006, ruff catches new occurrences, needs more work + "duplicate-except", # B014 + "duplicate-key", # F601 + "duplicate-string-formatting-argument", # F + "duplicate-value", # F + "eval-used", # S307 + "exec-used", # S102 + "expression-not-assigned", # B018 + "f-string-without-interpolation", # F541 + "forgotten-debug-statement", # T100 + "format-string-without-interpolation", # F + # "global-statement", # PLW0603, ruff catches new occurrences, needs more work + "global-variable-not-assigned", # PLW0602 + "implicit-str-concat", # ISC001 + "import-self", # PLW0406 + "inconsistent-quotes", # Q000 + "invalid-envvar-default", # PLW1508 + "keyword-arg-before-vararg", # B026 + "logging-format-interpolation", # G + "logging-fstring-interpolation", # G + "logging-not-lazy", # G + "misplaced-future", # F404 + "named-expr-without-context", # PLW0131 + "nested-min-max", # PLW3301 + "pointless-statement", # B018 + "raise-missing-from", # B904 + "redefined-builtin", # A001 + "try-except-raise", # TRY302 + "unused-argument", # ARG001, we don't use it + "unused-format-string-argument", #F507 + "unused-format-string-key", # F504 + "unused-import", # F401 + "unused-variable", # F841 + "useless-else-on-loop", # PLW0120 + "wildcard-import", # F403 + "bad-classmethod-argument", # N804 + "consider-iterating-dictionary", # SIM118 + "empty-docstring", # D419 + "invalid-name", # N815 + "line-too-long", # E501, disabled globally + "missing-class-docstring", # D101 + "missing-final-newline", # W292 + "missing-function-docstring", # D103 + "missing-module-docstring", # D100 + "multiple-imports", #E401 + "singleton-comparison", # E711, E712 + "subprocess-run-check", # PLW1510 + "superfluous-parens", # UP034 + "ungrouped-imports", # I001 + "unidiomatic-typecheck", # E721 + "unnecessary-direct-lambda-call", # PLC3002 + "unnecessary-lambda-assignment", # PLC3001 + "unnecessary-pass", # PIE790 + "unneeded-not", # SIM208 + "useless-import-alias", # PLC0414 + "wrong-import-order", # I001 + "wrong-import-position", # E402 + "comparison-of-constants", # PLR0133 + "comparison-with-itself", # PLR0124 + "consider-alternative-union-syntax", # UP007 + "consider-merging-isinstance", # PLR1701 + "consider-using-alias", # UP006 + "consider-using-dict-comprehension", # C402 + "consider-using-generator", # C417 + "consider-using-get", # SIM401 + "consider-using-set-comprehension", # C401 + "consider-using-sys-exit", # PLR1722 + "consider-using-ternary", # SIM108 + "literal-comparison", # F632 + "property-with-parameters", # PLR0206 + "super-with-arguments", # UP008 + "too-many-branches", # PLR0912 + "too-many-return-statements", # PLR0911 + "too-many-statements", # PLR0915 + "trailing-comma-tuple", # COM818 + "unnecessary-comprehension", # C416 + "use-a-generator", # C417 + "use-dict-literal", # C406 + "use-list-literal", # C405 + "useless-object-inheritance", # UP004 + "useless-return", # PLR1711 + "no-else-break", # RET508 + "no-else-continue", # RET507 + "no-else-raise", # RET506 + "no-else-return", # RET505 + "broad-except", # BLE001 + "protected-access", # SLF001 + "broad-exception-raised", # TRY002 + "consider-using-f-string", # PLC0209 + # "no-self-use", # PLR6301 # Optional plugin, not enabled + + # Handled by mypy + # Ref: + "abstract-class-instantiated", + "arguments-differ", + "assigning-non-slot", + "assignment-from-no-return", + "assignment-from-none", + "bad-exception-cause", + "bad-format-character", + "bad-reversed-sequence", + "bad-super-call", + "bad-thread-instantiation", + "catching-non-exception", + "comparison-with-callable", + "deprecated-class", + "dict-iter-missing-items", + "format-combined-specification", + "global-variable-undefined", + "import-error", + "inconsistent-mro", + "inherit-non-class", + "init-is-generator", + "invalid-class-object", + "invalid-enum-extension", + "invalid-envvar-value", + "invalid-format-returned", + "invalid-hash-returned", + "invalid-metaclass", + "invalid-overridden-method", + "invalid-repr-returned", + "invalid-sequence-index", + "invalid-slice-index", + "invalid-slots-object", + "invalid-slots", + "invalid-star-assignment-target", + "invalid-str-returned", + "invalid-unary-operand-type", + "invalid-unicode-codec", + "isinstance-second-argument-not-valid-type", + "method-hidden", + "misplaced-format-function", + "missing-format-argument-key", + "missing-format-attribute", + "missing-kwoa", + "no-member", + "no-value-for-parameter", + "non-iterator-returned", + "non-str-assignment-to-dunder-name", + "nonlocal-and-global", + "not-a-mapping", + "not-an-iterable", + "not-async-context-manager", + "not-callable", + "not-context-manager", + "overridden-final-method", + "raising-bad-type", + "raising-non-exception", + "redundant-keyword-arg", + "relative-beyond-top-level", + "self-cls-assignment", + "signature-differs", + "star-needs-assignment-target", + "subclassed-final-class", + "super-without-brackets", + "too-many-function-args", + "typevar-double-variance", + "typevar-name-mismatch", + "unbalanced-dict-unpacking", + "unbalanced-tuple-unpacking", + "unexpected-keyword-arg", + "unhashable-member", + "unpacking-non-sequence", + "unsubscriptable-object", + "unsupported-assignment-operation", + "unsupported-binary-operation", + "unsupported-delete-operation", + "unsupported-membership-test", + "used-before-assignment", + "using-final-decorator-in-unsupported-version", + "wrong-exception-operation", ] 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 +per-file-ignores = [ + # redefined-outer-name: Tests reference fixtures in the test function + # use-implicit-booleaness-not-comparison: Tests need to validate that a list + # or a dict is returned + "/tests/:redefined-outer-name,use-implicit-booleaness-not-comparison", ] -[tool.pylint.FORMAT] -expected-line-ending-format = "LF" +[tool.pylint.refactoring] +max-nested-blocks = 8 -[tool.pylint.EXCEPTIONS] -overgeneral-exceptions = [ - "BaseException", - "Exception", - "HomeAssistantError", -] +[tool.pylint.reports] +score = false + +[tool.pylint.typecheck] +ignored-classes = ["_CountingAttr"] [tool.pytest.ini_options] testpaths = [ @@ -115,3 +337,267 @@ norecursedirs = [ ".git", "testing_config", ] +log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" +log_date_format = "%Y-%m-%d %H:%M:%S" +log_cli = true # Enable live logging to the console +log_level = "DEBUG" # Set the logging level to DEBUG +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +timeout = 30 +addopts = "-vv --cov=custom_components/keymaster --cov-report=xml" + +[tool.ruff] +required-version = ">=0.8.0" + +[tool.ruff.lint] +select = [ + "A001", # Variable {name} is shadowing a Python builtin + "ASYNC210", # Async functions should not call blocking HTTP methods + "ASYNC220", # Async functions should not create subprocesses with blocking methods + "ASYNC221", # Async functions should not run processes with blocking methods + "ASYNC222", # Async functions should not wait on processes with blocking methods + "ASYNC230", # Async functions should not open files with blocking methods like open + "ASYNC251", # Async functions should not call time.sleep + "B002", # Python does not support the unary prefix increment + "B005", # Using .strip() with multi-character strings is misleading + "B007", # Loop control variable {name} not used within loop body + "B014", # Exception handler with duplicate exception + "B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it. + "B017", # pytest.raises(BaseException) should be considered evil + "B018", # Found useless attribute access. Either assign it to a variable or remove it. + "B023", # Function definition does not bind loop variable {name} + "B026", # Star-arg unpacking after a keyword argument is strongly discouraged + "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? + "B904", # Use raise from to specify exception cause + "B905", # zip() without an explicit strict= parameter + "BLE", + "C", # complexity + "COM818", # Trailing comma on bare tuple prohibited + "D", # docstrings + "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() + "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) + "E", # pycodestyle + "F", # pyflakes/autoflake + "F541", # f-string without any placeholders + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "ICN001", # import concentions; {name} should be imported as {asname} + "LOG", # flake8-logging + "N804", # First argument of a class method should be named cls + "N805", # First argument of a method should be named self + "N815", # Variable {name} in class scope should not be mixedCase + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF005", # Consider iterable unpacking instead of concatenation + "RUF006", # Store a reference to the return value of asyncio.create_task + "RUF010", # Use explicit conversion flag + "RUF013", # PEP 484 prohibits implicit Optional + "RUF017", # Avoid quadratic list summation + "RUF018", # Avoid assignment expressions in assert statements + "RUF019", # Unnecessary key check before dictionary access + # "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up + "S102", # Use of exec detected + "S103", # bad-file-permissions + "S108", # hardcoded-temp-file + "S306", # suspicious-mktemp-usage + "S307", # suspicious-eval-usage + "S313", # suspicious-xmlc-element-tree-usage + "S314", # suspicious-xml-element-tree-usage + "S315", # suspicious-xml-expat-reader-usage + "S316", # suspicious-xml-expat-builder-usage + "S317", # suspicious-xml-sax-usage + "S318", # suspicious-xml-mini-dom-usage + "S319", # suspicious-xml-pull-dom-usage + "S320", # suspicious-xmle-tree-usage + "S601", # paramiko-call + "S602", # subprocess-popen-with-shell-equals-true + "S604", # call-with-shell-equals-true + "S608", # hardcoded-sql-expression + "S609", # unix-command-wildcard-injection + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T100", # Trace found: {name} used + "T20", # flake8-print + "TC", # flake8-type-checking + "TID", # Tidy imports + "TRY", # tryceratops + "UP", # pyupgrade + "UP031", # Use format specifiers instead of percent format + "UP032", # Use f-string instead of `format` call + "W", # pycodestyle +] + +ignore = [ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D406", # Section name should end with a newline + "D407", # Section name underlining + "E501", # line too long + + "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives + "PLR0911", # Too many return statements ({returns} > {max_returns}) + "PLR0912", # Too many branches ({branches} > {max_branches}) + "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) + "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR0917", # too-many-positional-arguments + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target + "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception + "PT018", # Assertion should be broken down into multiple parts + "RUF001", # String contains ambiguous unicode character. + "RUF002", # Docstring contains ambiguous unicode character. + "RUF003", # Comment contains ambiguous unicode character. + "RUF015", # Prefer next(...) over single element slice + "SIM102", # Use a single if statement instead of nested if statements + "SIM103", # Return the condition {condition} directly + "SIM108", # Use ternary operator {contents} instead of if-else-block + "SIM115", # Use context handler for opening files + + # Moving imports into type-checking blocks can mess with pytest.patch() + "TC001", # Move application import {} into a type-checking block + "TC002", # Move third-party import {} into a type-checking block + "TC003", # Move standard library import {} into a type-checking block + + "TRY003", # Avoid specifying long messages outside the exception class + "TRY400", # Use `logging.exception` instead of `logging.error` + # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 + "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` + + # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q", + "COM812", + "COM819", + "ISC001", + + # Disabled because ruff does not understand type of __all__ generated by a function + "PLE0605" +] + +[tool.ruff.lint.flake8-import-conventions.extend-aliases] +voluptuous = "vol" +"homeassistant.components.air_quality.PLATFORM_SCHEMA" = "AIR_QUALITY_PLATFORM_SCHEMA" +"homeassistant.components.alarm_control_panel.PLATFORM_SCHEMA" = "ALARM_CONTROL_PANEL_PLATFORM_SCHEMA" +"homeassistant.components.binary_sensor.PLATFORM_SCHEMA" = "BINARY_SENSOR_PLATFORM_SCHEMA" +"homeassistant.components.button.PLATFORM_SCHEMA" = "BUTTON_PLATFORM_SCHEMA" +"homeassistant.components.calendar.PLATFORM_SCHEMA" = "CALENDAR_PLATFORM_SCHEMA" +"homeassistant.components.camera.PLATFORM_SCHEMA" = "CAMERA_PLATFORM_SCHEMA" +"homeassistant.components.climate.PLATFORM_SCHEMA" = "CLIMATE_PLATFORM_SCHEMA" +"homeassistant.components.conversation.PLATFORM_SCHEMA" = "CONVERSATION_PLATFORM_SCHEMA" +"homeassistant.components.cover.PLATFORM_SCHEMA" = "COVER_PLATFORM_SCHEMA" +"homeassistant.components.date.PLATFORM_SCHEMA" = "DATE_PLATFORM_SCHEMA" +"homeassistant.components.datetime.PLATFORM_SCHEMA" = "DATETIME_PLATFORM_SCHEMA" +"homeassistant.components.device_tracker.PLATFORM_SCHEMA" = "DEVICE_TRACKER_PLATFORM_SCHEMA" +"homeassistant.components.event.PLATFORM_SCHEMA" = "EVENT_PLATFORM_SCHEMA" +"homeassistant.components.fan.PLATFORM_SCHEMA" = "FAN_PLATFORM_SCHEMA" +"homeassistant.components.geo_location.PLATFORM_SCHEMA" = "GEO_LOCATION_PLATFORM_SCHEMA" +"homeassistant.components.humidifier.PLATFORM_SCHEMA" = "HUMIDIFIER_PLATFORM_SCHEMA" +"homeassistant.components.image.PLATFORM_SCHEMA" = "IMAGE_PLATFORM_SCHEMA" +"homeassistant.components.image_processing.PLATFORM_SCHEMA" = "IMAGE_PROCESSING_PLATFORM_SCHEMA" +"homeassistant.components.lawn_mower.PLATFORM_SCHEMA" = "LAWN_MOWER_PLATFORM_SCHEMA" +"homeassistant.components.light.PLATFORM_SCHEMA" = "LIGHT_PLATFORM_SCHEMA" +"homeassistant.components.lock.PLATFORM_SCHEMA" = "LOCK_PLATFORM_SCHEMA" +"homeassistant.components.media_player.PLATFORM_SCHEMA" = "MEDIA_PLAYER_PLATFORM_SCHEMA" +"homeassistant.components.notify.PLATFORM_SCHEMA" = "NOTIFY_PLATFORM_SCHEMA" +"homeassistant.components.number.PLATFORM_SCHEMA" = "NUMBER_PLATFORM_SCHEMA" +"homeassistant.components.remote.PLATFORM_SCHEMA" = "REMOTE_PLATFORM_SCHEMA" +"homeassistant.components.scene.PLATFORM_SCHEMA" = "SCENE_PLATFORM_SCHEMA" +"homeassistant.components.select.PLATFORM_SCHEMA" = "SELECT_PLATFORM_SCHEMA" +"homeassistant.components.sensor.PLATFORM_SCHEMA" = "SENSOR_PLATFORM_SCHEMA" +"homeassistant.components.siren.PLATFORM_SCHEMA" = "SIREN_PLATFORM_SCHEMA" +"homeassistant.components.stt.PLATFORM_SCHEMA" = "STT_PLATFORM_SCHEMA" +"homeassistant.components.switch.PLATFORM_SCHEMA" = "SWITCH_PLATFORM_SCHEMA" +"homeassistant.components.text.PLATFORM_SCHEMA" = "TEXT_PLATFORM_SCHEMA" +"homeassistant.components.time.PLATFORM_SCHEMA" = "TIME_PLATFORM_SCHEMA" +"homeassistant.components.todo.PLATFORM_SCHEMA" = "TODO_PLATFORM_SCHEMA" +"homeassistant.components.tts.PLATFORM_SCHEMA" = "TTS_PLATFORM_SCHEMA" +"homeassistant.components.vacuum.PLATFORM_SCHEMA" = "VACUUM_PLATFORM_SCHEMA" +"homeassistant.components.valve.PLATFORM_SCHEMA" = "VALVE_PLATFORM_SCHEMA" +"homeassistant.components.update.PLATFORM_SCHEMA" = "UPDATE_PLATFORM_SCHEMA" +"homeassistant.components.wake_word.PLATFORM_SCHEMA" = "WAKE_WORD_PLATFORM_SCHEMA" +"homeassistant.components.water_heater.PLATFORM_SCHEMA" = "WATER_HEATER_PLATFORM_SCHEMA" +"homeassistant.components.weather.PLATFORM_SCHEMA" = "WEATHER_PLATFORM_SCHEMA" +"homeassistant.core.DOMAIN" = "HOMEASSISTANT_DOMAIN" +"homeassistant.helpers.area_registry" = "ar" +"homeassistant.helpers.category_registry" = "cr" +"homeassistant.helpers.config_validation" = "cv" +"homeassistant.helpers.device_registry" = "dr" +"homeassistant.helpers.entity_registry" = "er" +"homeassistant.helpers.floor_registry" = "fr" +"homeassistant.helpers.issue_registry" = "ir" +"homeassistant.helpers.label_registry" = "lr" +"homeassistant.util.dt" = "dt_util" + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"async_timeout".msg = "use asyncio.timeout instead" +"pytz".msg = "use zoneinfo instead" + +[tool.ruff.lint.isort] +force-sort-within-sections = true +known-first-party = [ + "homeassistant", +] +combine-as-imports = true +split-on-trailing-comma = false + +[tool.ruff.lint.mccabe] +max-complexity = 25 + +[tool.ruff.lint.pydocstyle] +property-decorators = ["propcache.cached_property"] + +[tool.tox.gh-actions] +python = """ + 3.11: py311 + 3.12: py312 + 3.13: py313, lint, mypy +""" + +[tool.tox] +skipsdist = true +requires = ["tox>=4.19"] +env_list = ["py311", "py312", "py313", "lint", "mypy"] +skip_missing_interpreters = true + +[tool.tox.env_run_base] +description = "Run pytest under {base_python}" +commands = [[ "pytest", "tests", { replace = "posargs", extend = true} ]] +deps = ["-rrequirements_test.txt"] +ignore_errors = true + +[tool.tox.env.lint] +description = "Lint code using ruff under {base_python}" +ignore_errors = true +commands = [ + ["ruff", "check", "custom_components{/}"], + ["ruff", "check", "tests{/}"], +] +deps = ["-rrequirements_test.txt"] + +[tool.tox.env.mypy] +description = "Run mypy for type-checking under {base_python}" +ignore_errors = true +commands = [["mypy", "custom_components{/}keymaster"]] +deps = ["-rrequirements_test.txt"] diff --git a/requirements_test.txt b/requirements_test.txt index da158983..3e43c29c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,11 +3,9 @@ aiohttp_cors asyncio_mqtt pytest pytest-homeassistant-custom-component -black -isort pydispatcher zeroconf tox mypy -flake8 -pylint \ No newline at end of file +pylint +ruff \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index c2282f42..00000000 --- a/setup.cfg +++ /dev/null @@ -1,32 +0,0 @@ -[mypy] -python_version = 3.12 -show_error_codes = true -ignore_errors = true -follow_imports = silent -ignore_missing_imports = true -warn_incomplete_stub = true -warn_redundant_casts = true -warn_unused_configs = true - -[flake8] -exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build -# 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 - -[isort] -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -line_length = 88 diff --git a/tests/common.py b/tests/common.py index 5f38386a..694ce7c4 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,16 +1,16 @@ """Helpers for tests.""" import asyncio +from datetime import datetime import functools as ft import os import time from unittest.mock import patch -from datetime import datetime from homeassistant import core as ha from homeassistant.core import HomeAssistant from homeassistant.util.async_ import run_callback_threadsafe -import homeassistant.util.dt as date_util +import homeassistant.util.dt as dt_util def load_fixture(filename): @@ -52,13 +52,13 @@ def threadsafe(*args, **kwargs): @ha.callback def async_fire_time_changed( - hass: HomeAssistant, datetime_: datetime = None, fire_all: bool = False + hass: HomeAssistant, datetime_: datetime | None = None, fire_all: bool = False ) -> None: """Fire a time changed event.""" if datetime_ is None: - datetime_ = date_util.utcnow() + datetime_ = dt_util.utcnow() - for task in list(hass.loop._scheduled): + for task in list(hass.loop._scheduled): # noqa: SLF001 if not isinstance(task, asyncio.TimerHandle): continue if task.cancelled(): @@ -70,9 +70,9 @@ def async_fire_time_changed( if fire_all or mock_seconds_into_future >= future_seconds: with patch( "homeassistant.helpers.event.time_tracker_utcnow", - return_value=date_util.as_utc(datetime_), + return_value=dt_util.as_utc(datetime_), ): - task._run() + task._run() # noqa: SLF001 task.cancel() diff --git a/tests/conftest.py b/tests/conftest.py index 589ba9b3..93344d94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -""" Fixtures for keymaster tests. """ +"""Fixtures for keymaster tests.""" import asyncio import copy @@ -7,7 +7,6 @@ import pytest from pytest_homeassistant_custom_component.common import MockConfigEntry -from sqlalchemy import false from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node from zwave_js_server.version import VersionInfo @@ -19,27 +18,21 @@ @pytest.fixture(autouse=True) def auto_enable_custom_integrations(enable_custom_integrations): - yield + """Enable custom integrations defined in the test dir.""" + return @pytest.fixture(name="skip_notifications", autouse=True) def skip_notifications_fixture(): """Skip notification calls.""" - with patch("homeassistant.components.persistent_notification.async_create"), patch( - "homeassistant.components.persistent_notification.async_dismiss" + with ( + patch("homeassistant.components.persistent_notification.async_create"), + patch("homeassistant.components.persistent_notification.async_dismiss"), ): yield -@pytest.fixture(autouse=True) -async def mock_init_child_locks(): - with patch( - "custom_components.keymaster.services.init_child_locks", return_value=True - ), patch("custom_components.keymaster.init_child_locks"): - yield - - -@pytest.fixture() +@pytest.fixture def mock_get_entities(): """Mock email data update class values.""" with patch( @@ -51,6 +44,7 @@ def mock_get_entities(): "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", "binary_sensor.frontdoor", + "script.keymaster_frontdoor_manual_notify", ] yield mock_get_entities @@ -100,30 +94,9 @@ def mock_osmakedir(): yield -@pytest.fixture -def mock_generate_package_files(): - """Fixture to mock generate package files.""" - with patch("custom_components.keymaster.generate_package_files", return_value=None): - yield - - -@pytest.fixture -def mock_delete_folder(): - """Fixture to mock delete_folder helper function.""" - with patch("custom_components.keymaster.delete_folder"): - yield - - -@pytest.fixture -def mock_delete_lock_and_base_folder(): - """Fixture to mock delete_lock_and_base_folder helper function.""" - with patch("custom_components.keymaster.delete_lock_and_base_folder"): - yield - - @pytest.fixture def mock_os_path_join(): - """Fixture to mock splitext""" + """Fixture to mock path join.""" with patch("os.path.join"): yield @@ -172,7 +145,7 @@ async def connect(): async def listen(driver_ready: asyncio.Event) -> None: driver_ready.set() await asyncio.sleep(30) - assert False, "Listen wasn't canceled!" + pytest.fail("Listen wasn't canceled!") async def disconnect(): client.connected = False @@ -239,16 +212,47 @@ async def mock_zwavejs_get_usercodes(): {"code_slot": 14, "usercode": "", "in_use": False}, ] with patch( - "custom_components.keymaster.get_usercodes", return_value=slot_data + "zwave_js_server.util.lock.get_usercodes", return_value=slot_data + ) as mock_usercodes: + yield mock_usercodes + + +@pytest.fixture +async def mock_zwavejs_clear_usercode(): + """Fixture to mock clear_usercode.""" + with patch( + "zwave_js_server.util.lock.clear_usercode", return_value=None + ) as mock_usercodes: + yield mock_usercodes + + +@pytest.fixture +async def mock_zwavejs_set_usercode(): + """Fixture to mock set_usercode.""" + with patch( + "zwave_js_server.util.lock.set_usercode", return_value=None ) as mock_usercodes: yield mock_usercodes @pytest.fixture async def mock_using_zwavejs(): - """Fixture to mock using_ozw in helpers""" + """Fixture to mock using_zwavejs in helpers.""" with patch( - "custom_components.keymaster.binary_sensor.async_using_zwave_js", + "custom_components.keymaster.helpers.async_using_zwave_js", return_value=True, ) as mock_using_zwavejs_helpers: yield mock_using_zwavejs_helpers + + +@pytest.fixture +def mock_async_call_later(): + """Fixture to mock async_call_later to call the callback immediately.""" + with patch("homeassistant.helpers.event.async_call_later") as mock: + + def immediate_call(hass, delay, callback): + # Immediately call the callback with a mock `hass` object + return callback(None) + + mock.side_effect = immediate_call + yield mock diff --git a/tests/const.py b/tests/const.py index 55a1fc9e..cf485058 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,12 +1,10 @@ -""" Constants for tests. """ +"""Constants for tests.""" CONFIG_DATA = { "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", "lockname": "frontdoor", - "generate_package": True, - "packages_path": "packages/keymaster", "sensorname": "binary_sensor.frontdoor", "slots": 6, "start_from": 1, @@ -17,8 +15,6 @@ "alarm_type": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", "entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", "lockname": "frontdoor", - "generate_package": True, - "packages_path": "/config/packages/keymaster", "sensorname": "binary_sensor.frontdoor", "slots": 6, "start_from": 1, @@ -29,8 +25,6 @@ "alarm_type_or_access_control_entity_id": "sensor.smartcode_10_touchpad_electronic_deadbolt_alarm_type", "lock_entity_id": "lock.smartcode_10_touchpad_electronic_deadbolt_locked", "lockname": "frontdoor", - "generate_package": True, - "packages_path": "packages/keymaster", "sensorname": "binary_sensor.frontdoor", "slots": 6, "start_from": 1, @@ -41,8 +35,6 @@ "alarm_type_or_access_control_entity_id": "sensor.smart_code_with_home_connect_technology_alarmtype", "lock_entity_id": "lock.smart_code_with_home_connect_technology", "lockname": "frontdoor", - "generate_package": True, - "packages_path": "packages/keymaster", "sensorname": "binary_sensor.frontdoor", "slots": 6, "start_from": 1, @@ -53,8 +45,6 @@ "alarm_type_or_access_control_entity_id": "sensor.touchscreen_deadbolt_access_control_lock_state", "lock_entity_id": "lock.touchscreen_deadbolt", "lockname": "frontdoor", - "generate_package": True, - "packages_path": "packages/keymaster", "sensorname": "binary_sensor.frontdoor", "slots": 6, "start_from": 1, @@ -65,8 +55,6 @@ "alarm_type_or_access_control_entity_id": "sensor.fake", "lock_entity_id": "lock.smartcode_10_touchpad_electronic_deadbolt_locked", "lockname": "frontdoor", - "generate_package": True, - "packages_path": "packages/keymaster", "sensorname": "binary_sensor.fake", "slots": 6, "start_from": 1, @@ -77,8 +65,6 @@ "alarm_type_or_access_control_entity_id": "sensor.fake", "lock_entity_id": "lock.smartcode_10_touchpad_electronic_deadbolt_locked", "lockname": "sidedoor", - "generate_package": True, - "packages_path": "packages/keymaster", "sensorname": "binary_sensor.fake", "slots": 6, "start_from": 1, @@ -90,8 +76,6 @@ "alarm_type_or_access_control_entity_id": "sensor.fake", "lock_entity_id": "lock.smart_code_with_home_connect_technology", "lockname": "frontdoor", - "generate_package": True, - "packages_path": "packages/keymaster", "sensorname": "binary_sensor.fake", "slots": 5, "start_from": 10, diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py.old similarity index 99% rename from tests/test_binary_sensor.py rename to tests/test_binary_sensor.py.old index d7107dad..7986db3e 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py.old @@ -23,7 +23,7 @@ async def test_zwavejs_network_ready( # Load the integration with wrong lock entity_id config_entry = MockConfigEntry( - domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_910, version=2 + domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_910, version=3 ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 0fb182b8..a36b1fb8 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,30 +1,25 @@ -""" Test keymaster config flow """ +"""Test keymaster config flow.""" import logging from unittest.mock import patch import pytest -from pytest_homeassistant_custom_component.common import MockConfigEntry -from custom_components.keymaster.config_flow import ( - KeyMasterFlowHandler, - _get_entities, - _get_schema, -) -from custom_components.keymaster.const import CONF_PATH, DOMAIN -from homeassistant import config_entries, setup +from custom_components.keymaster.config_flow import _get_entities # noqa: PLC2701 +from custom_components.keymaster.const import DOMAIN +from homeassistant import config_entries from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN - -from .const import CONFIG_DATA +from homeassistant.components.lock.const import LockState +from homeassistant.data_entry_flow import FlowResultType KWIKSET_910_LOCK_ENTITY = "lock.smart_code_with_home_connect_technology" -_LOGGER = logging.getLogger(__name__) +_LOGGER: logging.Logger = logging.getLogger(__name__) pytestmark = pytest.mark.asyncio @pytest.mark.parametrize( - "input_1,title,data", + ("test_user_input", "title", "final_config_flow_data"), [ ( { @@ -32,7 +27,6 @@ "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", "lockname": "frontdoor", - "packages_path": "packages/keymaster", "sensorname": "binary_sensor.frontdoor", "slots": 6, "start_from": 1, @@ -44,355 +38,183 @@ "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", "lockname": "frontdoor", - "generate_package": True, - "packages_path": "packages/keymaster", "sensorname": "binary_sensor.frontdoor", "slots": 6, "start_from": 1, "hide_pins": False, "parent": None, - }, - ), - ( - { - "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", - "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", - "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", - "lockname": "frontdoor", - "packages_path": "packages/keymaster", - "sensorname": "binary_sensor.frontdoor", - "slots": 6, - "start_from": 1, - "parent": "(none)", - }, - "frontdoor", - { - "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", - "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", - "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", - "lockname": "frontdoor", - "generate_package": True, - "packages_path": "packages/keymaster", - "sensorname": "binary_sensor.frontdoor", - "slots": 6, - "start_from": 1, - "hide_pins": False, - "parent": None, - }, - ), - ], -) -async def test_form(input_1, title, data, hass, mock_get_entities): - """Test we get the form.""" - with patch( - "custom_components.keymaster.config_flow.os.path.exists", return_value=True - ), patch( - "custom_components.keymaster.config_flow.os.path.isfile", return_value=True - ), patch( - "custom_components.keymaster.async_setup", return_value=True - ) as mock_setup, patch( - "custom_components.keymaster.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["step_id"] == "user" - assert result["errors"] == {} - # assert result["title"] == title_1 - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], input_1 + } ) - assert result2["type"] == "create_entry" - assert result2["title"] == title - assert result2["data"] == data - - await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize( - "input_1,title,data", - [ - ( - { - "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", - "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", - "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", - "lockname": "frontdoor", - "packages_path": "/packages/keymaster", - "sensorname": "binary_sensor.frontdoor", - "slots": 6, - "start_from": 1, - "parent": "(none)", - }, - "frontdoor", - { - "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", - "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", - "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", - "lockname": "frontdoor", - "generate_package": True, - "packages_path": "/packages/keymaster", - "sensorname": "binary_sensor.frontdoor", - "slots": 6, - "start_from": 1, - "hide_pins": False, - "parent": None, - }, - ), - ], + ] ) -async def test_form_invalid_path(input_1, title, data, mock_get_entities, hass): +async def test_form(test_user_input, title, final_config_flow_data, hass, mock_get_entities): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + + _LOGGER.warning("[test_form] result Starting") result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" - assert result["errors"] == {} + _LOGGER.warning("[test_form] result: %s", result) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - - with patch( - "custom_components.keymaster.config_flow._get_entities", - return_value="['lock.kwikset_touchpad_electronic_deadbolt_frontdoor']", - ), patch( - "custom_components.keymaster.async_setup", return_value=True - ) as mock_setup, patch( - "custom_components.keymaster.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], input_1 - ) - assert result2["type"] == "form" - assert result2["errors"] == {CONF_PATH: "invalid_path"} - - -@pytest.mark.parametrize( - "input_1,title,data", - [ - ( - { - "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", - "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", - "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", - "lockname": "sidedoor", - "packages_path": "packages/keymaster", - "sensorname": "binary_sensor.frontdoor", - "slots": 4, - "start_from": 1, - "parent": "(none)", - }, - "frontdoor", - { - "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", - "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", - "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", - "lockname": "sidedoor", - "packages_path": "packages/keymaster", - "sensorname": "binary_sensor.frontdoor", - "slots": 4, - "start_from": 1, - "hide_pins": False, - "parent": None, - }, - ), - ], -) -async def test_options_flow(input_1, title, data, hass, mock_get_entities): - """Test config flow options.""" - _LOGGER.error(_get_schema(hass, CONFIG_DATA, KeyMasterFlowHandler.DEFAULTS)) - entry = MockConfigEntry( - domain=DOMAIN, - title="frontdoor", - data=_get_schema(hass, CONFIG_DATA, KeyMasterFlowHandler.DEFAULTS)(CONFIG_DATA), - version=2, - ) - - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == "form" - assert result["step_id"] == "init" assert result["errors"] == {} with patch( - "custom_components.keymaster.async_setup", return_value=True - ) as mock_setup, patch( - "custom_components.keymaster.async_setup_entry", - return_value=True, + "custom_components.keymaster.async_setup_entry", return_value=True ) as mock_setup_entry: - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], input_1 - ) - assert result2["type"] == "create_entry" - - await hass.async_block_till_done() - assert entry.data.copy() == data - - -@pytest.mark.parametrize( - "input_1,title,data", - [ - ( - { - "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", - "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", - "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", - "lockname": "sidedoor", - "packages_path": "packages/keymaster_test", - "sensorname": "binary_sensor.frontdoor", - "slots": 4, - "start_from": 1, - "parent": "(none)", - }, - "frontdoor", - { - "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", - "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", - "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", - "lockname": "sidedoor", - "packages_path": "packages/keymaster_test", - "sensorname": "binary_sensor.frontdoor", - "slots": 4, - "start_from": 1, - "hide_pins": False, - "parent": None, - }, - ), - ], -) -async def test_options_flow_path_change(input_1, title, data, hass, mock_get_entities): - """Test config flow options.""" - _LOGGER.error(_get_schema(hass, CONFIG_DATA, KeyMasterFlowHandler.DEFAULTS)) - entry = MockConfigEntry( - domain=DOMAIN, - title="frontdoor", - data=_get_schema(hass, CONFIG_DATA, KeyMasterFlowHandler.DEFAULTS)(CONFIG_DATA), - version=2, - ) - - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == "form" - assert result["step_id"] == "init" - assert result["errors"] == {} - - with patch( - "custom_components.keymaster.async_setup", return_value=True - ) as mock_setup, patch( - "custom_components.keymaster.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], input_1 + _LOGGER.warning("[test_form] result2 Starting") + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], test_user_input ) - assert result2["type"] == "create_entry" + _LOGGER.warning("[test_form] result2: %s", result2) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == title + assert result2["data"] == final_config_flow_data await hass.async_block_till_done() - assert entry.data.copy() == data - - -@pytest.mark.parametrize( - "input_1,title,data", - [ - ( - { - "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", - "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", - "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", - "lockname": "sidedoor", - "packages_path": "packages/keymaster", - "sensorname": "binary_sensor.frontdoor", - "slots": 4, - "start_from": 1, - "parent": "(none)", - }, - "frontdoor", - { - "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", - "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", - "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", - "lockname": "sidedoor", - "packages_path": "packages/keymaster", - "sensorname": "binary_sensor.frontdoor", - "slots": 4, - "start_from": 1, - "hide_pins": False, - "parent": None, - }, - ), - ], -) -async def test_options_flow_with_zwavejs( - input_1, title, data, hass, mock_get_entities, client, lock_kwikset_910, integration -): - """Test config flow options.""" - - # Load ZwaveJS - node = lock_kwikset_910 - state = hass.states.get(KWIKSET_910_LOCK_ENTITY) - - _LOGGER.error(_get_schema(hass, CONFIG_DATA, KeyMasterFlowHandler.DEFAULTS)) - entry = MockConfigEntry( - domain=DOMAIN, - title="frontdoor", - data=_get_schema(hass, CONFIG_DATA, KeyMasterFlowHandler.DEFAULTS)(CONFIG_DATA), - version=2, - ) - - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == "form" - assert result["step_id"] == "init" - assert result["errors"] == {} - - with patch( - "custom_components.keymaster.async_setup", return_value=True - ) as mock_setup, patch( - "custom_components.keymaster.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + assert len(mock_setup_entry.mock_calls) == 1 - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], input_1 - ) - assert result2["type"] == "create_entry" - await hass.async_block_till_done() - assert entry.data.copy() == data +# @pytest.mark.parametrize( +# ("input_1", "title", "data"), +# [ +# ( +# { +# "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", +# "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", +# "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", +# "lockname": "sidedoor", +# "sensorname": "binary_sensor.frontdoor", +# "slots": 4, +# "start_from": 1, +# "parent": "(none)", +# }, +# "frontdoor", +# { +# "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", +# "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", +# "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", +# "lockname": "sidedoor", +# "sensorname": "binary_sensor.frontdoor", +# "slots": 4, +# "start_from": 1, +# "hide_pins": False, +# "parent": None, +# } +# ) +# ] +# ) +# async def test_options_flow(input_1, title, data, hass, mock_get_entities): +# """Test config flow options.""" +# _LOGGER.error(_get_schema(hass, CONFIG_DATA, KeymasterFlowHandler.DEFAULTS)) +# entry = MockConfigEntry( +# domain=DOMAIN, +# title="frontdoor", +# data=_get_schema(hass, CONFIG_DATA, KeymasterFlowHandler.DEFAULTS)(CONFIG_DATA), +# version=3, +# ) + +# entry.add_to_hass(hass) +# assert await hass.config_entries.async_setup(entry.entry_id) +# await hass.async_block_till_done() + +# result = await hass.config_entries.options.async_init(entry.entry_id) + +# assert result["type"] == "form" +# assert result["step_id"] == "init" +# assert result["errors"] == {} + +# with patch( +# "custom_components.keymaster.async_setup_entry", +# return_value=True, +# ): + +# result2 = await hass.config_entries.options.async_configure( +# result["flow_id"], input_1 +# ) +# assert result2["type"] == "create_entry" + +# await hass.async_block_till_done() +# assert entry.data.copy() == data + + +# @pytest.mark.parametrize( +# "input_1,title,data", +# [ +# ( +# { +# "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", +# "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", +# "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", +# "lockname": "sidedoor", +# "sensorname": "binary_sensor.frontdoor", +# "slots": 4, +# "start_from": 1, +# "parent": "(none)", +# }, +# "frontdoor", +# { +# "alarm_level_or_user_code_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_level_frontdoor", +# "alarm_type_or_access_control_entity_id": "sensor.kwikset_touchpad_electronic_deadbolt_alarm_type_frontdoor", +# "lock_entity_id": "lock.kwikset_touchpad_electronic_deadbolt_frontdoor", +# "lockname": "sidedoor", +# "sensorname": "binary_sensor.frontdoor", +# "slots": 4, +# "start_from": 1, +# "hide_pins": False, +# "parent": None, +# }, +# ), +# ], +# ) +# async def test_options_flow_with_zwavejs( +# input_1, title, data, hass, mock_get_entities, client, lock_kwikset_910, integration +# ): +# """Test config flow options.""" + +# # Load ZwaveJS +# node = lock_kwikset_910 +# state = hass.states.get(KWIKSET_910_LOCK_ENTITY) + +# _LOGGER.error(_get_schema(hass, CONFIG_DATA, KeymasterFlowHandler.DEFAULTS)) +# entry = MockConfigEntry( +# domain=DOMAIN, +# title="frontdoor", +# data=_get_schema(hass, CONFIG_DATA, KeymasterFlowHandler.DEFAULTS)(CONFIG_DATA), +# version=3, +# ) + +# entry.add_to_hass(hass) +# assert await hass.config_entries.async_setup(entry.entry_id) +# await hass.async_block_till_done() + +# result = await hass.config_entries.options.async_init(entry.entry_id) + +# assert result["type"] == "form" +# assert result["step_id"] == "init" +# assert result["errors"] == {} + +# with patch( +# "custom_components.keymaster.async_setup_entry", +# return_value=True, +# ): + +# result2 = await hass.config_entries.options.async_configure( +# result["flow_id"], input_1 +# ) +# assert result2["type"] == "create_entry" + +# await hass.async_block_till_done() +# assert entry.data.copy() == data async def test_get_entities(hass, lock_kwikset_910, client, integration): """Test function that returns entities by domain.""" # Load ZwaveJS - node = lock_kwikset_910 + # node = lock_kwikset_910 state = hass.states.get(KWIKSET_910_LOCK_ENTITY) assert state is not None - assert state.state == "locked" + assert state.state == LockState.LOCKED assert KWIKSET_910_LOCK_ENTITY in _get_entities(hass, LOCK_DOMAIN) diff --git a/tests/test_helpers.py b/tests/test_helpers.py.old similarity index 71% rename from tests/test_helpers.py rename to tests/test_helpers.py.old index f4d8141f..0b6d2810 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py.old @@ -1,7 +1,5 @@ """ Test keymaster helpers """ -from unittest.mock import patch - from pytest_homeassistant_custom_component.common import MockConfigEntry from zwave_js_server.event import Event @@ -13,54 +11,16 @@ DOMAIN, EVENT_KEYMASTER_LOCK_STATE_CHANGED, ) -from custom_components.keymaster.helpers import delete_lock_and_base_folder -from homeassistant.const import ( - ATTR_STATE, - EVENT_HOMEASSISTANT_STARTED, - STATE_LOCKED, - STATE_UNLOCKED, -) +from homeassistant.components.lock.const import LockState +from homeassistant.const import ATTR_STATE, EVENT_HOMEASSISTANT_STARTED from .common import async_capture_events -from .const import CONFIG_DATA, CONFIG_DATA_910 +from .const import CONFIG_DATA_910 SCHLAGE_BE469_LOCK_ENTITY = "lock.touchscreen_deadbolt_current_lock_mode" KWIKSET_910_LOCK_ENTITY = "lock.smart_code_with_home_connect_technology" -async def test_delete_lock_and_base_folder(hass, mock_osrmdir, mock_osremove): - """Test delete_lock_and_base_folder""" - entry = MockConfigEntry( - domain=DOMAIN, title="frontdoor", data=CONFIG_DATA, version=2 - ) - - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - delete_lock_and_base_folder(hass, entry) - - assert mock_osrmdir.called - assert mock_osremove.called - - -async def test_delete_lock_and_base_folder_error( - hass, mock_osrmdir, mock_osremove, mock_listdir_err -): - """Test delete_lock_and_base_folder""" - entry = MockConfigEntry( - domain=DOMAIN, title="frontdoor", data=CONFIG_DATA, version=2 - ) - - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - delete_lock_and_base_folder(hass, entry) - - assert mock_osrmdir.called_once - - async def test_handle_state_change_zwave_js( hass, client, lock_kwikset_910, integration ): @@ -69,14 +29,14 @@ async def test_handle_state_change_zwave_js( node = lock_kwikset_910 state = hass.states.get(KWIKSET_910_LOCK_ENTITY) assert state - assert state.state == STATE_LOCKED + assert state.state == LockState.LOCKED events = async_capture_events(hass, EVENT_KEYMASTER_LOCK_STATE_CHANGED) events_js = async_capture_events(hass, "zwave_js_notification") # Load the integration config_entry = MockConfigEntry( - domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_910, version=2 + domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_910, version=3 ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -111,7 +71,7 @@ async def test_handle_state_change_zwave_js( }, ) node.receive_event(event) - assert hass.states.get(KWIKSET_910_LOCK_ENTITY).state == STATE_UNLOCKED + assert hass.states.get(KWIKSET_910_LOCK_ENTITY).state == LockState.UNLOCKED client.async_send_command.reset_mock() # Fire zwave_js event diff --git a/tests/test_init.py b/tests/test_init.py.old similarity index 82% rename from tests/test_init.py rename to tests/test_init.py.old index cc39d573..ef65210d 100644 --- a/tests/test_init.py +++ b/tests/test_init.py.old @@ -7,9 +7,9 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.keymaster.const import DOMAIN -from homeassistant import setup from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, STATE_LOCKED +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .common import async_fire_time_changed @@ -22,12 +22,16 @@ _LOGGER = logging.getLogger(__name__) -async def test_setup_entry(hass, mock_generate_package_files): +async def test_setup_entry( + hass, + mock_zwavejs_get_usercodes, + mock_zwavejs_clear_usercode, + mock_zwavejs_set_usercode, +): """Test setting up entities.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( - domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_REAL, version=2 + domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_REAL, version=3 ) entry.add_to_hass(hass) @@ -39,12 +43,16 @@ async def test_setup_entry(hass, mock_generate_package_files): assert len(entries) == 1 -async def test_setup_entry_core_state(hass, mock_generate_package_files): +async def test_setup_entry_core_state( + hass, + mock_zwavejs_get_usercodes, + mock_zwavejs_clear_usercode, + mock_zwavejs_set_usercode, +): """Test setting up entities.""" with patch.object(hass, "state", return_value="STARTING"): - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( - domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_REAL, version=2 + domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_REAL, version=3 ) entry.add_to_hass(hass) @@ -58,14 +66,12 @@ async def test_setup_entry_core_state(hass, mock_generate_package_files): async def test_unload_entry( hass, - mock_delete_folder, - mock_delete_lock_and_base_folder, + mock_async_call_later, ): """Test unloading entities.""" - - await setup.async_setup_component(hass, "persistent_notification", {}) + now = dt_util.now() entry = MockConfigEntry( - domain=DOMAIN, title="frontdoor", data=CONFIG_DATA, version=2 + domain=DOMAIN, title="frontdoor", data=CONFIG_DATA, version=3 ) entry.add_to_hass(hass) @@ -86,7 +92,7 @@ async def test_unload_entry( assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 -async def test_setup_migration_with_old_path(hass, mock_generate_package_files): +async def test_setup_migration_with_old_path(hass): """Test setting up entities with old path""" with patch.object(hass.config, "path", return_value="/config"): entry = MockConfigEntry( @@ -104,12 +110,14 @@ async def test_setup_migration_with_old_path(hass, mock_generate_package_files): async def test_setup_entry_alt_slots( hass, - mock_generate_package_files, client, lock_kwikset_910, integration, mock_zwavejs_get_usercodes, + mock_zwavejs_clear_usercode, + mock_zwavejs_set_usercode, mock_using_zwavejs, + mock_async_call_later, caplog, ): """Test setting up entities with alternate slot setting.""" @@ -122,15 +130,20 @@ async def test_setup_entry_alt_slots( assert state assert state.state == STATE_LOCKED - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( - domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_ALT_SLOTS, version=2 + domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_ALT_SLOTS, version=3 + ) + await async_setup_component( + hass=hass, + domain=DOMAIN, + config=CONFIG_DATA_ALT_SLOTS, ) - - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + # entry.add_to_hass(hass) + # assert await hass.config_entries.async_setup(entry.entry_id) + # await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 7 entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -156,3 +169,7 @@ async def test_setup_entry_alt_slots( assert hass.states.get(SENSOR_CHECK_2).state == "1234" assert "DEBUG: Code slot 12 not enabled" in caplog.text + + assert await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 diff --git a/tests/test_package_files.py b/tests/test_package_files.py.old similarity index 100% rename from tests/test_package_files.py rename to tests/test_package_files.py.old diff --git a/tests/test_services.py b/tests/test_services.py.old similarity index 79% rename from tests/test_services.py rename to tests/test_services.py.old index 597dda2e..c94223da 100644 --- a/tests/test_services.py +++ b/tests/test_services.py.old @@ -6,63 +6,57 @@ import pytest from pytest_homeassistant_custom_component.common import MockConfigEntry -from custom_components.keymaster import ( - SERVICE_ADD_CODE, - SERVICE_CLEAR_CODE, - SERVICE_GENERATE_PACKAGE, - SERVICE_REFRESH_CODES, -) from custom_components.keymaster.const import DOMAIN +from custom_components.keymaster.services import ( + SERVICE_CLEAR_PIN, + SERVICE_REGENERATE_LOVELACE, + SERVICE_UPDATE_PIN, +) -from .const import CONFIG_DATA, CONFIG_DATA_910, CONFIG_DATA_ALT +from .const import CONFIG_DATA, CONFIG_DATA_910 KWIKSET_910_LOCK_ENTITY = "lock.smart_code_with_home_connect_technology" -async def test_generate_package_files(hass, caplog): +async def test_service_regenerate_lovelace(hass, caplog): """Test generate_package_files""" entry = MockConfigEntry( - domain=DOMAIN, title="frontdoor", data=CONFIG_DATA, version=2 + domain=DOMAIN, title="frontdoor", data=CONFIG_DATA, version=3 ) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - servicedata = { - "lockname": "backdoor", - } + servicedata = {} with pytest.raises(ValueError): await hass.services.async_call( - DOMAIN, SERVICE_GENERATE_PACKAGE, servicedata, blocking=True + DOMAIN, SERVICE_REGENERATE_LOVELACE, servicedata, blocking=True ) await hass.async_block_till_done() # Check for exception when unable to create directory - with patch( - "custom_components.keymaster.services.os", autospec=True - ) as mock_os, patch( - "custom_components.keymaster.services.output_to_file_from_template" + with ( + patch("custom_components.keymaster.services.os", autospec=True) as mock_os, + patch("custom_components.keymaster.services.output_to_file_from_template"), ): mock_os.path.isdir.return_value = False mock_os.makedirs.side_effect = OSError(errno.EEXIST, "error") - servicedata = { - "lockname": "frontdoor", - } - await hass.services.async_call(DOMAIN, SERVICE_GENERATE_PACKAGE, servicedata) + servicedata = {} + await hass.services.async_call(DOMAIN, SERVICE_REGENERATE_LOVELACE, servicedata) await hass.async_block_till_done() mock_os.path.isdir.assert_called_once mock_os.makedirs.assert_called_once assert "Error creating directory:" in caplog.text -async def test_add_code_zwave_js(hass, client, lock_kwikset_910, integration): +async def test_service_update_pin(hass, client, lock_kwikset_910, integration): """Test refresh_codes""" node = lock_kwikset_910 entry = MockConfigEntry( - domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_910, version=2 + domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_910, version=3 ) entry.add_to_hass(hass) @@ -81,7 +75,7 @@ async def test_add_code_zwave_js(hass, client, lock_kwikset_910, integration): "code_slot": 1, "usercode": "1234", } - await hass.services.async_call(DOMAIN, SERVICE_ADD_CODE, servicedata) + await hass.services.async_call(DOMAIN, SERVICE_UPDATE_PIN, servicedata) await hass.async_block_till_done() assert len(client.async_send_command.call_args_list) == 1 @@ -110,13 +104,13 @@ async def test_add_code_zwave_js(hass, client, lock_kwikset_910, integration): assert args["value"] == "1234" -async def test_clear_code_zwave_js(hass, client, lock_kwikset_910, integration): +async def test_service_clear_pin(hass, client, lock_kwikset_910, integration): """Test refresh_codes""" node = lock_kwikset_910 entry = MockConfigEntry( - domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_910, version=2 + domain=DOMAIN, title="frontdoor", data=CONFIG_DATA_910, version=3 ) entry.add_to_hass(hass) @@ -134,7 +128,7 @@ async def test_clear_code_zwave_js(hass, client, lock_kwikset_910, integration): "entity_id": KWIKSET_910_LOCK_ENTITY, "code_slot": 1, } - await hass.services.async_call(DOMAIN, SERVICE_CLEAR_CODE, servicedata) + await hass.services.async_call(DOMAIN, SERVICE_CLEAR_PIN, servicedata) await hass.async_block_till_done() assert len(client.async_send_command.call_args_list) == 1 diff --git a/tests/yaml/frontdoor/frontdoor_keymaster_1.yaml b/tests/yaml/frontdoor/frontdoor_keymaster_1.yaml deleted file mode 100644 index d6bcc988..00000000 --- a/tests/yaml/frontdoor/frontdoor_keymaster_1.yaml +++ /dev/null @@ -1,320 +0,0 @@ - -############ input_number: ##################### -input_number: - accesscount_frontdoor_1: - name: 'Unlock events' - min: 0 - max: 100 - step: 1 - mode: box - -################# input_datetime: ############## -input_datetime: - end_date_frontdoor_1: - name: 'End' - has_time: false - has_date: true - start_date_frontdoor_1: - name: 'Start' - has_time: false - has_date: true - - sun_start_date_frontdoor_1: - name: 'Start' - has_time: true - has_date: false - sun_end_date_frontdoor_1: - name: 'End' - has_time: true - has_date: false - - mon_start_date_frontdoor_1: - name: 'Start' - has_time: true - has_date: false - mon_end_date_frontdoor_1: - name: 'End' - has_time: true - has_date: false - - tue_start_date_frontdoor_1: - name: 'Start' - has_time: true - has_date: false - tue_end_date_frontdoor_1: - name: 'End' - has_time: true - has_date: false - - wed_start_date_frontdoor_1: - name: 'Start' - has_time: true - has_date: false - wed_end_date_frontdoor_1: - name: 'End' - has_time: true - has_date: false - - thu_start_date_frontdoor_1: - name: 'Start' - has_time: true - has_date: false - thu_end_date_frontdoor_1: - name: 'End' - has_time: true - has_date: false - - fri_start_date_frontdoor_1: - name: 'Start' - has_time: true - has_date: false - fri_end_date_frontdoor_1: - name: 'End' - has_time: true - has_date: false - - sat_start_date_frontdoor_1: - name: 'Start' - has_time: true - has_date: false - sat_end_date_frontdoor_1: - name: 'End' - has_time: true - has_date: false - - -#################### input_text: ############### -input_text: - frontdoor_name_1: - name: 'Name' - frontdoor_pin_1: - name: 'PIN' - mode: text - -################# input_boolean: ################ -input_boolean: - notify_frontdoor_1: - name: 'Notifications' - daterange_frontdoor_1: - name: 'Use Date Range' - smtwtfs_frontdoor_1: - name: 'Use SMTWTFS' - enabled_frontdoor_1: - name: 'Enabled' - accesslimit_frontdoor_1: - name: 'Enforce PIN limit' - initial: off - reset_codeslot_frontdoor_1: - name: 'Reset Code Slot' - initial: off - - sun_frontdoor_1: - name: 'Sunday' - initial: on - - mon_frontdoor_1: - name: 'Monday' - initial: on - - tue_frontdoor_1: - name: 'Tuesday' - initial: on - - wed_frontdoor_1: - name: 'Wednesday' - initial: on - - thu_frontdoor_1: - name: 'Thursday' - initial: on - - fri_frontdoor_1: - name: 'Friday' - initial: on - - sat_frontdoor_1: - name: 'Saturday' - initial: on - - sun_inc_frontdoor_1: - name: 'include (on)/exclude (off)' - initial: on - - mon_inc_frontdoor_1: - name: 'include (on)/exclude (off)' - initial: on - - tue_inc_frontdoor_1: - name: 'include (on)/exclude (off)' - initial: on - - wed_inc_frontdoor_1: - name: 'include (on)/exclude (off)' - initial: on - - thu_inc_frontdoor_1: - name: 'include (on)/exclude (off)' - initial: on - - fri_inc_frontdoor_1: - name: 'include (on)/exclude (off)' - initial: on - - sat_inc_frontdoor_1: - name: 'include (on)/exclude (off)' - initial: on - -################ automation: ################# -automation: - -- alias: synchronize_codeslot_frontdoor_1 - initial_state: true - trigger: - - platform: state - entity_id: "binary_sensor.pin_synched_frontdoor_1" - to: 'off' - - platform: state - entity_id: "input_boolean.allow_automation_execution" - to: 'on' - - platform: state - entity_id: "sensor.frontdoor_code_slot_1" - condition: - - condition: state - entity_id: "input_boolean.allow_automation_execution" - state: "on" - - condition: state - entity_id: "binary_sensor.pin_synched_frontdoor_1" - state: "off" - - condition: template - value_template: "{{ not is_state('sensor.frontdoor_code_slot_1', 'unavailable') }}" - action: - - choose: - - # The code should be added to the lock's slot - - conditions: - - condition: template - value_template: "{{ is_state('binary_sensor.active_frontdoor_1', 'on') }}" - sequence: - - service: keymaster.add_code - data_template: - entity_id: lock.smartcode_10_touchpad_electronic_deadbolt_locked - code_slot: "{{ 1 }}" - usercode: "{{ states('input_text.frontdoor_pin_1').strip() }}" - - # The code should be removed from the lock's slot - - conditions: - - condition: template - value_template: "{{ is_state('binary_sensor.active_frontdoor_1', 'off') }}" - sequence: - - service: keymaster.clear_code - data_template: - entity_id: lock.smartcode_10_touchpad_electronic_deadbolt_locked - code_slot: "{{ 1 }}" - -- alias: reset_codeslot_frontdoor_1 - trigger: - entity_id: input_boolean.reset_codeslot_frontdoor_1 - platform: state - to: 'on' - action: - - service: script.reset_codeslot_frontdoor - data_template: - code_slot: 1 - -################ binary_sensor: ################# -binary_sensor: - -- platform: template - sensors: - - active_frontdoor_1: - friendly_name: "Desired PIN State" - value_template: >- - {## This template checks whether the PIN should be considered active based on ##} - {## all of the different ways the PIN can be conditionally disabled ##} - - {% set now = now() %} - - {% set current_day = now.strftime('%a')[0:3] | lower %} - {% set current_date = now.strftime('%Y%m%d') | int %} - {% set current_time = now.strftime('%H%M') | int %} - - {% set start_date = states('input_datetime.start_date_frontdoor_1').replace('-', '') | int %} - {% set end_date = states('input_datetime.end_date_frontdoor_1').replace('-', '') | int %} - {% set current_day_start_time = (states('input_datetime.' + current_day + '_start_date_frontdoor_1')[0:5]).replace(':', '') | int %} - {% set current_day_end_time = (states('input_datetime.' + current_day + '_end_date_frontdoor_1')[0:5]).replace(':', '') | int %} - - {% set is_slot_active = is_state('input_boolean.enabled_frontdoor_1', 'on') %} - {% set is_current_day_active = is_state('input_boolean.' + current_day + '_frontdoor_1', 'on') %} - - {% set is_date_range_enabled = is_state('input_boolean.daterange_frontdoor_1', 'on') %} - {% set is_in_date_range = (current_date >= start_date and current_date <= end_date) %} - - {% set is_time_range_enabled = (current_day_start_time != current_day_end_time) %} - {% set is_time_range_inclusive = is_state('input_boolean.' + current_day + '_inc_frontdoor_1', 'on') %} - {% set is_in_time_range = ( - (is_time_range_inclusive and (current_time >= current_day_start_time and current_time <= current_day_end_time)) - or - (not is_time_range_inclusive and (current_time < current_day_start_time or current_time > current_day_end_time)) - ) %} - - {% set is_access_limit_enabled = is_state('input_boolean.accesslimit_frontdoor_1', 'on') %} - {% set is_access_count_valid = states('input_number.accesscount_frontdoor_1') | int > 0 %} - - {{ - is_slot_active and is_current_day_active - and - (not is_date_range_enabled or is_in_date_range) - and - (not is_time_range_enabled or is_in_time_range) - and - (not is_access_limit_enabled or is_access_count_valid) - }} - - pin_synched_frontdoor_1: - friendly_name: 'PIN synchronized with lock' - value_template: > - {% set lockpin = states('sensor.frontdoor_code_slot_1') %} - {% if is_state('binary_sensor.active_frontdoor_1', 'on') %} - {{ is_state('input_text.frontdoor_pin_1', lockpin) }} - {% else %} - {{ lockpin in ("", "0000") }} - {% endif %} - -################### sensor: #################### -sensor: - -- platform: template - sensors: - - connected_frontdoor_1: - # icon: mdi:glassdoor - friendly_name: "PIN Status" - value_template: >- - {% set value_map = { - True: { - True: 'Connected', - False: 'Adding', - }, - False: { - True: 'Disconnected', - False: 'Deleting', - }, - } %} - {% set slot_active = is_state('binary_sensor.active_frontdoor_1', 'on') %} - {% set pin_synched = is_state('binary_sensor.pin_synched_frontdoor_1', 'on') %} - {{ value_map[slot_active][pin_synched] }} - icon_template: > - {% set icon_map = { - True: { - True: 'mdi:folder-key', - False: 'mdi:folder-key-network', - }, - False: { - True: 'mdi:folder-open', - False: 'mdi:wiper-watch', - }, - } %} - {% set slot_active = is_state('binary_sensor.active_frontdoor_1', 'on') %} - {% set pin_synched = is_state('binary_sensor.pin_synched_frontdoor_1', 'on') %} - {{ icon_map[slot_active][pin_synched] }} \ No newline at end of file diff --git a/tests/yaml/frontdoor/frontdoor_keymaster_2.yaml b/tests/yaml/frontdoor/frontdoor_keymaster_2.yaml deleted file mode 100644 index f1d3240c..00000000 --- a/tests/yaml/frontdoor/frontdoor_keymaster_2.yaml +++ /dev/null @@ -1,320 +0,0 @@ - -############ input_number: ##################### -input_number: - accesscount_frontdoor_2: - name: 'Unlock events' - min: 0 - max: 100 - step: 1 - mode: box - -################# input_datetime: ############## -input_datetime: - end_date_frontdoor_2: - name: 'End' - has_time: false - has_date: true - start_date_frontdoor_2: - name: 'Start' - has_time: false - has_date: true - - sun_start_date_frontdoor_2: - name: 'Start' - has_time: true - has_date: false - sun_end_date_frontdoor_2: - name: 'End' - has_time: true - has_date: false - - mon_start_date_frontdoor_2: - name: 'Start' - has_time: true - has_date: false - mon_end_date_frontdoor_2: - name: 'End' - has_time: true - has_date: false - - tue_start_date_frontdoor_2: - name: 'Start' - has_time: true - has_date: false - tue_end_date_frontdoor_2: - name: 'End' - has_time: true - has_date: false - - wed_start_date_frontdoor_2: - name: 'Start' - has_time: true - has_date: false - wed_end_date_frontdoor_2: - name: 'End' - has_time: true - has_date: false - - thu_start_date_frontdoor_2: - name: 'Start' - has_time: true - has_date: false - thu_end_date_frontdoor_2: - name: 'End' - has_time: true - has_date: false - - fri_start_date_frontdoor_2: - name: 'Start' - has_time: true - has_date: false - fri_end_date_frontdoor_2: - name: 'End' - has_time: true - has_date: false - - sat_start_date_frontdoor_2: - name: 'Start' - has_time: true - has_date: false - sat_end_date_frontdoor_2: - name: 'End' - has_time: true - has_date: false - - -#################### input_text: ############### -input_text: - frontdoor_name_2: - name: 'Name' - frontdoor_pin_2: - name: 'PIN' - mode: text - -################# input_boolean: ################ -input_boolean: - notify_frontdoor_2: - name: 'Notifications' - daterange_frontdoor_2: - name: 'Use Date Range' - smtwtfs_frontdoor_2: - name: 'Use SMTWTFS' - enabled_frontdoor_2: - name: 'Enabled' - accesslimit_frontdoor_2: - name: 'Enforce PIN limit' - initial: off - reset_codeslot_frontdoor_2: - name: 'Reset Code Slot' - initial: off - - sun_frontdoor_2: - name: 'Sunday' - initial: on - - mon_frontdoor_2: - name: 'Monday' - initial: on - - tue_frontdoor_2: - name: 'Tuesday' - initial: on - - wed_frontdoor_2: - name: 'Wednesday' - initial: on - - thu_frontdoor_2: - name: 'Thursday' - initial: on - - fri_frontdoor_2: - name: 'Friday' - initial: on - - sat_frontdoor_2: - name: 'Saturday' - initial: on - - sun_inc_frontdoor_2: - name: 'include (on)/exclude (off)' - initial: on - - mon_inc_frontdoor_2: - name: 'include (on)/exclude (off)' - initial: on - - tue_inc_frontdoor_2: - name: 'include (on)/exclude (off)' - initial: on - - wed_inc_frontdoor_2: - name: 'include (on)/exclude (off)' - initial: on - - thu_inc_frontdoor_2: - name: 'include (on)/exclude (off)' - initial: on - - fri_inc_frontdoor_2: - name: 'include (on)/exclude (off)' - initial: on - - sat_inc_frontdoor_2: - name: 'include (on)/exclude (off)' - initial: on - -################ automation: ################# -automation: - -- alias: synchronize_codeslot_frontdoor_2 - initial_state: true - trigger: - - platform: state - entity_id: "binary_sensor.pin_synched_frontdoor_2" - to: 'off' - - platform: state - entity_id: "input_boolean.allow_automation_execution" - to: 'on' - - platform: state - entity_id: "sensor.frontdoor_code_slot_2" - condition: - - condition: state - entity_id: "input_boolean.allow_automation_execution" - state: "on" - - condition: state - entity_id: "binary_sensor.pin_synched_frontdoor_2" - state: "off" - - condition: template - value_template: "{{ not is_state('sensor.frontdoor_code_slot_2', 'unavailable') }}" - action: - - choose: - - # The code should be added to the lock's slot - - conditions: - - condition: template - value_template: "{{ is_state('binary_sensor.active_frontdoor_2', 'on') }}" - sequence: - - service: keymaster.add_code - data_template: - entity_id: lock.smartcode_10_touchpad_electronic_deadbolt_locked - code_slot: "{{ 2 }}" - usercode: "{{ states('input_text.frontdoor_pin_2').strip() }}" - - # The code should be removed from the lock's slot - - conditions: - - condition: template - value_template: "{{ is_state('binary_sensor.active_frontdoor_2', 'off') }}" - sequence: - - service: keymaster.clear_code - data_template: - entity_id: lock.smartcode_10_touchpad_electronic_deadbolt_locked - code_slot: "{{ 2 }}" - -- alias: reset_codeslot_frontdoor_2 - trigger: - entity_id: input_boolean.reset_codeslot_frontdoor_2 - platform: state - to: 'on' - action: - - service: script.reset_codeslot_frontdoor - data_template: - code_slot: 2 - -################ binary_sensor: ################# -binary_sensor: - -- platform: template - sensors: - - active_frontdoor_2: - friendly_name: "Desired PIN State" - value_template: >- - {## This template checks whether the PIN should be considered active based on ##} - {## all of the different ways the PIN can be conditionally disabled ##} - - {% set now = now() %} - - {% set current_day = now.strftime('%a')[0:3] | lower %} - {% set current_date = now.strftime('%Y%m%d') | int %} - {% set current_time = now.strftime('%H%M') | int %} - - {% set start_date = states('input_datetime.start_date_frontdoor_2').replace('-', '') | int %} - {% set end_date = states('input_datetime.end_date_frontdoor_2').replace('-', '') | int %} - {% set current_day_start_time = (states('input_datetime.' + current_day + '_start_date_frontdoor_2')[0:5]).replace(':', '') | int %} - {% set current_day_end_time = (states('input_datetime.' + current_day + '_end_date_frontdoor_2')[0:5]).replace(':', '') | int %} - - {% set is_slot_active = is_state('input_boolean.enabled_frontdoor_2', 'on') %} - {% set is_current_day_active = is_state('input_boolean.' + current_day + '_frontdoor_2', 'on') %} - - {% set is_date_range_enabled = is_state('input_boolean.daterange_frontdoor_2', 'on') %} - {% set is_in_date_range = (current_date >= start_date and current_date <= end_date) %} - - {% set is_time_range_enabled = (current_day_start_time != current_day_end_time) %} - {% set is_time_range_inclusive = is_state('input_boolean.' + current_day + '_inc_frontdoor_2', 'on') %} - {% set is_in_time_range = ( - (is_time_range_inclusive and (current_time >= current_day_start_time and current_time <= current_day_end_time)) - or - (not is_time_range_inclusive and (current_time < current_day_start_time or current_time > current_day_end_time)) - ) %} - - {% set is_access_limit_enabled = is_state('input_boolean.accesslimit_frontdoor_2', 'on') %} - {% set is_access_count_valid = states('input_number.accesscount_frontdoor_2') | int > 0 %} - - {{ - is_slot_active and is_current_day_active - and - (not is_date_range_enabled or is_in_date_range) - and - (not is_time_range_enabled or is_in_time_range) - and - (not is_access_limit_enabled or is_access_count_valid) - }} - - pin_synched_frontdoor_2: - friendly_name: 'PIN synchronized with lock' - value_template: > - {% set lockpin = states('sensor.frontdoor_code_slot_2') %} - {% if is_state('binary_sensor.active_frontdoor_2', 'on') %} - {{ is_state('input_text.frontdoor_pin_2', lockpin) }} - {% else %} - {{ lockpin in ("", "0000") }} - {% endif %} - -################### sensor: #################### -sensor: - -- platform: template - sensors: - - connected_frontdoor_2: - # icon: mdi:glassdoor - friendly_name: "PIN Status" - value_template: >- - {% set value_map = { - True: { - True: 'Connected', - False: 'Adding', - }, - False: { - True: 'Disconnected', - False: 'Deleting', - }, - } %} - {% set slot_active = is_state('binary_sensor.active_frontdoor_2', 'on') %} - {% set pin_synched = is_state('binary_sensor.pin_synched_frontdoor_2', 'on') %} - {{ value_map[slot_active][pin_synched] }} - icon_template: > - {% set icon_map = { - True: { - True: 'mdi:folder-key', - False: 'mdi:folder-key-network', - }, - False: { - True: 'mdi:folder-open', - False: 'mdi:wiper-watch', - }, - } %} - {% set slot_active = is_state('binary_sensor.active_frontdoor_2', 'on') %} - {% set pin_synched = is_state('binary_sensor.pin_synched_frontdoor_2', 'on') %} - {{ icon_map[slot_active][pin_synched] }} \ No newline at end of file diff --git a/tests/yaml/frontdoor/frontdoor_keymaster_3.yaml b/tests/yaml/frontdoor/frontdoor_keymaster_3.yaml deleted file mode 100644 index 11223a2c..00000000 --- a/tests/yaml/frontdoor/frontdoor_keymaster_3.yaml +++ /dev/null @@ -1,320 +0,0 @@ - -############ input_number: ##################### -input_number: - accesscount_frontdoor_3: - name: 'Unlock events' - min: 0 - max: 100 - step: 1 - mode: box - -################# input_datetime: ############## -input_datetime: - end_date_frontdoor_3: - name: 'End' - has_time: false - has_date: true - start_date_frontdoor_3: - name: 'Start' - has_time: false - has_date: true - - sun_start_date_frontdoor_3: - name: 'Start' - has_time: true - has_date: false - sun_end_date_frontdoor_3: - name: 'End' - has_time: true - has_date: false - - mon_start_date_frontdoor_3: - name: 'Start' - has_time: true - has_date: false - mon_end_date_frontdoor_3: - name: 'End' - has_time: true - has_date: false - - tue_start_date_frontdoor_3: - name: 'Start' - has_time: true - has_date: false - tue_end_date_frontdoor_3: - name: 'End' - has_time: true - has_date: false - - wed_start_date_frontdoor_3: - name: 'Start' - has_time: true - has_date: false - wed_end_date_frontdoor_3: - name: 'End' - has_time: true - has_date: false - - thu_start_date_frontdoor_3: - name: 'Start' - has_time: true - has_date: false - thu_end_date_frontdoor_3: - name: 'End' - has_time: true - has_date: false - - fri_start_date_frontdoor_3: - name: 'Start' - has_time: true - has_date: false - fri_end_date_frontdoor_3: - name: 'End' - has_time: true - has_date: false - - sat_start_date_frontdoor_3: - name: 'Start' - has_time: true - has_date: false - sat_end_date_frontdoor_3: - name: 'End' - has_time: true - has_date: false - - -#################### input_text: ############### -input_text: - frontdoor_name_3: - name: 'Name' - frontdoor_pin_3: - name: 'PIN' - mode: text - -################# input_boolean: ################ -input_boolean: - notify_frontdoor_3: - name: 'Notifications' - daterange_frontdoor_3: - name: 'Use Date Range' - smtwtfs_frontdoor_3: - name: 'Use SMTWTFS' - enabled_frontdoor_3: - name: 'Enabled' - accesslimit_frontdoor_3: - name: 'Enforce PIN limit' - initial: off - reset_codeslot_frontdoor_3: - name: 'Reset Code Slot' - initial: off - - sun_frontdoor_3: - name: 'Sunday' - initial: on - - mon_frontdoor_3: - name: 'Monday' - initial: on - - tue_frontdoor_3: - name: 'Tuesday' - initial: on - - wed_frontdoor_3: - name: 'Wednesday' - initial: on - - thu_frontdoor_3: - name: 'Thursday' - initial: on - - fri_frontdoor_3: - name: 'Friday' - initial: on - - sat_frontdoor_3: - name: 'Saturday' - initial: on - - sun_inc_frontdoor_3: - name: 'include (on)/exclude (off)' - initial: on - - mon_inc_frontdoor_3: - name: 'include (on)/exclude (off)' - initial: on - - tue_inc_frontdoor_3: - name: 'include (on)/exclude (off)' - initial: on - - wed_inc_frontdoor_3: - name: 'include (on)/exclude (off)' - initial: on - - thu_inc_frontdoor_3: - name: 'include (on)/exclude (off)' - initial: on - - fri_inc_frontdoor_3: - name: 'include (on)/exclude (off)' - initial: on - - sat_inc_frontdoor_3: - name: 'include (on)/exclude (off)' - initial: on - -################ automation: ################# -automation: - -- alias: synchronize_codeslot_frontdoor_3 - initial_state: true - trigger: - - platform: state - entity_id: "binary_sensor.pin_synched_frontdoor_3" - to: 'off' - - platform: state - entity_id: "input_boolean.allow_automation_execution" - to: 'on' - - platform: state - entity_id: "sensor.frontdoor_code_slot_3" - condition: - - condition: state - entity_id: "input_boolean.allow_automation_execution" - state: "on" - - condition: state - entity_id: "binary_sensor.pin_synched_frontdoor_3" - state: "off" - - condition: template - value_template: "{{ not is_state('sensor.frontdoor_code_slot_3', 'unavailable') }}" - action: - - choose: - - # The code should be added to the lock's slot - - conditions: - - condition: template - value_template: "{{ is_state('binary_sensor.active_frontdoor_3', 'on') }}" - sequence: - - service: keymaster.add_code - data_template: - entity_id: lock.smartcode_10_touchpad_electronic_deadbolt_locked - code_slot: "{{ 3 }}" - usercode: "{{ states('input_text.frontdoor_pin_3').strip() }}" - - # The code should be removed from the lock's slot - - conditions: - - condition: template - value_template: "{{ is_state('binary_sensor.active_frontdoor_3', 'off') }}" - sequence: - - service: keymaster.clear_code - data_template: - entity_id: lock.smartcode_10_touchpad_electronic_deadbolt_locked - code_slot: "{{ 3 }}" - -- alias: reset_codeslot_frontdoor_3 - trigger: - entity_id: input_boolean.reset_codeslot_frontdoor_3 - platform: state - to: 'on' - action: - - service: script.reset_codeslot_frontdoor - data_template: - code_slot: 3 - -################ binary_sensor: ################# -binary_sensor: - -- platform: template - sensors: - - active_frontdoor_3: - friendly_name: "Desired PIN State" - value_template: >- - {## This template checks whether the PIN should be considered active based on ##} - {## all of the different ways the PIN can be conditionally disabled ##} - - {% set now = now() %} - - {% set current_day = now.strftime('%a')[0:3] | lower %} - {% set current_date = now.strftime('%Y%m%d') | int %} - {% set current_time = now.strftime('%H%M') | int %} - - {% set start_date = states('input_datetime.start_date_frontdoor_3').replace('-', '') | int %} - {% set end_date = states('input_datetime.end_date_frontdoor_3').replace('-', '') | int %} - {% set current_day_start_time = (states('input_datetime.' + current_day + '_start_date_frontdoor_3')[0:5]).replace(':', '') | int %} - {% set current_day_end_time = (states('input_datetime.' + current_day + '_end_date_frontdoor_3')[0:5]).replace(':', '') | int %} - - {% set is_slot_active = is_state('input_boolean.enabled_frontdoor_3', 'on') %} - {% set is_current_day_active = is_state('input_boolean.' + current_day + '_frontdoor_3', 'on') %} - - {% set is_date_range_enabled = is_state('input_boolean.daterange_frontdoor_3', 'on') %} - {% set is_in_date_range = (current_date >= start_date and current_date <= end_date) %} - - {% set is_time_range_enabled = (current_day_start_time != current_day_end_time) %} - {% set is_time_range_inclusive = is_state('input_boolean.' + current_day + '_inc_frontdoor_3', 'on') %} - {% set is_in_time_range = ( - (is_time_range_inclusive and (current_time >= current_day_start_time and current_time <= current_day_end_time)) - or - (not is_time_range_inclusive and (current_time < current_day_start_time or current_time > current_day_end_time)) - ) %} - - {% set is_access_limit_enabled = is_state('input_boolean.accesslimit_frontdoor_3', 'on') %} - {% set is_access_count_valid = states('input_number.accesscount_frontdoor_3') | int > 0 %} - - {{ - is_slot_active and is_current_day_active - and - (not is_date_range_enabled or is_in_date_range) - and - (not is_time_range_enabled or is_in_time_range) - and - (not is_access_limit_enabled or is_access_count_valid) - }} - - pin_synched_frontdoor_3: - friendly_name: 'PIN synchronized with lock' - value_template: > - {% set lockpin = states('sensor.frontdoor_code_slot_3') %} - {% if is_state('binary_sensor.active_frontdoor_3', 'on') %} - {{ is_state('input_text.frontdoor_pin_3', lockpin) }} - {% else %} - {{ lockpin in ("", "0000") }} - {% endif %} - -################### sensor: #################### -sensor: - -- platform: template - sensors: - - connected_frontdoor_3: - # icon: mdi:glassdoor - friendly_name: "PIN Status" - value_template: >- - {% set value_map = { - True: { - True: 'Connected', - False: 'Adding', - }, - False: { - True: 'Disconnected', - False: 'Deleting', - }, - } %} - {% set slot_active = is_state('binary_sensor.active_frontdoor_3', 'on') %} - {% set pin_synched = is_state('binary_sensor.pin_synched_frontdoor_3', 'on') %} - {{ value_map[slot_active][pin_synched] }} - icon_template: > - {% set icon_map = { - True: { - True: 'mdi:folder-key', - False: 'mdi:folder-key-network', - }, - False: { - True: 'mdi:folder-open', - False: 'mdi:wiper-watch', - }, - } %} - {% set slot_active = is_state('binary_sensor.active_frontdoor_3', 'on') %} - {% set pin_synched = is_state('binary_sensor.pin_synched_frontdoor_3', 'on') %} - {{ icon_map[slot_active][pin_synched] }} \ No newline at end of file diff --git a/tests/yaml/frontdoor/frontdoor_keymaster_4.yaml b/tests/yaml/frontdoor/frontdoor_keymaster_4.yaml deleted file mode 100644 index 5a38f617..00000000 --- a/tests/yaml/frontdoor/frontdoor_keymaster_4.yaml +++ /dev/null @@ -1,320 +0,0 @@ - -############ input_number: ##################### -input_number: - accesscount_frontdoor_4: - name: 'Unlock events' - min: 0 - max: 100 - step: 1 - mode: box - -################# input_datetime: ############## -input_datetime: - end_date_frontdoor_4: - name: 'End' - has_time: false - has_date: true - start_date_frontdoor_4: - name: 'Start' - has_time: false - has_date: true - - sun_start_date_frontdoor_4: - name: 'Start' - has_time: true - has_date: false - sun_end_date_frontdoor_4: - name: 'End' - has_time: true - has_date: false - - mon_start_date_frontdoor_4: - name: 'Start' - has_time: true - has_date: false - mon_end_date_frontdoor_4: - name: 'End' - has_time: true - has_date: false - - tue_start_date_frontdoor_4: - name: 'Start' - has_time: true - has_date: false - tue_end_date_frontdoor_4: - name: 'End' - has_time: true - has_date: false - - wed_start_date_frontdoor_4: - name: 'Start' - has_time: true - has_date: false - wed_end_date_frontdoor_4: - name: 'End' - has_time: true - has_date: false - - thu_start_date_frontdoor_4: - name: 'Start' - has_time: true - has_date: false - thu_end_date_frontdoor_4: - name: 'End' - has_time: true - has_date: false - - fri_start_date_frontdoor_4: - name: 'Start' - has_time: true - has_date: false - fri_end_date_frontdoor_4: - name: 'End' - has_time: true - has_date: false - - sat_start_date_frontdoor_4: - name: 'Start' - has_time: true - has_date: false - sat_end_date_frontdoor_4: - name: 'End' - has_time: true - has_date: false - - -#################### input_text: ############### -input_text: - frontdoor_name_4: - name: 'Name' - frontdoor_pin_4: - name: 'PIN' - mode: text - -################# input_boolean: ################ -input_boolean: - notify_frontdoor_4: - name: 'Notifications' - daterange_frontdoor_4: - name: 'Use Date Range' - smtwtfs_frontdoor_4: - name: 'Use SMTWTFS' - enabled_frontdoor_4: - name: 'Enabled' - accesslimit_frontdoor_4: - name: 'Enforce PIN limit' - initial: off - reset_codeslot_frontdoor_4: - name: 'Reset Code Slot' - initial: off - - sun_frontdoor_4: - name: 'Sunday' - initial: on - - mon_frontdoor_4: - name: 'Monday' - initial: on - - tue_frontdoor_4: - name: 'Tuesday' - initial: on - - wed_frontdoor_4: - name: 'Wednesday' - initial: on - - thu_frontdoor_4: - name: 'Thursday' - initial: on - - fri_frontdoor_4: - name: 'Friday' - initial: on - - sat_frontdoor_4: - name: 'Saturday' - initial: on - - sun_inc_frontdoor_4: - name: 'include (on)/exclude (off)' - initial: on - - mon_inc_frontdoor_4: - name: 'include (on)/exclude (off)' - initial: on - - tue_inc_frontdoor_4: - name: 'include (on)/exclude (off)' - initial: on - - wed_inc_frontdoor_4: - name: 'include (on)/exclude (off)' - initial: on - - thu_inc_frontdoor_4: - name: 'include (on)/exclude (off)' - initial: on - - fri_inc_frontdoor_4: - name: 'include (on)/exclude (off)' - initial: on - - sat_inc_frontdoor_4: - name: 'include (on)/exclude (off)' - initial: on - -################ automation: ################# -automation: - -- alias: synchronize_codeslot_frontdoor_4 - initial_state: true - trigger: - - platform: state - entity_id: "binary_sensor.pin_synched_frontdoor_4" - to: 'off' - - platform: state - entity_id: "input_boolean.allow_automation_execution" - to: 'on' - - platform: state - entity_id: "sensor.frontdoor_code_slot_4" - condition: - - condition: state - entity_id: "input_boolean.allow_automation_execution" - state: "on" - - condition: state - entity_id: "binary_sensor.pin_synched_frontdoor_4" - state: "off" - - condition: template - value_template: "{{ not is_state('sensor.frontdoor_code_slot_4', 'unavailable') }}" - action: - - choose: - - # The code should be added to the lock's slot - - conditions: - - condition: template - value_template: "{{ is_state('binary_sensor.active_frontdoor_4', 'on') }}" - sequence: - - service: keymaster.add_code - data_template: - entity_id: lock.smartcode_10_touchpad_electronic_deadbolt_locked - code_slot: "{{ 4 }}" - usercode: "{{ states('input_text.frontdoor_pin_4').strip() }}" - - # The code should be removed from the lock's slot - - conditions: - - condition: template - value_template: "{{ is_state('binary_sensor.active_frontdoor_4', 'off') }}" - sequence: - - service: keymaster.clear_code - data_template: - entity_id: lock.smartcode_10_touchpad_electronic_deadbolt_locked - code_slot: "{{ 4 }}" - -- alias: reset_codeslot_frontdoor_4 - trigger: - entity_id: input_boolean.reset_codeslot_frontdoor_4 - platform: state - to: 'on' - action: - - service: script.reset_codeslot_frontdoor - data_template: - code_slot: 4 - -################ binary_sensor: ################# -binary_sensor: - -- platform: template - sensors: - - active_frontdoor_4: - friendly_name: "Desired PIN State" - value_template: >- - {## This template checks whether the PIN should be considered active based on ##} - {## all of the different ways the PIN can be conditionally disabled ##} - - {% set now = now() %} - - {% set current_day = now.strftime('%a')[0:3] | lower %} - {% set current_date = now.strftime('%Y%m%d') | int %} - {% set current_time = now.strftime('%H%M') | int %} - - {% set start_date = states('input_datetime.start_date_frontdoor_4').replace('-', '') | int %} - {% set end_date = states('input_datetime.end_date_frontdoor_4').replace('-', '') | int %} - {% set current_day_start_time = (states('input_datetime.' + current_day + '_start_date_frontdoor_4')[0:5]).replace(':', '') | int %} - {% set current_day_end_time = (states('input_datetime.' + current_day + '_end_date_frontdoor_4')[0:5]).replace(':', '') | int %} - - {% set is_slot_active = is_state('input_boolean.enabled_frontdoor_4', 'on') %} - {% set is_current_day_active = is_state('input_boolean.' + current_day + '_frontdoor_4', 'on') %} - - {% set is_date_range_enabled = is_state('input_boolean.daterange_frontdoor_4', 'on') %} - {% set is_in_date_range = (current_date >= start_date and current_date <= end_date) %} - - {% set is_time_range_enabled = (current_day_start_time != current_day_end_time) %} - {% set is_time_range_inclusive = is_state('input_boolean.' + current_day + '_inc_frontdoor_4', 'on') %} - {% set is_in_time_range = ( - (is_time_range_inclusive and (current_time >= current_day_start_time and current_time <= current_day_end_time)) - or - (not is_time_range_inclusive and (current_time < current_day_start_time or current_time > current_day_end_time)) - ) %} - - {% set is_access_limit_enabled = is_state('input_boolean.accesslimit_frontdoor_4', 'on') %} - {% set is_access_count_valid = states('input_number.accesscount_frontdoor_4') | int > 0 %} - - {{ - is_slot_active and is_current_day_active - and - (not is_date_range_enabled or is_in_date_range) - and - (not is_time_range_enabled or is_in_time_range) - and - (not is_access_limit_enabled or is_access_count_valid) - }} - - pin_synched_frontdoor_4: - friendly_name: 'PIN synchronized with lock' - value_template: > - {% set lockpin = states('sensor.frontdoor_code_slot_4') %} - {% if is_state('binary_sensor.active_frontdoor_4', 'on') %} - {{ is_state('input_text.frontdoor_pin_4', lockpin) }} - {% else %} - {{ lockpin in ("", "0000") }} - {% endif %} - -################### sensor: #################### -sensor: - -- platform: template - sensors: - - connected_frontdoor_4: - # icon: mdi:glassdoor - friendly_name: "PIN Status" - value_template: >- - {% set value_map = { - True: { - True: 'Connected', - False: 'Adding', - }, - False: { - True: 'Disconnected', - False: 'Deleting', - }, - } %} - {% set slot_active = is_state('binary_sensor.active_frontdoor_4', 'on') %} - {% set pin_synched = is_state('binary_sensor.pin_synched_frontdoor_4', 'on') %} - {{ value_map[slot_active][pin_synched] }} - icon_template: > - {% set icon_map = { - True: { - True: 'mdi:folder-key', - False: 'mdi:folder-key-network', - }, - False: { - True: 'mdi:folder-open', - False: 'mdi:wiper-watch', - }, - } %} - {% set slot_active = is_state('binary_sensor.active_frontdoor_4', 'on') %} - {% set pin_synched = is_state('binary_sensor.pin_synched_frontdoor_4', 'on') %} - {{ icon_map[slot_active][pin_synched] }} \ No newline at end of file diff --git a/tests/yaml/frontdoor/frontdoor_keymaster_common.yaml b/tests/yaml/frontdoor/frontdoor_keymaster_common.yaml deleted file mode 100644 index b8654e62..00000000 --- a/tests/yaml/frontdoor/frontdoor_keymaster_common.yaml +++ /dev/null @@ -1,243 +0,0 @@ -## WARNING ## -# This file is auotmaticly generated, any changes -# will be overwritten. - -################################################## -################ COMMON ENTITIES ############### -################################################## - -############### input_boolean: ################# -input_boolean: - frontdoor_lock_notifications: - name: frontdoor Lock Notifications - frontdoor_dooraccess_notifications: - name: frontdoor Door Notifications - frontdoor_reset_lock: - name: frontdoor reset lock - -################### script: #################### -script: - frontdoor_reset_lock: - sequence: - - service: script.frontdoor_manual_notify - data_template: - title: "reset" - message: "frontdoor" - - frontdoor_refreshnodeinfo: - description: 'Send MQTT RefreshNodeInfo command' - sequence: - - service: system_log.write - data_template: - message: "frontdoor started noderefreshinfo: {{ now() }}" - level: debug - - service: mqtt.publish - data: - topic: 'OpenZWave/1/command/refreshnodeinfo/' - payload: >- - { "node": {{ state_attr('lock.smartcode_10_touchpad_electronic_deadbolt_locked','node_id') }} } - retain: true - - reset_codeslot_frontdoor: - fields: - code_slot: - description: The code slot to reset - example: 1 - variables: - # Constant used later to loop through day specific entities - days: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] - sequence: - - service: input_text.set_value - data_template: - entity_id: "input_text.frontdoor_name_{{ code_slot | string }}" - value: "" - - service: input_text.set_value - data_template: - entity_id: "input_text.frontdoor_pin_{{ code_slot | string }}" - value: "" - - service: input_boolean.turn_off - data_template: - entity_id: "input_boolean.notify_frontdoor_{{ code_slot | string }}" - - service: input_boolean.turn_off - data_template: - entity_id: "input_boolean.enabled_frontdoor_{{ code_slot | string }}" - - service: input_number.set_value - data_template: - entity_id: "input_number.accesscount_frontdoor_{{ code_slot | string }}" - value: "0" - - service: input_datetime.set_datetime - data_template: - entity_id: "input_datetime.start_date_frontdoor_{{ code_slot | string }}" - date: > - {{ (("1980-01-01") | timestamp_custom("%Y %m %d")) }} - - service: input_datetime.set_datetime - data_template: - entity_id: "input_datetime.end_date_frontdoor_{{ code_slot | string }}" - date: > - {{ (("1980-01-01") | timestamp_custom("%Y %m %d")) }} - - service: input_boolean.turn_off - data_template: - entity_id: "input_boolean.daterange_frontdoor_{{ code_slot | string }}" - - service: input_boolean.turn_off - data_template: - entity_id: "input_boolean.accesslimit_frontdoor_{{ code_slot | string }}" - - service: input_boolean.turn_off - data_template: - entity_id: "input_boolean.reset_codeslot_frontdoor_{{ code_slot | string }}" - # Loop through each day of the week and reset the entities related to each one - - repeat: - count: 7 - sequence: - - service: input_datetime.set_datetime - data_template: - entity_id: "input_datetime.{{ days[repeat.index - 1] }}_start_date_frontdoor_{{ code_slot | string }}" - time: "{{ '00:00' | timestamp_custom('%H:%M') }}" - - service: input_datetime.set_datetime - data_template: - entity_id: "input_datetime.{{ days[repeat.index - 1] }}_end_date_frontdoor_{{ code_slot | string }}" - time: "{{ '00:00' | timestamp_custom('%H:%M') }}" - - service: input_boolean.turn_on - data_template: - entity_id: "input_boolean.{{ days[repeat.index - 1] }}_frontdoor_{{ code_slot | string }}" - - service: input_boolean.turn_on - data_template: - entity_id: "input_boolean.{{ days[repeat.index - 1] }}_inc_frontdoor_{{ code_slot | string }}" - -################### automation: #################### -automation: - - alias: frontdoor Lock Notifications - trigger: - platform: event - event_type: keymaster_lock_state_changed - event_data: - lockname: frontdoor - condition: - - condition: state - entity_id: "input_boolean.allow_automation_execution" - state: "on" - - condition: state - entity_id: "input_boolean.frontdoor_lock_notifications" - state: "on" - action: - - service: script.frontdoor_manual_notify - data_template: - title: frontdoor - message: "{{ trigger.event.data.action_text }}" - - - alias: frontdoor User Notifications - trigger: - platform: event - event_type: keymaster_lock_state_changed - event_data: - lockname: frontdoor - condition: - - condition: state - entity_id: "input_boolean.allow_automation_execution" - state: "on" - - condition: template - value_template: "{{ trigger.event.data.code_slot > 0 }}" - - condition: template - value_template: "{{ is_state('input_boolean.notify_frontdoor_' + trigger.event.data.code_slot | string, 'on') }}" - action: - - service: script.frontdoor_manual_notify - data_template: - title: "{{ trigger.event.data.action_text }}" - message: "{{ trigger.event.data.code_slot_name }}" - - - alias: frontdoor Sensor Closed - trigger: - entity_id: binary_sensor.fake - platform: state - to: "off" - condition: - - condition: state - entity_id: "input_boolean.allow_automation_execution" - state: "on" - - condition: state - entity_id: "input_boolean.frontdoor_dooraccess_notifications" - state: "on" - action: - - service: script.frontdoor_manual_notify - data_template: - title: frontdoor - message: "Closed" - - - alias: frontdoor Sensor Opened - trigger: - entity_id: binary_sensor.fake - platform: state - to: "on" - condition: - - condition: state - entity_id: "input_boolean.allow_automation_execution" - state: "on" - - condition: state - entity_id: "input_boolean.frontdoor_dooraccess_notifications" - state: "on" - action: - - service: script.frontdoor_manual_notify - data_template: - title: frontdoor - message: "Opened" - - - alias: frontdoor Changed Code - trigger: - entity_id: input_text.frontdoor_pin_1,input_text.frontdoor_pin_2,input_text.frontdoor_pin_3,input_text.frontdoor_pin_4 - platform: state - condition: - - condition: state - entity_id: "input_boolean.allow_automation_execution" - state: "on" - - condition: template - value_template: >- - {{ - is_state('input_boolean.enabled_frontdoor_' + trigger.entity_id.split('_')[-1:], 'on') - and - (trigger.from_state.state != trigger.to_state.state) - }} - action: - - service: persistent_notification.create - data_template: - title: frontdoor LOCK MANAGER - message: >- - {{ 'You changed the PIN for code ' + trigger.entity_id.split('_')[-1:] + '. Please enable it in order to make it active.'}} - - service: input_boolean.turn_off - data_template: - entity_id: >- - {{ 'input_boolean.enabled_frontdoor_' + trigger.entity_id.split('_')[-1:] }} - - - alias: frontdoor Reset - condition: - - condition: state - entity_id: "input_boolean.allow_automation_execution" - state: "on" - trigger: - entity_id: input_boolean.frontdoor_reset_lock - platform: state - from: "off" - to: "on" - action: - - service: script.frontdoor_reset_lock - - service: input_boolean.turn_off - entity_id: input_boolean.frontdoor_reset_lock - - - alias: frontdoor Decrement Access Count - trigger: - platform: event - event_type: keymaster_lock_state_changed - event_data: - lockname: frontdoor - condition: - - condition: state - entity_id: "input_boolean.allow_automation_execution" - state: "on" - - condition: template - # make sure decrementing access entries is enabled - value_template: "{{ is_state('input_boolean.accesslimit_frontdoor_' + trigger.event.data.code_slot | string, 'on') }}" - - condition: template - # Check for Keypad Unlock code - value_template: "{{ trigger.event.data.code_slot > 0 and trigger.event.data.action_code in (6, 19)}}" - action: - - service: input_number.decrement - data_template: - entity_id: "{{ 'input_number.accesscount_frontdoor_' + trigger.event.data.code_slot | string }}" diff --git a/tox.ini b/tox.ini deleted file mode 100644 index d3491069..00000000 --- a/tox.ini +++ /dev/null @@ -1,36 +0,0 @@ -[tox] -skipsdist = true -envlist = py310, py311, py312, lint, mypy -skip_missing_interpreters = True - -[gh-actions] -python = - 3.10: py310 - 3.11: py311 - 3.12: py312, lint, mypy - -[testenv] -commands = - pytest --asyncio-mode=auto --timeout=30 --cov=custom_components/keymaster --cov-report=xml {posargs} -deps = - -rrequirements_test.txt - -[testenv:lint] -basepython = python3 -ignore_errors = True -commands = - black --check custom_components/ - black --check tests/ - flake8 custom_components/keymaster - pylint custom_components/keymaster - pydocstyle custom_components/keymaster tests -deps = - -rrequirements_test.txt - -[testenv:mypy] -basepython = python3 -ignore_errors = True -commands = - mypy custom_components/keymaster -deps = - -rrequirements_test.txt