diff --git a/.coveragerc b/.coveragerc index 8a8b31fa6e3..abd29a8614b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -54,6 +54,7 @@ omit = homeassistant/components/august/* homeassistant/components/aurora_abb_powerone/sensor.py homeassistant/components/automatic/device_tracker.py + homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/azure_event_hub/* homeassistant/components/baidu/tts.py @@ -122,6 +123,7 @@ omit = homeassistant/components/ddwrt/device_tracker.py homeassistant/components/decora/light.py homeassistant/components/decora_wifi/light.py + homeassistant/components/delijn/* homeassistant/components/deluge/sensor.py homeassistant/components/deluge/switch.py homeassistant/components/denon/media_player.py @@ -210,6 +212,7 @@ omit = homeassistant/components/folder/sensor.py homeassistant/components/folder_watcher/* homeassistant/components/foobot/sensor.py + homeassistant/components/fortios/device_tracker.py homeassistant/components/fortigate/* homeassistant/components/foscam/camera.py homeassistant/components/foursquare/* @@ -464,6 +467,7 @@ omit = homeassistant/components/plaato/* homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py + homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/* @@ -473,7 +477,6 @@ omit = homeassistant/components/prometheus/* homeassistant/components/prowl/notify.py homeassistant/components/proxy/camera.py - homeassistant/components/ps4/media_player.py homeassistant/components/ptvsd/* homeassistant/components/pulseaudio_loopback/switch.py homeassistant/components/pushbullet/notify.py @@ -499,6 +502,7 @@ omit = homeassistant/components/rainmachine/binary_sensor.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py + homeassistant/components/rainforest_eagle/sensor.py homeassistant/components/raspihats/* homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/sensor.py @@ -588,6 +592,7 @@ omit = homeassistant/components/stiebel_eltron/* homeassistant/components/streamlabswater/* homeassistant/components/stride/notify.py + homeassistant/components/suez_water/* homeassistant/components/supervisord/sensor.py homeassistant/components/swiss_hydrological_data/sensor.py homeassistant/components/swiss_public_transport/sensor.py @@ -667,11 +672,20 @@ omit = homeassistant/components/usps/* homeassistant/components/vallox/* homeassistant/components/vasttrafik/sensor.py - homeassistant/components/velbus/* + homeassistant/components/velbus/__init__.py + homeassistant/components/velbus/binary_sensor.py + homeassistant/components/velbus/climate.py + homeassistant/components/velbus/const.py + homeassistant/components/velbus/cover.py + homeassistant/components/velbus/sensor.py + homeassistant/components/velbus/switch.py homeassistant/components/velux/* homeassistant/components/venstar/climate.py homeassistant/components/vera/* homeassistant/components/verisure/* + homeassistant/components/vesync/__init__.py + homeassistant/components/vesync/common.py + homeassistant/components/vesync/const.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vizio/media_player.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 767094b4c20..c85eaece8b6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,8 +17,18 @@ "python.pythonPath": "/usr/local/bin/python", "python.linting.pylintEnabled": true, "python.linting.enabled": true, + "python.formatting.provider": "black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, "files.trimTrailingWhitespace": true, - "editor.rulers": [80], - "terminal.integrated.shell.linux": "/bin/bash" + "terminal.integrated.shell.linux": "/bin/bash", + "yaml.customTags": [ + "!secret scalar", + "!include_dir_named scalar", + "!include_dir_list scalar", + "!include_dir_merge_list scalar", + "!include_dir_merge_named scalar" + ] } } diff --git a/.gitignore b/.gitignore index 9c3afdd9091..ff3e8d838a3 100644 --- a/.gitignore +++ b/.gitignore @@ -114,6 +114,7 @@ desktop.ini # mypy /.mypy_cache/* +/.dmypy.json # Secrets .lokalise_token diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000000..5134f5f14aa --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +repos: +- repo: https://github.com/python/black + rev: 19.3b0 + hooks: + - id: black + args: + - --safe + - --quiet diff --git a/.travis.yml b/.travis.yml index 4167b1c9923..f54f4027de4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,14 +16,14 @@ addons: matrix: fast_finish: true include: - - python: "3.5.3" + - python: "3.6" env: TOXENV=lint - - python: "3.5.3" + - python: "3.6" env: TOXENV=pylint - - python: "3.5.3" + - python: "3.6" env: TOXENV=typing - - python: "3.5.3" - env: TOXENV=py35 + - python: "3.6" + env: TOXENV=py36 - python: "3.7" env: TOXENV=py37 diff --git a/CODEOWNERS b/CODEOWNERS index d9b485a3c19..2d68eecf160 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -35,6 +35,7 @@ homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core homeassistant/components/automatic/* @armills homeassistant/components/automation/* @home-assistant/core +homeassistant/components/avea/* @pattyland homeassistant/components/awair/* @danielsjf homeassistant/components/aws/* @awarecan @robbiet480 homeassistant/components/axis/* @kane610 @@ -42,12 +43,11 @@ homeassistant/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blink/* @fronzbot -homeassistant/components/bmw_connected_drive/* @ChristianKuehnel homeassistant/components/braviatv/* @robbiet480 homeassistant/components/broadlink/* @danielhiversen homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bt_smarthub/* @jxwolstenholme -homeassistant/components/buienradar/* @ties +homeassistant/components/buienradar/* @mjj4791 @ties homeassistant/components/cisco_ios/* @fbradyirl homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl @@ -65,6 +65,7 @@ homeassistant/components/cups/* @fabaff homeassistant/components/daikin/* @fredrike @rofrantz homeassistant/components/darksky/* @fabaff homeassistant/components/deconz/* @kane610 +homeassistant/components/delijn/* @bollewolle homeassistant/components/demo/* @home-assistant/core homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/digital_ocean/* @fabaff @@ -93,6 +94,7 @@ homeassistant/components/fixer/* @fabaff homeassistant/components/flock/* @fabaff homeassistant/components/flunearyou/* @bachya homeassistant/components/fortigate/* @kifeo +homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foursquare/* @robbiet480 homeassistant/components/freebox/* @snoof85 homeassistant/components/fronius/* @nielstron @@ -202,6 +204,7 @@ homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel +homeassistant/components/plugwise/* @laetificat @CoMPaTech homeassistant/components/point/* @fredrike homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig @@ -212,6 +215,7 @@ homeassistant/components/qnap/* @colinodell homeassistant/components/quantum_gateway/* @cisasteelersfan homeassistant/components/qwikswitch/* @kellerza homeassistant/components/raincloud/* @vanstinator +homeassistant/components/rainforest_eagle/* @gtdiehl homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/repetier/* @MTrab @@ -246,6 +250,7 @@ homeassistant/components/sql/* @dgomes homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stream/* @hunterjm +homeassistant/components/suez_water/* @ooii homeassistant/components/sun/* @Swamp-Ig homeassistant/components/supla/* @mwegrzynek homeassistant/components/swiss_hydrological_data/* @fabaff @@ -283,8 +288,10 @@ homeassistant/components/updater/* @home-assistant/core homeassistant/components/upnp/* @robbiet480 homeassistant/components/uptimerobot/* @ludeeus homeassistant/components/utility_meter/* @dgomes +homeassistant/components/velbus/* @cereal2nd homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff +homeassistant/components/vesync/* @markperdue @webdjoe homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf homeassistant/components/waqi/* @andrey-git diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 904c3220663..1e3d914aaf0 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -4,14 +4,16 @@ trigger: batch: true branches: include: + - rc - dev + - master pr: + - rc - dev + - master resources: containers: - - container: 35 - image: homeassistant/ci-azure:3.5 - container: 36 image: homeassistant/ci-azure:3.6 - container: 37 @@ -20,7 +22,7 @@ variables: - name: ArtifactFeed value: '2df3ae11-3bf6-49bc-a809-ba0d340d6a6d' - name: PythonMain - value: '35' + value: '36' - group: codecov stages: @@ -36,7 +38,7 @@ stages: python -m venv venv . venv/bin/activate - pip install flake8 + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt displayName: 'Setup Env' - script: | . venv/bin/activate @@ -61,6 +63,21 @@ stages: . venv/bin/activate ./script/gen_requirements_all.py validate displayName: 'requirements_all validate' + - job: 'CheckFormat' + pool: + vmImage: 'ubuntu-latest' + container: $[ variables['PythonMain'] ] + steps: + - script: | + python -m venv venv + + . venv/bin/activate + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + displayName: 'Setup Env' + - script: | + . venv/bin/activate + ./script/check_format + displayName: 'Check Black formatting' - stage: 'Tests' dependsOn: @@ -72,8 +89,6 @@ stages: strategy: maxParallel: 3 matrix: - Python35: - python.container: '35' Python36: python.container: '36' Python37: @@ -86,43 +101,45 @@ stages: - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 displayName: 'Restore artifacts based on Requirements' inputs: - keyfile: 'requirements_test_all.txt, .cache' + keyfile: 'requirements_test_all.txt, .cache, homeassistant/package_constraints.txt' targetfolder: './venv' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: '$(ArtifactFeed)' - script: | set -e python -m venv venv - + . venv/bin/activate - pip install -U pip setuptools + pip install -U pip setuptools pytest-azurepipelines -c homeassistant/package_constraints.txt pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt - pip install pytest-azurepipelines -c homeassistant/package_constraints.txt + # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. + # Find offending deps with `pipdeptree -r -p typing` + pip uninstall -y typing displayName: 'Create Virtual Environment & Install Requirements' condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) # Explicit Cache Save (instead of using RestoreAndSaveCache) - # Dont wait with cache save for all the other task in this job to complete (±30 minutes), other parallel jobs might utilize this - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - displayName: 'Save artifacts based on Requirements' - inputs: - keyfile: 'requirements_test_all.txt, .cache' - targetfolder: './venv' + # Dont wait with cache save for all the other task in this job to complete (±30 minutes), other parallel jobs might utilize this + - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 + displayName: 'Save artifacts based on Requirements' + inputs: + keyfile: 'requirements_test_all.txt, .cache, homeassistant/package_constraints.txt' + targetfolder: './venv' vstsFeed: '$(ArtifactFeed)' - - script: | + - script: | . venv/bin/activate pip install -e . - displayName: 'Install Home Assistant for python $(python.container)' - - script: | + displayName: 'Install Home Assistant for python $(python.container)' + - script: | . venv/bin/activate pytest --timeout=9 --durations=10 --junitxml=test-results.xml -qq -o console_output_style=count -p no:sugar tests displayName: 'Run pytest for python $(python.container)' condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) - script: | + set -e + . venv/bin/activate pytest --timeout=9 --durations=10 --junitxml=test-results.xml --cov --cov-report=xml -qq -o console_output_style=count -p no:sugar tests - codecov + codecov --token $(codecovToken) displayName: 'Run pytest for python $(python.container) / coverage' - env: - CODECOV_TOKEN: '$(codecovToken)' condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain'])) - task: PublishTestResults@2 condition: succeededOrFailed() @@ -151,13 +168,13 @@ stages: - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 displayName: 'Restore artifacts based on Requirements' inputs: - keyfile: 'requirements_all.txt, requirements_test.txt, .cache' + keyfile: 'requirements_all.txt, requirements_test.txt, .cache, homeassistant/package_constraints.txt' targetfolder: './venv' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: '$(ArtifactFeed)' - script: | set -e python -m venv venv - + . venv/bin/activate pip install -U pip setuptools pip install -r requirements_all.txt -c homeassistant/package_constraints.txt @@ -167,13 +184,13 @@ stages: - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 displayName: 'Save artifacts based on Requirements' inputs: - keyfile: 'requirements_all.txt, requirements_test.txt, .cache' + keyfile: 'requirements_all.txt, requirements_test.txt, .cache, homeassistant/package_constraints.txt' targetfolder: './venv' vstsFeed: '$(ArtifactFeed)' - - script: | + - script: | . venv/bin/activate pip install -e . - displayName: 'Install Home Assistant for python $(PythonMain)' + displayName: 'Install Home Assistant for python $(PythonMain)' - script: | . venv/bin/activate pylint homeassistant @@ -187,10 +204,13 @@ stages: python -m venv venv . venv/bin/activate - pip install -r requirements_test.txt + pip install -e . + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt displayName: 'Setup Env' - script: | - . venv/bin/activate TYPING_FILES=$(cat mypyrc) + echo -e "Run mypy on: \n$TYPING_FILES" + + . venv/bin/activate mypy $TYPING_FILES displayName: 'Run mypy' diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index b75d5b6bee8..768e9627e4c 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -1,7 +1,6 @@ # https://dev.azure.com/home-assistant trigger: - batch: true tags: include: - '*' diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 023faadef0c..d21bfb5a71a 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -7,9 +7,7 @@ import platform import subprocess import sys import threading -from typing import ( # noqa pylint: disable=unused-import - List, Dict, Any, TYPE_CHECKING -) +from typing import List, Dict, Any, TYPE_CHECKING # noqa pylint: disable=unused-import from homeassistant import monkey_patch from homeassistant.const import ( @@ -30,11 +28,12 @@ def set_loop() -> None: policy = None - if sys.platform == 'win32': - if hasattr(asyncio, 'WindowsProactorEventLoopPolicy'): + if sys.platform == "win32": + if hasattr(asyncio, "WindowsProactorEventLoopPolicy"): # pylint: disable=no-member policy = asyncio.WindowsProactorEventLoopPolicy() else: + class ProactorPolicy(BaseDefaultEventLoopPolicy): """Event loop policy to create proactor loops.""" @@ -56,28 +55,40 @@ def set_loop() -> None: def validate_python() -> None: """Validate that the right Python version is running.""" if sys.version_info[:3] < REQUIRED_PYTHON_VER: - print("Home Assistant requires at least Python {}.{}.{}".format( - *REQUIRED_PYTHON_VER)) + print( + "Home Assistant requires at least Python {}.{}.{}".format( + *REQUIRED_PYTHON_VER + ) + ) sys.exit(1) def ensure_config_path(config_dir: str) -> None: """Validate the configuration directory.""" import homeassistant.config as config_util - lib_dir = os.path.join(config_dir, 'deps') + + lib_dir = os.path.join(config_dir, "deps") # Test if configuration directory exists if not os.path.isdir(config_dir): if config_dir != config_util.get_default_config_dir(): - print(('Fatal Error: Specified configuration directory does ' - 'not exist {} ').format(config_dir)) + print( + ( + "Fatal Error: Specified configuration directory does " + "not exist {} " + ).format(config_dir) + ) sys.exit(1) try: os.mkdir(config_dir) except OSError: - print(('Fatal Error: Unable to create default configuration ' - 'directory {} ').format(config_dir)) + print( + ( + "Fatal Error: Unable to create default configuration " + "directory {} " + ).format(config_dir) + ) sys.exit(1) # Test if library directory exists @@ -85,20 +96,22 @@ def ensure_config_path(config_dir: str) -> None: try: os.mkdir(lib_dir) except OSError: - print(('Fatal Error: Unable to create library ' - 'directory {} ').format(lib_dir)) + print( + ("Fatal Error: Unable to create library " "directory {} ").format( + lib_dir + ) + ) sys.exit(1) -async def ensure_config_file(hass: 'core.HomeAssistant', config_dir: str) \ - -> str: +async def ensure_config_file(hass: "core.HomeAssistant", config_dir: str) -> str: """Ensure configuration file exists.""" import homeassistant.config as config_util - config_path = await config_util.async_ensure_config_exists( - hass, config_dir) + + config_path = await config_util.async_ensure_config_exists(hass, config_dir) if config_path is None: - print('Error getting configuration path') + print("Error getting configuration path") sys.exit(1) return config_path @@ -107,71 +120,72 @@ async def ensure_config_file(hass: 'core.HomeAssistant', config_dir: str) \ def get_arguments() -> argparse.Namespace: """Get parsed passed in arguments.""" import homeassistant.config as config_util + parser = argparse.ArgumentParser( - description="Home Assistant: Observe, Control, Automate.") - parser.add_argument('--version', action='version', version=__version__) + description="Home Assistant: Observe, Control, Automate." + ) + parser.add_argument("--version", action="version", version=__version__) parser.add_argument( - '-c', '--config', - metavar='path_to_config_dir', + "-c", + "--config", + metavar="path_to_config_dir", default=config_util.get_default_config_dir(), - help="Directory that contains the Home Assistant configuration") + help="Directory that contains the Home Assistant configuration", + ) parser.add_argument( - '--demo-mode', - action='store_true', - help='Start Home Assistant in demo mode') + "--demo-mode", action="store_true", help="Start Home Assistant in demo mode" + ) parser.add_argument( - '--debug', - action='store_true', - help='Start Home Assistant in debug mode') + "--debug", action="store_true", help="Start Home Assistant in debug mode" + ) parser.add_argument( - '--open-ui', - action='store_true', - help='Open the webinterface in a browser') + "--open-ui", action="store_true", help="Open the webinterface in a browser" + ) parser.add_argument( - '--skip-pip', - action='store_true', - help='Skips pip install of required packages on startup') + "--skip-pip", + action="store_true", + help="Skips pip install of required packages on startup", + ) parser.add_argument( - '-v', '--verbose', - action='store_true', - help="Enable verbose logging to file.") + "-v", "--verbose", action="store_true", help="Enable verbose logging to file." + ) parser.add_argument( - '--pid-file', - metavar='path_to_pid_file', + "--pid-file", + metavar="path_to_pid_file", default=None, - help='Path to PID file useful for running as daemon') + help="Path to PID file useful for running as daemon", + ) parser.add_argument( - '--log-rotate-days', + "--log-rotate-days", type=int, default=None, - help='Enables daily log rotation and keeps up to the specified days') + help="Enables daily log rotation and keeps up to the specified days", + ) parser.add_argument( - '--log-file', + "--log-file", type=str, default=None, - help='Log file to write to. If not set, CONFIG/home-assistant.log ' - 'is used') + help="Log file to write to. If not set, CONFIG/home-assistant.log " "is used", + ) parser.add_argument( - '--log-no-color', - action='store_true', - help="Disable color logs") + "--log-no-color", action="store_true", help="Disable color logs" + ) parser.add_argument( - '--runner', - action='store_true', - help='On restart exit with code {}'.format(RESTART_EXIT_CODE)) + "--runner", + action="store_true", + help="On restart exit with code {}".format(RESTART_EXIT_CODE), + ) parser.add_argument( - '--script', - nargs=argparse.REMAINDER, - help='Run one of the embedded scripts') + "--script", nargs=argparse.REMAINDER, help="Run one of the embedded scripts" + ) if os.name == "posix": parser.add_argument( - '--daemon', - action='store_true', - help='Run Home Assistant as daemon') + "--daemon", action="store_true", help="Run Home Assistant as daemon" + ) arguments = parser.parse_args() if os.name != "posix" or arguments.debug or arguments.runner: - setattr(arguments, 'daemon', False) + setattr(arguments, "daemon", False) return arguments @@ -192,8 +206,8 @@ def daemonize() -> None: sys.exit(0) # redirect standard file descriptors to devnull - infd = open(os.devnull, 'r') - outfd = open(os.devnull, 'a+') + infd = open(os.devnull, "r") + outfd = open(os.devnull, "a+") sys.stdout.flush() sys.stderr.flush() os.dup2(infd.fileno(), sys.stdin.fileno()) @@ -205,7 +219,7 @@ def check_pid(pid_file: str) -> None: """Check that Home Assistant is not already running.""" # Check pid file try: - with open(pid_file, 'r') as file: + with open(pid_file, "r") as file: pid = int(file.readline()) except IOError: # PID File does not exist @@ -220,7 +234,7 @@ def check_pid(pid_file: str) -> None: except OSError: # PID does not exist return - print('Fatal Error: HomeAssistant is already running.') + print("Fatal Error: HomeAssistant is already running.") sys.exit(1) @@ -228,10 +242,10 @@ def write_pid(pid_file: str) -> None: """Create a PID File.""" pid = os.getpid() try: - with open(pid_file, 'w') as file: + with open(pid_file, "w") as file: file.write(str(pid)) except IOError: - print('Fatal Error: Unable to write pid file {}'.format(pid_file)) + print("Fatal Error: Unable to write pid file {}".format(pid_file)) sys.exit(1) @@ -255,17 +269,15 @@ def closefds_osx(min_fd: int, max_fd: int) -> None: def cmdline() -> List[str]: """Collect path and arguments to re-execute the current hass instance.""" - if os.path.basename(sys.argv[0]) == '__main__.py': + if os.path.basename(sys.argv[0]) == "__main__.py": modulepath = os.path.dirname(sys.argv[0]) - os.environ['PYTHONPATH'] = os.path.dirname(modulepath) - return [sys.executable] + [arg for arg in sys.argv if - arg != '--daemon'] + os.environ["PYTHONPATH"] = os.path.dirname(modulepath) + return [sys.executable] + [arg for arg in sys.argv if arg != "--daemon"] - return [arg for arg in sys.argv if arg != '--daemon'] + return [arg for arg in sys.argv if arg != "--daemon"] -async def setup_and_run_hass(config_dir: str, - args: argparse.Namespace) -> int: +async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: """Set up HASS and run.""" # pylint: disable=redefined-outer-name from homeassistant import bootstrap, core @@ -273,21 +285,29 @@ async def setup_and_run_hass(config_dir: str, hass = core.HomeAssistant() if args.demo_mode: - config = { - 'frontend': {}, - 'demo': {} - } # type: Dict[str, Any] + config = {"frontend": {}, "demo": {}} # type: Dict[str, Any] bootstrap.async_from_config_dict( - config, hass, config_dir=config_dir, verbose=args.verbose, - skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, - log_file=args.log_file, log_no_color=args.log_no_color) + config, + hass, + config_dir=config_dir, + verbose=args.verbose, + skip_pip=args.skip_pip, + log_rotate_days=args.log_rotate_days, + log_file=args.log_file, + log_no_color=args.log_no_color, + ) else: config_file = await ensure_config_file(hass, config_dir) - print('Config directory:', config_dir) + print("Config directory:", config_dir) await bootstrap.async_from_config_file( - config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip, - log_rotate_days=args.log_rotate_days, log_file=args.log_file, - log_no_color=args.log_no_color) + config_file, + hass, + verbose=args.verbose, + skip_pip=args.skip_pip, + log_rotate_days=args.log_rotate_days, + log_file=args.log_file, + log_no_color=args.log_no_color, + ) if args.open_ui: # Imported here to avoid importing asyncio before monkey patch @@ -297,12 +317,14 @@ async def setup_and_run_hass(config_dir: str, """Open the web interface in a browser.""" if hass.config.api is not None: import webbrowser + webbrowser.open(hass.config.api.base_url) run_callback_threadsafe( hass.loop, hass.bus.async_listen_once, - EVENT_HOMEASSISTANT_START, open_browser + EVENT_HOMEASSISTANT_START, + open_browser, ) return await hass.async_run() @@ -312,17 +334,17 @@ def try_to_restart() -> None: """Attempt to clean up state and start a new Home Assistant instance.""" # Things should be mostly shut down already at this point, now just try # to clean up things that may have been left behind. - sys.stderr.write('Home Assistant attempting to restart.\n') + sys.stderr.write("Home Assistant attempting to restart.\n") # Count remaining threads, ideally there should only be one non-daemonized # thread left (which is us). Nothing we really do with it, but it might be # useful when debugging shutdown/restart issues. try: - nthreads = sum(thread.is_alive() and not thread.daemon - for thread in threading.enumerate()) + nthreads = sum( + thread.is_alive() and not thread.daemon for thread in threading.enumerate() + ) if nthreads > 1: - sys.stderr.write( - "Found {} non-daemonic threads.\n".format(nthreads)) + sys.stderr.write("Found {} non-daemonic threads.\n".format(nthreads)) # Somehow we sometimes seem to trigger an assertion in the python threading # module. It seems we find threads that have no associated OS level thread @@ -336,7 +358,7 @@ def try_to_restart() -> None: except ValueError: max_fd = 256 - if platform.system() == 'Darwin': + if platform.system() == "Darwin": closefds_osx(3, max_fd) else: os.closerange(3, max_fd) @@ -355,16 +377,15 @@ def main() -> int: validate_python() monkey_patch_needed = sys.version_info[:3] < (3, 6, 3) - if monkey_patch_needed and os.environ.get('HASS_NO_MONKEY') != '1': - if sys.version_info[:2] >= (3, 6): - monkey_patch.disable_c_asyncio() + if monkey_patch_needed and os.environ.get("HASS_NO_MONKEY") != "1": + monkey_patch.disable_c_asyncio() monkey_patch.patch_weakref_tasks() set_loop() # Run a simple daemon runner process on Windows to handle restarts - if os.name == 'nt' and '--runner' not in sys.argv: - nt_args = cmdline() + ['--runner'] + if os.name == "nt" and "--runner" not in sys.argv: + nt_args = cmdline() + ["--runner"] while True: try: subprocess.check_call(nt_args) @@ -379,6 +400,7 @@ def main() -> int: if args.script is not None: from homeassistant import scripts + return scripts.run(args.script) config_dir = os.path.join(os.getcwd(), args.config) @@ -393,6 +415,7 @@ def main() -> int: write_pid(args.pid_file) from homeassistant.util.async_ import asyncio_run + exit_code = asyncio_run(setup_and_run_hass(config_dir, args)) if exit_code == RESTART_EXIT_CODE and not args.runner: try_to_restart() diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 9e4b9d09d78..f00687b828c 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -17,8 +17,8 @@ from .const import GROUP_ID_ADMIN from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule from .providers import auth_provider_from_config, AuthProvider, LoginFlow -EVENT_USER_ADDED = 'user_added' -EVENT_USER_REMOVED = 'user_removed' +EVENT_USER_ADDED = "user_added" +EVENT_USER_REMOVED = "user_removed" _LOGGER = logging.getLogger(__name__) _MfaModuleDict = Dict[str, MultiFactorAuthModule] @@ -27,9 +27,10 @@ _ProviderDict = Dict[_ProviderKey, AuthProvider] async def auth_manager_from_config( - hass: HomeAssistant, - provider_configs: List[Dict[str, Any]], - module_configs: List[Dict[str, Any]]) -> 'AuthManager': + hass: HomeAssistant, + provider_configs: List[Dict[str, Any]], + module_configs: List[Dict[str, Any]], +) -> "AuthManager": """Initialize an auth manager from config. CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or @@ -38,8 +39,11 @@ async def auth_manager_from_config( store = auth_store.AuthStore(hass) if provider_configs: providers = await asyncio.gather( - *[auth_provider_from_config(hass, store, config) - for config in provider_configs]) + *( + auth_provider_from_config(hass, store, config) + for config in provider_configs + ) + ) else: providers = () # So returned auth providers are in same order as config @@ -50,8 +54,8 @@ async def auth_manager_from_config( if module_configs: modules = await asyncio.gather( - *[auth_mfa_module_from_config(hass, config) - for config in module_configs]) + *(auth_mfa_module_from_config(hass, config) for config in module_configs) + ) else: modules = () # So returned auth modules are in same order as config @@ -66,17 +70,21 @@ async def auth_manager_from_config( class AuthManager: """Manage the authentication for Home Assistant.""" - def __init__(self, hass: HomeAssistant, store: auth_store.AuthStore, - providers: _ProviderDict, mfa_modules: _MfaModuleDict) \ - -> None: + def __init__( + self, + hass: HomeAssistant, + store: auth_store.AuthStore, + providers: _ProviderDict, + mfa_modules: _MfaModuleDict, + ) -> None: """Initialize the auth manager.""" self.hass = hass self._store = store self._providers = providers self._mfa_modules = mfa_modules self.login_flow = data_entry_flow.FlowManager( - hass, self._async_create_login_flow, - self._async_finish_login_flow) + hass, self._async_create_login_flow, self._async_finish_login_flow + ) @property def support_legacy(self) -> bool: @@ -86,7 +94,7 @@ class AuthManager: Should be removed when we removed legacy_api_password auth providers. """ for provider_type, _ in self._providers: - if provider_type == 'legacy_api_password': + if provider_type == "legacy_api_password": return True return False @@ -100,20 +108,21 @@ class AuthManager: """Return a list of available auth modules.""" return list(self._mfa_modules.values()) - def get_auth_provider(self, provider_type: str, provider_id: str) \ - -> Optional[AuthProvider]: + def get_auth_provider( + self, provider_type: str, provider_id: str + ) -> Optional[AuthProvider]: """Return an auth provider, None if not found.""" return self._providers.get((provider_type, provider_id)) - def get_auth_providers(self, provider_type: str) \ - -> List[AuthProvider]: + def get_auth_providers(self, provider_type: str) -> List[AuthProvider]: """Return a List of auth provider of one type, Empty if not found.""" - return [provider - for (p_type, _), provider in self._providers.items() - if p_type == provider_type] + return [ + provider + for (p_type, _), provider in self._providers.items() + if p_type == provider_type + ] - def get_auth_mfa_module(self, module_id: str) \ - -> Optional[MultiFactorAuthModule]: + def get_auth_mfa_module(self, module_id: str) -> Optional[MultiFactorAuthModule]: """Return a multi-factor auth module, None if not found.""" return self._mfa_modules.get(module_id) @@ -135,7 +144,8 @@ class AuthManager: return await self._store.async_get_group(group_id) async def async_get_user_by_credentials( - self, credentials: models.Credentials) -> Optional[models.User]: + self, credentials: models.Credentials + ) -> Optional[models.User]: """Get a user by credential, return None if not found.""" for user in await self.async_get_users(): for creds in user.credentials: @@ -145,57 +155,50 @@ class AuthManager: return None async def async_create_system_user( - self, name: str, - group_ids: Optional[List[str]] = None) -> models.User: + self, name: str, group_ids: Optional[List[str]] = None + ) -> models.User: """Create a system user.""" user = await self._store.async_create_user( - name=name, - system_generated=True, - is_active=True, - group_ids=group_ids or [], + name=name, system_generated=True, is_active=True, group_ids=group_ids or [] ) - self.hass.bus.async_fire(EVENT_USER_ADDED, { - 'user_id': user.id - }) + self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id}) return user async def async_create_user(self, name: str) -> models.User: """Create a user.""" kwargs = { - 'name': name, - 'is_active': True, - 'group_ids': [GROUP_ID_ADMIN] + "name": name, + "is_active": True, + "group_ids": [GROUP_ID_ADMIN], } # type: Dict[str, Any] if await self._user_should_be_owner(): - kwargs['is_owner'] = True + kwargs["is_owner"] = True user = await self._store.async_create_user(**kwargs) - self.hass.bus.async_fire(EVENT_USER_ADDED, { - 'user_id': user.id - }) + self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id}) return user - async def async_get_or_create_user(self, credentials: models.Credentials) \ - -> models.User: + async def async_get_or_create_user( + self, credentials: models.Credentials + ) -> models.User: """Get or create a user.""" if not credentials.is_new: user = await self.async_get_user_by_credentials(credentials) if user is None: - raise ValueError('Unable to find the user.') + raise ValueError("Unable to find the user.") return user auth_provider = self._async_get_auth_provider(credentials) if auth_provider is None: - raise RuntimeError('Credential with unknown provider encountered') + raise RuntimeError("Credential with unknown provider encountered") - info = await auth_provider.async_user_meta_for_credentials( - credentials) + info = await auth_provider.async_user_meta_for_credentials(credentials) user = await self._store.async_create_user( credentials=credentials, @@ -204,14 +207,13 @@ class AuthManager: group_ids=[GROUP_ID_ADMIN], ) - self.hass.bus.async_fire(EVENT_USER_ADDED, { - 'user_id': user.id - }) + self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id}) return user - async def async_link_user(self, user: models.User, - credentials: models.Credentials) -> None: + async def async_link_user( + self, user: models.User, credentials: models.Credentials + ) -> None: """Link credentials to an existing user.""" await self._store.async_link_user(user, credentials) @@ -227,19 +229,20 @@ class AuthManager: await self._store.async_remove_user(user) - self.hass.bus.async_fire(EVENT_USER_REMOVED, { - 'user_id': user.id - }) + self.hass.bus.async_fire(EVENT_USER_REMOVED, {"user_id": user.id}) - async def async_update_user(self, user: models.User, - name: Optional[str] = None, - group_ids: Optional[List[str]] = None) -> None: + async def async_update_user( + self, + user: models.User, + name: Optional[str] = None, + group_ids: Optional[List[str]] = None, + ) -> None: """Update a user.""" kwargs = {} # type: Dict[str,Any] if name is not None: - kwargs['name'] = name + kwargs["name"] = name if group_ids is not None: - kwargs['group_ids'] = group_ids + kwargs["group_ids"] = group_ids await self._store.async_update_user(user, **kwargs) async def async_activate_user(self, user: models.User) -> None: @@ -249,47 +252,52 @@ class AuthManager: async def async_deactivate_user(self, user: models.User) -> None: """Deactivate a user.""" if user.is_owner: - raise ValueError('Unable to deactive the owner') + raise ValueError("Unable to deactive the owner") await self._store.async_deactivate_user(user) - async def async_remove_credentials( - self, credentials: models.Credentials) -> None: + async def async_remove_credentials(self, credentials: models.Credentials) -> None: """Remove credentials.""" provider = self._async_get_auth_provider(credentials) - if (provider is not None and - hasattr(provider, 'async_will_remove_credentials')): + if provider is not None and hasattr(provider, "async_will_remove_credentials"): # https://github.com/python/mypy/issues/1424 await provider.async_will_remove_credentials( # type: ignore - credentials) + credentials + ) await self._store.async_remove_credentials(credentials) - async def async_enable_user_mfa(self, user: models.User, - mfa_module_id: str, data: Any) -> None: + async def async_enable_user_mfa( + self, user: models.User, mfa_module_id: str, data: Any + ) -> None: """Enable a multi-factor auth module for user.""" if user.system_generated: - raise ValueError('System generated users cannot enable ' - 'multi-factor auth module.') + raise ValueError( + "System generated users cannot enable " "multi-factor auth module." + ) module = self.get_auth_mfa_module(mfa_module_id) if module is None: - raise ValueError('Unable find multi-factor auth module: {}' - .format(mfa_module_id)) + raise ValueError( + "Unable find multi-factor auth module: {}".format(mfa_module_id) + ) await module.async_setup_user(user.id, data) - async def async_disable_user_mfa(self, user: models.User, - mfa_module_id: str) -> None: + async def async_disable_user_mfa( + self, user: models.User, mfa_module_id: str + ) -> None: """Disable a multi-factor auth module for user.""" if user.system_generated: - raise ValueError('System generated users cannot disable ' - 'multi-factor auth module.') + raise ValueError( + "System generated users cannot disable " "multi-factor auth module." + ) module = self.get_auth_mfa_module(mfa_module_id) if module is None: - raise ValueError('Unable find multi-factor auth module: {}' - .format(mfa_module_id)) + raise ValueError( + "Unable find multi-factor auth module: {}".format(mfa_module_id) + ) await module.async_depose_user(user.id) @@ -302,20 +310,23 @@ class AuthManager: return modules async def async_create_refresh_token( - self, user: models.User, client_id: Optional[str] = None, - client_name: Optional[str] = None, - client_icon: Optional[str] = None, - token_type: Optional[str] = None, - access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \ - -> models.RefreshToken: + self, + user: models.User, + client_id: Optional[str] = None, + client_name: Optional[str] = None, + client_icon: Optional[str] = None, + token_type: Optional[str] = None, + access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, + ) -> models.RefreshToken: """Create a new refresh token for a user.""" if not user.is_active: - raise ValueError('User is not active') + raise ValueError("User is not active") if user.system_generated and client_id is not None: raise ValueError( - 'System generated users cannot have refresh tokens connected ' - 'to a client.') + "System generated users cannot have refresh tokens connected " + "to a client." + ) if token_type is None: if user.system_generated: @@ -325,61 +336,76 @@ class AuthManager: if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM): raise ValueError( - 'System generated users can only have system type ' - 'refresh tokens') + "System generated users can only have system type " "refresh tokens" + ) if token_type == models.TOKEN_TYPE_NORMAL and client_id is None: - raise ValueError('Client is required to generate a refresh token.') + raise ValueError("Client is required to generate a refresh token.") - if (token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and - client_name is None): - raise ValueError('Client_name is required for long-lived access ' - 'token') + if ( + token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN + and client_name is None + ): + raise ValueError("Client_name is required for long-lived access " "token") if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN: for token in user.refresh_tokens.values(): - if (token.client_name == client_name and token.token_type == - models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN): + if ( + token.client_name == client_name + and token.token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN + ): # Each client_name can only have one # long_lived_access_token type of refresh token - raise ValueError('{} already exists'.format(client_name)) + raise ValueError("{} already exists".format(client_name)) return await self._store.async_create_refresh_token( - user, client_id, client_name, client_icon, - token_type, access_token_expiration) + user, + client_id, + client_name, + client_icon, + token_type, + access_token_expiration, + ) async def async_get_refresh_token( - self, token_id: str) -> Optional[models.RefreshToken]: + self, token_id: str + ) -> Optional[models.RefreshToken]: """Get refresh token by id.""" return await self._store.async_get_refresh_token(token_id) async def async_get_refresh_token_by_token( - self, token: str) -> Optional[models.RefreshToken]: + self, token: str + ) -> Optional[models.RefreshToken]: """Get refresh token by token.""" return await self._store.async_get_refresh_token_by_token(token) - async def async_remove_refresh_token(self, - refresh_token: models.RefreshToken) \ - -> None: + async def async_remove_refresh_token( + self, refresh_token: models.RefreshToken + ) -> None: """Delete a refresh token.""" await self._store.async_remove_refresh_token(refresh_token) @callback - def async_create_access_token(self, - refresh_token: models.RefreshToken, - remote_ip: Optional[str] = None) -> str: + def async_create_access_token( + self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None + ) -> str: """Create a new access token.""" self._store.async_log_refresh_token_usage(refresh_token, remote_ip) now = dt_util.utcnow() - return jwt.encode({ - 'iss': refresh_token.id, - 'iat': now, - 'exp': now + refresh_token.access_token_expiration, - }, refresh_token.jwt_key, algorithm='HS256').decode() + return jwt.encode( + { + "iss": refresh_token.id, + "iat": now, + "exp": now + refresh_token.access_token_expiration, + }, + refresh_token.jwt_key, + algorithm="HS256", + ).decode() async def async_validate_access_token( - self, token: str) -> Optional[models.RefreshToken]: + self, token: str + ) -> Optional[models.RefreshToken]: """Return refresh token if an access token is valid.""" try: unverif_claims = jwt.decode(token, verify=False) @@ -387,23 +413,18 @@ class AuthManager: return None refresh_token = await self.async_get_refresh_token( - cast(str, unverif_claims.get('iss'))) + cast(str, unverif_claims.get("iss")) + ) if refresh_token is None: - jwt_key = '' - issuer = '' + jwt_key = "" + issuer = "" else: jwt_key = refresh_token.jwt_key issuer = refresh_token.id try: - jwt.decode( - token, - jwt_key, - leeway=10, - issuer=issuer, - algorithms=['HS256'] - ) + jwt.decode(token, jwt_key, leeway=10, issuer=issuer, algorithms=["HS256"]) except jwt.InvalidTokenError: return None @@ -413,31 +434,32 @@ class AuthManager: return refresh_token async def _async_create_login_flow( - self, handler: _ProviderKey, *, context: Optional[Dict], - data: Optional[Any]) -> data_entry_flow.FlowHandler: + self, handler: _ProviderKey, *, context: Optional[Dict], data: Optional[Any] + ) -> data_entry_flow.FlowHandler: """Create a login flow.""" auth_provider = self._providers[handler] return await auth_provider.async_login_flow(context) async def _async_finish_login_flow( - self, flow: LoginFlow, result: Dict[str, Any]) \ - -> Dict[str, Any]: + self, flow: LoginFlow, result: Dict[str, Any] + ) -> Dict[str, Any]: """Return a user as result of login flow.""" - if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: return result # we got final result - if isinstance(result['data'], models.User): - result['result'] = result['data'] + if isinstance(result["data"], models.User): + result["result"] = result["data"] return result - auth_provider = self._providers[result['handler']] + auth_provider = self._providers[result["handler"]] credentials = await auth_provider.async_get_or_create_credentials( - result['data']) + result["data"] + ) - if flow.context is not None and flow.context.get('credential_only'): - result['result'] = credentials + if flow.context is not None and flow.context.get("credential_only"): + result["result"] = credentials return result # multi-factor module cannot enabled for new credential @@ -452,15 +474,18 @@ class AuthManager: flow.available_mfa_modules = modules return await flow.async_step_select_mfa_module() - result['result'] = await self.async_get_or_create_user(credentials) + result["result"] = await self.async_get_or_create_user(credentials) return result @callback def _async_get_auth_provider( - self, credentials: models.Credentials) -> Optional[AuthProvider]: + self, credentials: models.Credentials + ) -> Optional[AuthProvider]: """Get auth provider from a set of credentials.""" - auth_provider_key = (credentials.auth_provider_type, - credentials.auth_provider_id) + auth_provider_key = ( + credentials.auth_provider_type, + credentials.auth_provider_id, + ) return self._providers.get(auth_provider_key) async def _user_should_be_owner(self) -> bool: diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index b9acc90d5c2..82db0bcf7a9 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -16,10 +16,10 @@ from .permissions import PermissionLookup, system_policies from .permissions.types import PolicyType # noqa: F401 STORAGE_VERSION = 1 -STORAGE_KEY = 'auth' -GROUP_NAME_ADMIN = 'Administrators' +STORAGE_KEY = "auth" +GROUP_NAME_ADMIN = "Administrators" GROUP_NAME_USER = "Users" -GROUP_NAME_READ_ONLY = 'Read Only' +GROUP_NAME_READ_ONLY = "Read Only" class AuthStore: @@ -37,8 +37,9 @@ class AuthStore: self._users = None # type: Optional[Dict[str, models.User]] self._groups = None # type: Optional[Dict[str, models.Group]] self._perm_lookup = None # type: Optional[PermissionLookup] - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, - private=True) + self._store = hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY, private=True + ) self._lock = asyncio.Lock() async def async_get_groups(self) -> List[models.Group]: @@ -74,11 +75,14 @@ class AuthStore: return self._users.get(user_id) async def async_create_user( - self, name: Optional[str], is_owner: Optional[bool] = None, - is_active: Optional[bool] = None, - system_generated: Optional[bool] = None, - credentials: Optional[models.Credentials] = None, - group_ids: Optional[List[str]] = None) -> models.User: + self, + name: Optional[str], + is_owner: Optional[bool] = None, + is_active: Optional[bool] = None, + system_generated: Optional[bool] = None, + credentials: Optional[models.Credentials] = None, + group_ids: Optional[List[str]] = None, + ) -> models.User: """Create a new user.""" if self._users is None: await self._async_load() @@ -87,28 +91,28 @@ class AuthStore: assert self._groups is not None groups = [] - for group_id in (group_ids or []): + for group_id in group_ids or []: group = self._groups.get(group_id) if group is None: - raise ValueError('Invalid group specified {}'.format(group_id)) + raise ValueError("Invalid group specified {}".format(group_id)) groups.append(group) kwargs = { - 'name': name, + "name": name, # Until we get group management, we just put everyone in the # same group. - 'groups': groups, - 'perm_lookup': self._perm_lookup, + "groups": groups, + "perm_lookup": self._perm_lookup, } # type: Dict[str, Any] if is_owner is not None: - kwargs['is_owner'] = is_owner + kwargs["is_owner"] = is_owner if is_active is not None: - kwargs['is_active'] = is_active + kwargs["is_active"] = is_active if system_generated is not None: - kwargs['system_generated'] = system_generated + kwargs["system_generated"] = system_generated new_user = models.User(**kwargs) @@ -122,8 +126,9 @@ class AuthStore: await self.async_link_user(new_user, credentials) return new_user - async def async_link_user(self, user: models.User, - credentials: models.Credentials) -> None: + async def async_link_user( + self, user: models.User, credentials: models.Credentials + ) -> None: """Add credentials to an existing user.""" user.credentials.append(credentials) self._async_schedule_save() @@ -139,9 +144,12 @@ class AuthStore: self._async_schedule_save() async def async_update_user( - self, user: models.User, name: Optional[str] = None, - is_active: Optional[bool] = None, - group_ids: Optional[List[str]] = None) -> None: + self, + user: models.User, + name: Optional[str] = None, + is_active: Optional[bool] = None, + group_ids: Optional[List[str]] = None, + ) -> None: """Update a user.""" assert self._groups is not None @@ -156,10 +164,7 @@ class AuthStore: user.groups = groups user.invalidate_permission_cache() - for attr_name, value in ( - ('name', name), - ('is_active', is_active), - ): + for attr_name, value in (("name", name), ("is_active", is_active)): if value is not None: setattr(user, attr_name, value) @@ -175,8 +180,7 @@ class AuthStore: user.is_active = False self._async_schedule_save() - async def async_remove_credentials( - self, credentials: models.Credentials) -> None: + async def async_remove_credentials(self, credentials: models.Credentials) -> None: """Remove credentials.""" if self._users is None: await self._async_load() @@ -197,23 +201,25 @@ class AuthStore: self._async_schedule_save() async def async_create_refresh_token( - self, user: models.User, client_id: Optional[str] = None, - client_name: Optional[str] = None, - client_icon: Optional[str] = None, - token_type: str = models.TOKEN_TYPE_NORMAL, - access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \ - -> models.RefreshToken: + self, + user: models.User, + client_id: Optional[str] = None, + client_name: Optional[str] = None, + client_icon: Optional[str] = None, + token_type: str = models.TOKEN_TYPE_NORMAL, + access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, + ) -> models.RefreshToken: """Create a new token for a user.""" kwargs = { - 'user': user, - 'client_id': client_id, - 'token_type': token_type, - 'access_token_expiration': access_token_expiration + "user": user, + "client_id": client_id, + "token_type": token_type, + "access_token_expiration": access_token_expiration, } # type: Dict[str, Any] if client_name: - kwargs['client_name'] = client_name + kwargs["client_name"] = client_name if client_icon: - kwargs['client_icon'] = client_icon + kwargs["client_icon"] = client_icon refresh_token = models.RefreshToken(**kwargs) user.refresh_tokens[refresh_token.id] = refresh_token @@ -222,7 +228,8 @@ class AuthStore: return refresh_token async def async_remove_refresh_token( - self, refresh_token: models.RefreshToken) -> None: + self, refresh_token: models.RefreshToken + ) -> None: """Remove a refresh token.""" if self._users is None: await self._async_load() @@ -234,7 +241,8 @@ class AuthStore: break async def async_get_refresh_token( - self, token_id: str) -> Optional[models.RefreshToken]: + self, token_id: str + ) -> Optional[models.RefreshToken]: """Get refresh token by id.""" if self._users is None: await self._async_load() @@ -248,7 +256,8 @@ class AuthStore: return None async def async_get_refresh_token_by_token( - self, token: str) -> Optional[models.RefreshToken]: + self, token: str + ) -> Optional[models.RefreshToken]: """Get refresh token by token.""" if self._users is None: await self._async_load() @@ -265,8 +274,8 @@ class AuthStore: @callback def async_log_refresh_token_usage( - self, refresh_token: models.RefreshToken, - remote_ip: Optional[str] = None) -> None: + self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None + ) -> None: """Update refresh token last used information.""" refresh_token.last_used_at = dt_util.utcnow() refresh_token.last_used_ip = remote_ip @@ -292,9 +301,7 @@ class AuthStore: if self._users is not None: return - self._perm_lookup = perm_lookup = PermissionLookup( - ent_reg, dev_reg - ) + self._perm_lookup = perm_lookup = PermissionLookup(ent_reg, dev_reg) if data is None: self._set_defaults() @@ -317,24 +324,24 @@ class AuthStore: # prevents crashing if user rolls back HA version after a new property # was added. - for group_dict in data.get('groups', []): + for group_dict in data.get("groups", []): policy = None # type: Optional[PolicyType] - if group_dict['id'] == GROUP_ID_ADMIN: + if group_dict["id"] == GROUP_ID_ADMIN: has_admin_group = True name = GROUP_NAME_ADMIN policy = system_policies.ADMIN_POLICY system_generated = True - elif group_dict['id'] == GROUP_ID_USER: + elif group_dict["id"] == GROUP_ID_USER: has_user_group = True name = GROUP_NAME_USER policy = system_policies.USER_POLICY system_generated = True - elif group_dict['id'] == GROUP_ID_READ_ONLY: + elif group_dict["id"] == GROUP_ID_READ_ONLY: has_read_only_group = True name = GROUP_NAME_READ_ONLY @@ -342,18 +349,18 @@ class AuthStore: system_generated = True else: - name = group_dict['name'] - policy = group_dict.get('policy') + name = group_dict["name"] + policy = group_dict.get("policy") system_generated = False # We don't want groups without a policy that are not system groups # This is part of migrating from state 1 if policy is None: - group_without_policy = group_dict['id'] + group_without_policy = group_dict["id"] continue - groups[group_dict['id']] = models.Group( - id=group_dict['id'], + groups[group_dict["id"]] = models.Group( + id=group_dict["id"], name=name, policy=policy, system_generated=system_generated, @@ -361,8 +368,7 @@ class AuthStore: # If there are no groups, add all existing users to the admin group. # This is part of migrating from state 2 - migrate_users_to_admin_group = (not groups and - group_without_policy is None) + migrate_users_to_admin_group = not groups and group_without_policy is None # If we find a no_policy_group, we need to migrate all users to the # admin group. We only do this if there are no other groups, as is @@ -385,82 +391,86 @@ class AuthStore: user_group = _system_user_group() groups[user_group.id] = user_group - for user_dict in data['users']: + for user_dict in data["users"]: # Collect the users group. user_groups = [] - for group_id in user_dict.get('group_ids', []): + for group_id in user_dict.get("group_ids", []): # This is part of migrating from state 1 if group_id == group_without_policy: group_id = GROUP_ID_ADMIN user_groups.append(groups[group_id]) # This is part of migrating from state 2 - if (not user_dict['system_generated'] and - migrate_users_to_admin_group): + if not user_dict["system_generated"] and migrate_users_to_admin_group: user_groups.append(groups[GROUP_ID_ADMIN]) - users[user_dict['id']] = models.User( - name=user_dict['name'], + users[user_dict["id"]] = models.User( + name=user_dict["name"], groups=user_groups, - id=user_dict['id'], - is_owner=user_dict['is_owner'], - is_active=user_dict['is_active'], - system_generated=user_dict['system_generated'], + id=user_dict["id"], + is_owner=user_dict["is_owner"], + is_active=user_dict["is_active"], + system_generated=user_dict["system_generated"], perm_lookup=perm_lookup, ) - for cred_dict in data['credentials']: - users[cred_dict['user_id']].credentials.append(models.Credentials( - id=cred_dict['id'], - is_new=False, - auth_provider_type=cred_dict['auth_provider_type'], - auth_provider_id=cred_dict['auth_provider_id'], - data=cred_dict['data'], - )) + for cred_dict in data["credentials"]: + users[cred_dict["user_id"]].credentials.append( + models.Credentials( + id=cred_dict["id"], + is_new=False, + auth_provider_type=cred_dict["auth_provider_type"], + auth_provider_id=cred_dict["auth_provider_id"], + data=cred_dict["data"], + ) + ) - for rt_dict in data['refresh_tokens']: + for rt_dict in data["refresh_tokens"]: # Filter out the old keys that don't have jwt_key (pre-0.76) - if 'jwt_key' not in rt_dict: + if "jwt_key" not in rt_dict: continue - created_at = dt_util.parse_datetime(rt_dict['created_at']) + created_at = dt_util.parse_datetime(rt_dict["created_at"]) if created_at is None: getLogger(__name__).error( - 'Ignoring refresh token %(id)s with invalid created_at ' - '%(created_at)s for user_id %(user_id)s', rt_dict) + "Ignoring refresh token %(id)s with invalid created_at " + "%(created_at)s for user_id %(user_id)s", + rt_dict, + ) continue - token_type = rt_dict.get('token_type') + token_type = rt_dict.get("token_type") if token_type is None: - if rt_dict['client_id'] is None: + if rt_dict["client_id"] is None: token_type = models.TOKEN_TYPE_SYSTEM else: token_type = models.TOKEN_TYPE_NORMAL # old refresh_token don't have last_used_at (pre-0.78) - last_used_at_str = rt_dict.get('last_used_at') + last_used_at_str = rt_dict.get("last_used_at") if last_used_at_str: last_used_at = dt_util.parse_datetime(last_used_at_str) else: last_used_at = None token = models.RefreshToken( - id=rt_dict['id'], - user=users[rt_dict['user_id']], - client_id=rt_dict['client_id'], + id=rt_dict["id"], + user=users[rt_dict["user_id"]], + client_id=rt_dict["client_id"], # use dict.get to keep backward compatibility - client_name=rt_dict.get('client_name'), - client_icon=rt_dict.get('client_icon'), + client_name=rt_dict.get("client_name"), + client_icon=rt_dict.get("client_icon"), token_type=token_type, created_at=created_at, access_token_expiration=timedelta( - seconds=rt_dict['access_token_expiration']), - token=rt_dict['token'], - jwt_key=rt_dict['jwt_key'], + seconds=rt_dict["access_token_expiration"] + ), + token=rt_dict["token"], + jwt_key=rt_dict["jwt_key"], last_used_at=last_used_at, - last_used_ip=rt_dict.get('last_used_ip'), + last_used_ip=rt_dict.get("last_used_ip"), ) - users[rt_dict['user_id']].refresh_tokens[token.id] = token + users[rt_dict["user_id"]].refresh_tokens[token.id] = token self._groups = groups self._users = users @@ -481,12 +491,12 @@ class AuthStore: users = [ { - 'id': user.id, - 'group_ids': [group.id for group in user.groups], - 'is_owner': user.is_owner, - 'is_active': user.is_active, - 'name': user.name, - 'system_generated': user.system_generated, + "id": user.id, + "group_ids": [group.id for group in user.groups], + "is_owner": user.is_owner, + "is_active": user.is_active, + "name": user.name, + "system_generated": user.system_generated, } for user in self._users.values() ] @@ -494,23 +504,23 @@ class AuthStore: groups = [] for group in self._groups.values(): g_dict = { - 'id': group.id, + "id": group.id, # Name not read for sys groups. Kept here for backwards compat - 'name': group.name + "name": group.name, } # type: Dict[str, Any] if not group.system_generated: - g_dict['policy'] = group.policy + g_dict["policy"] = group.policy groups.append(g_dict) credentials = [ { - 'id': credential.id, - 'user_id': user.id, - 'auth_provider_type': credential.auth_provider_type, - 'auth_provider_id': credential.auth_provider_id, - 'data': credential.data, + "id": credential.id, + "user_id": user.id, + "auth_provider_type": credential.auth_provider_type, + "auth_provider_id": credential.auth_provider_id, + "data": credential.data, } for user in self._users.values() for credential in user.credentials @@ -518,31 +528,30 @@ class AuthStore: refresh_tokens = [ { - 'id': refresh_token.id, - 'user_id': user.id, - 'client_id': refresh_token.client_id, - 'client_name': refresh_token.client_name, - 'client_icon': refresh_token.client_icon, - 'token_type': refresh_token.token_type, - 'created_at': refresh_token.created_at.isoformat(), - 'access_token_expiration': - refresh_token.access_token_expiration.total_seconds(), - 'token': refresh_token.token, - 'jwt_key': refresh_token.jwt_key, - 'last_used_at': - refresh_token.last_used_at.isoformat() - if refresh_token.last_used_at else None, - 'last_used_ip': refresh_token.last_used_ip, + "id": refresh_token.id, + "user_id": user.id, + "client_id": refresh_token.client_id, + "client_name": refresh_token.client_name, + "client_icon": refresh_token.client_icon, + "token_type": refresh_token.token_type, + "created_at": refresh_token.created_at.isoformat(), + "access_token_expiration": refresh_token.access_token_expiration.total_seconds(), + "token": refresh_token.token, + "jwt_key": refresh_token.jwt_key, + "last_used_at": refresh_token.last_used_at.isoformat() + if refresh_token.last_used_at + else None, + "last_used_ip": refresh_token.last_used_ip, } for user in self._users.values() for refresh_token in user.refresh_tokens.values() ] return { - 'users': users, - 'groups': groups, - 'credentials': credentials, - 'refresh_tokens': refresh_tokens, + "users": users, + "groups": groups, + "credentials": credentials, + "refresh_tokens": refresh_tokens, } def _set_defaults(self) -> None: diff --git a/homeassistant/auth/const.py b/homeassistant/auth/const.py index ef2d54ccbab..5e17e752bdd 100644 --- a/homeassistant/auth/const.py +++ b/homeassistant/auth/const.py @@ -4,6 +4,6 @@ from datetime import timedelta ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) MFA_SESSION_EXPIRATION = timedelta(minutes=5) -GROUP_ID_ADMIN = 'system-admin' -GROUP_ID_USER = 'system-users' -GROUP_ID_READ_ONLY = 'system-read-only' +GROUP_ID_ADMIN = "system-admin" +GROUP_ID_USER = "system-users" +GROUP_ID_READ_ONLY = "system-read-only" diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 3313063679d..fa9b1f50224 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -15,14 +15,17 @@ from homeassistant.util.decorator import Registry MULTI_FACTOR_AUTH_MODULES = Registry() -MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({ - vol.Required(CONF_TYPE): str, - vol.Optional(CONF_NAME): str, - # Specify ID if you have two mfa auth module for same type. - vol.Optional(CONF_ID): str, -}, extra=vol.ALLOW_EXTRA) +MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema( + { + vol.Required(CONF_TYPE): str, + vol.Optional(CONF_NAME): str, + # Specify ID if you have two mfa auth module for same type. + vol.Optional(CONF_ID): str, + }, + extra=vol.ALLOW_EXTRA, +) -DATA_REQS = 'mfa_auth_module_reqs_processed' +DATA_REQS = "mfa_auth_module_reqs_processed" _LOGGER = logging.getLogger(__name__) @@ -30,7 +33,7 @@ _LOGGER = logging.getLogger(__name__) class MultiFactorAuthModule: """Multi-factor Auth Module of validation function.""" - DEFAULT_TITLE = 'Unnamed auth module' + DEFAULT_TITLE = "Unnamed auth module" MAX_RETRY_TIME = 3 def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: @@ -63,7 +66,7 @@ class MultiFactorAuthModule: """Return a voluptuous schema to define mfa auth module's input.""" raise NotImplementedError - async def async_setup_flow(self, user_id: str) -> 'SetupFlow': + async def async_setup_flow(self, user_id: str) -> "SetupFlow": """Return a data entry flow handler for setup module. Mfa module should extend SetupFlow @@ -82,8 +85,7 @@ class MultiFactorAuthModule: """Return whether user is setup.""" raise NotImplementedError - async def async_validate( - self, user_id: str, user_input: Dict[str, Any]) -> bool: + async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool: """Return True if validation passed.""" raise NotImplementedError @@ -91,17 +93,17 @@ class MultiFactorAuthModule: class SetupFlow(data_entry_flow.FlowHandler): """Handler for the setup flow.""" - def __init__(self, auth_module: MultiFactorAuthModule, - setup_schema: vol.Schema, - user_id: str) -> None: + def __init__( + self, auth_module: MultiFactorAuthModule, setup_schema: vol.Schema, user_id: str + ) -> None: """Initialize the setup flow.""" self._auth_module = auth_module self._setup_schema = setup_schema self._user_id = user_id async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None) \ - -> Dict[str, Any]: + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. @@ -110,23 +112,19 @@ class SetupFlow(data_entry_flow.FlowHandler): errors = {} # type: Dict[str, str] if user_input: - result = await self._auth_module.async_setup_user( - self._user_id, user_input) + result = await self._auth_module.async_setup_user(self._user_id, user_input) return self.async_create_entry( - title=self._auth_module.name, - data={'result': result} + title=self._auth_module.name, data={"result": result} ) return self.async_show_form( - step_id='init', - data_schema=self._setup_schema, - errors=errors + step_id="init", data_schema=self._setup_schema, errors=errors ) async def auth_mfa_module_from_config( - hass: HomeAssistant, config: Dict[str, Any]) \ - -> MultiFactorAuthModule: + hass: HomeAssistant, config: Dict[str, Any] +) -> MultiFactorAuthModule: """Initialize an auth module from a config.""" module_name = config[CONF_TYPE] module = await _load_mfa_module(hass, module_name) @@ -134,26 +132,29 @@ async def auth_mfa_module_from_config( try: config = module.CONFIG_SCHEMA(config) # type: ignore except vol.Invalid as err: - _LOGGER.error('Invalid configuration for multi-factor module %s: %s', - module_name, humanize_error(config, err)) + _LOGGER.error( + "Invalid configuration for multi-factor module %s: %s", + module_name, + humanize_error(config, err), + ) raise return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore -async def _load_mfa_module(hass: HomeAssistant, module_name: str) \ - -> types.ModuleType: +async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.ModuleType: """Load an mfa auth module.""" - module_path = 'homeassistant.auth.mfa_modules.{}'.format(module_name) + module_path = "homeassistant.auth.mfa_modules.{}".format(module_name) try: module = importlib.import_module(module_path) except ImportError as err: - _LOGGER.error('Unable to load mfa module %s: %s', module_name, err) - raise HomeAssistantError('Unable to load mfa module {}: {}'.format( - module_name, err)) + _LOGGER.error("Unable to load mfa module %s: %s", module_name, err) + raise HomeAssistantError( + "Unable to load mfa module {}: {}".format(module_name, err) + ) - if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'): + if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"): return module processed = hass.data.get(DATA_REQS) @@ -164,12 +165,13 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \ # https://github.com/python/mypy/issues/1424 req_success = await requirements.async_process_requirements( - hass, module_path, module.REQUIREMENTS) # type: ignore + hass, module_path, module.REQUIREMENTS # type: ignore + ) if not req_success: raise HomeAssistantError( - 'Unable to process requirements of mfa module {}'.format( - module_name)) + "Unable to process requirements of mfa module {}".format(module_name) + ) processed.add(module_name) return module diff --git a/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant/auth/mfa_modules/insecure_example.py index 9804cbcf635..a3f0d58c6b3 100644 --- a/homeassistant/auth/mfa_modules/insecure_example.py +++ b/homeassistant/auth/mfa_modules/insecure_example.py @@ -6,39 +6,45 @@ import voluptuous as vol from homeassistant.core import HomeAssistant -from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \ - MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow +from . import ( + MultiFactorAuthModule, + MULTI_FACTOR_AUTH_MODULES, + MULTI_FACTOR_AUTH_MODULE_SCHEMA, + SetupFlow, +) -CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({ - vol.Required('data'): [vol.Schema({ - vol.Required('user_id'): str, - vol.Required('pin'): str, - })] -}, extra=vol.PREVENT_EXTRA) +CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend( + { + vol.Required("data"): [ + vol.Schema({vol.Required("user_id"): str, vol.Required("pin"): str}) + ] + }, + extra=vol.PREVENT_EXTRA, +) _LOGGER = logging.getLogger(__name__) -@MULTI_FACTOR_AUTH_MODULES.register('insecure_example') +@MULTI_FACTOR_AUTH_MODULES.register("insecure_example") class InsecureExampleModule(MultiFactorAuthModule): """Example auth module validate pin.""" - DEFAULT_TITLE = 'Insecure Personal Identify Number' + DEFAULT_TITLE = "Insecure Personal Identify Number" def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: """Initialize the user data store.""" super().__init__(hass, config) - self._data = config['data'] + self._data = config["data"] @property def input_schema(self) -> vol.Schema: """Validate login flow input data.""" - return vol.Schema({'pin': str}) + return vol.Schema({"pin": str}) @property def setup_schema(self) -> vol.Schema: """Validate async_setup_user input data.""" - return vol.Schema({'pin': str}) + return vol.Schema({"pin": str}) async def async_setup_flow(self, user_id: str) -> SetupFlow: """Return a data entry flow handler for setup module. @@ -50,21 +56,21 @@ class InsecureExampleModule(MultiFactorAuthModule): async def async_setup_user(self, user_id: str, setup_data: Any) -> Any: """Set up user to use mfa module.""" # data shall has been validate in caller - pin = setup_data['pin'] + pin = setup_data["pin"] for data in self._data: - if data['user_id'] == user_id: + if data["user_id"] == user_id: # already setup, override - data['pin'] = pin + data["pin"] = pin return - self._data.append({'user_id': user_id, 'pin': pin}) + self._data.append({"user_id": user_id, "pin": pin}) async def async_depose_user(self, user_id: str) -> None: """Remove user from mfa module.""" found = None for data in self._data: - if data['user_id'] == user_id: + if data["user_id"] == user_id: found = data break if found: @@ -73,17 +79,16 @@ class InsecureExampleModule(MultiFactorAuthModule): async def async_is_user_setup(self, user_id: str) -> bool: """Return whether user is setup.""" for data in self._data: - if data['user_id'] == user_id: + if data["user_id"] == user_id: return True return False - async def async_validate( - self, user_id: str, user_input: Dict[str, Any]) -> bool: + async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool: """Return True if validation passed.""" for data in self._data: - if data['user_id'] == user_id: + if data["user_id"] == user_id: # user_input has been validate in caller - if data['pin'] == user_input['pin']: + if data["pin"] == user_input["pin"]: return True return False diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 396a0fb8d3f..4a41ff03ef6 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -15,26 +15,32 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import config_validation as cv -from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \ - MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow +from . import ( + MultiFactorAuthModule, + MULTI_FACTOR_AUTH_MODULES, + MULTI_FACTOR_AUTH_MODULE_SCHEMA, + SetupFlow, +) -REQUIREMENTS = ['pyotp==2.2.7'] +REQUIREMENTS = ["pyotp==2.2.7"] -CONF_MESSAGE = 'message' +CONF_MESSAGE = "message" -CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({ - vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_MESSAGE, - default='{} is your Home Assistant login code'): str -}, extra=vol.PREVENT_EXTRA) +CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend( + { + vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_MESSAGE, default="{} is your Home Assistant login code"): str, + }, + extra=vol.PREVENT_EXTRA, +) STORAGE_VERSION = 1 -STORAGE_KEY = 'auth_module.notify' -STORAGE_USERS = 'users' -STORAGE_USER_ID = 'user_id' +STORAGE_KEY = "auth_module.notify" +STORAGE_USERS = "users" +STORAGE_USER_ID = "user_id" -INPUT_FIELD_CODE = 'code' +INPUT_FIELD_CODE = "code" _LOGGER = logging.getLogger(__name__) @@ -42,24 +48,28 @@ _LOGGER = logging.getLogger(__name__) def _generate_secret() -> str: """Generate a secret.""" import pyotp + return str(pyotp.random_base32()) def _generate_random() -> int: """Generate a 8 digit number.""" import pyotp - return int(pyotp.random_base32(length=8, chars=list('1234567890'))) + + return int(pyotp.random_base32(length=8, chars=list("1234567890"))) def _generate_otp(secret: str, count: int) -> str: """Generate one time password.""" import pyotp + return str(pyotp.HOTP(secret).at(count)) def _verify_otp(secret: str, otp: str, count: int) -> bool: """Verify one time password.""" import pyotp + return bool(pyotp.HOTP(secret).verify(otp, count)) @@ -67,7 +77,7 @@ def _verify_otp(secret: str, otp: str, count: int) -> bool: class NotifySetting: """Store notify setting for one user.""" - secret = attr.ib(type=str, factory=_generate_secret) # not persistent + secret = attr.ib(type=str, factory=_generate_secret) # not persistent counter = attr.ib(type=int, factory=_generate_random) # not persistent notify_service = attr.ib(type=Optional[str], default=None) target = attr.ib(type=Optional[str], default=None) @@ -76,18 +86,19 @@ class NotifySetting: _UsersDict = Dict[str, NotifySetting] -@MULTI_FACTOR_AUTH_MODULES.register('notify') +@MULTI_FACTOR_AUTH_MODULES.register("notify") class NotifyAuthModule(MultiFactorAuthModule): """Auth module send hmac-based one time password by notify service.""" - DEFAULT_TITLE = 'Notify One-Time Password' + DEFAULT_TITLE = "Notify One-Time Password" def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: """Initialize the user data store.""" super().__init__(hass, config) self._user_settings = None # type: Optional[_UsersDict] self._user_store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, private=True) + STORAGE_VERSION, STORAGE_KEY, private=True + ) self._include = config.get(CONF_INCLUDE, []) self._exclude = config.get(CONF_EXCLUDE, []) self._message_template = config[CONF_MESSAGE] @@ -119,22 +130,27 @@ class NotifyAuthModule(MultiFactorAuthModule): if self._user_settings is None: return - await self._user_store.async_save({STORAGE_USERS: { - user_id: attr.asdict( - notify_setting, filter=attr.filters.exclude( - attr.fields(NotifySetting).secret, - attr.fields(NotifySetting).counter, - )) - for user_id, notify_setting - in self._user_settings.items() - }}) + await self._user_store.async_save( + { + STORAGE_USERS: { + user_id: attr.asdict( + notify_setting, + filter=attr.filters.exclude( + attr.fields(NotifySetting).secret, + attr.fields(NotifySetting).counter, + ), + ) + for user_id, notify_setting in self._user_settings.items() + } + } + ) @callback def aync_get_available_notify_services(self) -> List[str]: """Return list of notify services.""" unordered_services = set() - for service in self.hass.services.async_services().get('notify', {}): + for service in self.hass.services.async_services().get("notify", {}): if service not in self._exclude: unordered_services.add(service) @@ -149,8 +165,8 @@ class NotifyAuthModule(MultiFactorAuthModule): Mfa module should extend SetupFlow """ return NotifySetupFlow( - self, self.input_schema, user_id, - self.aync_get_available_notify_services()) + self, self.input_schema, user_id, self.aync_get_available_notify_services() + ) async def async_setup_user(self, user_id: str, setup_data: Any) -> Any: """Set up auth module for user.""" @@ -159,8 +175,8 @@ class NotifyAuthModule(MultiFactorAuthModule): assert self._user_settings is not None self._user_settings[user_id] = NotifySetting( - notify_service=setup_data.get('notify_service'), - target=setup_data.get('target'), + notify_service=setup_data.get("notify_service"), + target=setup_data.get("target"), ) await self._async_save() @@ -182,8 +198,7 @@ class NotifyAuthModule(MultiFactorAuthModule): return user_id in self._user_settings - async def async_validate( - self, user_id: str, user_input: Dict[str, Any]) -> bool: + async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool: """Return True if validation passed.""" if self._user_settings is None: await self._async_load() @@ -195,9 +210,11 @@ class NotifyAuthModule(MultiFactorAuthModule): # user_input has been validate in caller return await self.hass.async_add_executor_job( - _verify_otp, notify_setting.secret, - user_input.get(INPUT_FIELD_CODE, ''), - notify_setting.counter) + _verify_otp, + notify_setting.secret, + user_input.get(INPUT_FIELD_CODE, ""), + notify_setting.counter, + ) async def async_initialize_login_mfa_step(self, user_id: str) -> None: """Generate code and notify user.""" @@ -207,7 +224,7 @@ class NotifyAuthModule(MultiFactorAuthModule): notify_setting = self._user_settings.get(user_id, None) if notify_setting is None: - raise ValueError('Cannot find user_id') + raise ValueError("Cannot find user_id") def generate_secret_and_one_time_password() -> str: """Generate and send one time password.""" @@ -215,11 +232,11 @@ class NotifyAuthModule(MultiFactorAuthModule): # secret and counter are not persistent notify_setting.secret = _generate_secret() notify_setting.counter = _generate_random() - return _generate_otp( - notify_setting.secret, notify_setting.counter) + return _generate_otp(notify_setting.secret, notify_setting.counter) code = await self.hass.async_add_executor_job( - generate_secret_and_one_time_password) + generate_secret_and_one_time_password + ) await self.async_notify_user(user_id, code) @@ -231,105 +248,107 @@ class NotifyAuthModule(MultiFactorAuthModule): notify_setting = self._user_settings.get(user_id, None) if notify_setting is None: - _LOGGER.error('Cannot find user %s', user_id) + _LOGGER.error("Cannot find user %s", user_id) return - await self.async_notify( # type: ignore - code, notify_setting.notify_service, notify_setting.target) + await self.async_notify( # type: ignore + code, notify_setting.notify_service, notify_setting.target + ) - async def async_notify(self, code: str, notify_service: str, - target: Optional[str] = None) -> None: + async def async_notify( + self, code: str, notify_service: str, target: Optional[str] = None + ) -> None: """Send code by notify service.""" - data = {'message': self._message_template.format(code)} + data = {"message": self._message_template.format(code)} if target: - data['target'] = [target] + data["target"] = [target] - await self.hass.services.async_call('notify', notify_service, data) + await self.hass.services.async_call("notify", notify_service, data) class NotifySetupFlow(SetupFlow): """Handler for the setup flow.""" - def __init__(self, auth_module: NotifyAuthModule, - setup_schema: vol.Schema, - user_id: str, - available_notify_services: List[str]) -> None: + def __init__( + self, + auth_module: NotifyAuthModule, + setup_schema: vol.Schema, + user_id: str, + available_notify_services: List[str], + ) -> None: """Initialize the setup flow.""" super().__init__(auth_module, setup_schema, user_id) # to fix typing complaint self._auth_module = auth_module # type: NotifyAuthModule self._available_notify_services = available_notify_services self._secret = None # type: Optional[str] - self._count = None # type: Optional[int] + self._count = None # type: Optional[int] self._notify_service = None # type: Optional[str] self._target = None # type: Optional[str] async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None) \ - -> Dict[str, Any]: + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: """Let user select available notify services.""" errors = {} # type: Dict[str, str] hass = self._auth_module.hass if user_input: - self._notify_service = user_input['notify_service'] - self._target = user_input.get('target') + self._notify_service = user_input["notify_service"] + self._target = user_input.get("target") self._secret = await hass.async_add_executor_job(_generate_secret) self._count = await hass.async_add_executor_job(_generate_random) return await self.async_step_setup() if not self._available_notify_services: - return self.async_abort(reason='no_available_service') + return self.async_abort(reason="no_available_service") schema = OrderedDict() # type: Dict[str, Any] - schema['notify_service'] = vol.In(self._available_notify_services) - schema['target'] = vol.Optional(str) + schema["notify_service"] = vol.In(self._available_notify_services) + schema["target"] = vol.Optional(str) return self.async_show_form( - step_id='init', - data_schema=vol.Schema(schema), - errors=errors + step_id="init", data_schema=vol.Schema(schema), errors=errors ) async def async_step_setup( - self, user_input: Optional[Dict[str, str]] = None) \ - -> Dict[str, Any]: + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: """Verify user can recevie one-time password.""" errors = {} # type: Dict[str, str] hass = self._auth_module.hass if user_input: verified = await hass.async_add_executor_job( - _verify_otp, self._secret, user_input['code'], self._count) + _verify_otp, self._secret, user_input["code"], self._count + ) if verified: await self._auth_module.async_setup_user( - self._user_id, { - 'notify_service': self._notify_service, - 'target': self._target, - }) - return self.async_create_entry( - title=self._auth_module.name, - data={} + self._user_id, + {"notify_service": self._notify_service, "target": self._target}, ) + return self.async_create_entry(title=self._auth_module.name, data={}) - errors['base'] = 'invalid_code' + errors["base"] = "invalid_code" # generate code every time, no retry logic assert self._secret and self._count code = await hass.async_add_executor_job( - _generate_otp, self._secret, self._count) + _generate_otp, self._secret, self._count + ) assert self._notify_service try: await self._auth_module.async_notify( - code, self._notify_service, self._target) + code, self._notify_service, self._target + ) except ServiceNotFound: - return self.async_abort(reason='notify_service_not_exist') + return self.async_abort(reason="notify_service_not_exist") return self.async_show_form( - step_id='setup', + step_id="setup", data_schema=self._setup_schema, - description_placeholders={'notify_service': self._notify_service}, + description_placeholders={"notify_service": self._notify_service}, errors=errors, ) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index bb07d9e479f..22d153e3420 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -9,23 +9,26 @@ import voluptuous as vol from homeassistant.auth.models import User from homeassistant.core import HomeAssistant -from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \ - MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow +from . import ( + MultiFactorAuthModule, + MULTI_FACTOR_AUTH_MODULES, + MULTI_FACTOR_AUTH_MODULE_SCHEMA, + SetupFlow, +) -REQUIREMENTS = ['pyotp==2.2.7', 'PyQRCode==1.2.1'] +REQUIREMENTS = ["pyotp==2.2.7", "PyQRCode==1.2.1"] -CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({ -}, extra=vol.PREVENT_EXTRA) +CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA) STORAGE_VERSION = 1 -STORAGE_KEY = 'auth_module.totp' -STORAGE_USERS = 'users' -STORAGE_USER_ID = 'user_id' -STORAGE_OTA_SECRET = 'ota_secret' +STORAGE_KEY = "auth_module.totp" +STORAGE_USERS = "users" +STORAGE_USER_ID = "user_id" +STORAGE_OTA_SECRET = "ota_secret" -INPUT_FIELD_CODE = 'code' +INPUT_FIELD_CODE = "code" -DUMMY_SECRET = 'FPPTH34D4E3MI2HG' +DUMMY_SECRET = "FPPTH34D4E3MI2HG" _LOGGER = logging.getLogger(__name__) @@ -38,10 +41,15 @@ def _generate_qr_code(data: str) -> str: with BytesIO() as buffer: qr_code.svg(file=buffer, scale=4) - return '{}'.format( - buffer.getvalue().decode("ascii").replace('\n', '') - .replace('' - '' + ' Tuple[str, str, str]: ota_secret = pyotp.random_base32() url = pyotp.totp.TOTP(ota_secret).provisioning_uri( - username, issuer_name="Home Assistant") + username, issuer_name="Home Assistant" + ) image = _generate_qr_code(url) return ota_secret, url, image -@MULTI_FACTOR_AUTH_MODULES.register('totp') +@MULTI_FACTOR_AUTH_MODULES.register("totp") class TotpAuthModule(MultiFactorAuthModule): """Auth module validate time-based one time password.""" - DEFAULT_TITLE = 'Time-based One Time Password' + DEFAULT_TITLE = "Time-based One Time Password" MAX_RETRY_TIME = 5 def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: @@ -68,7 +77,8 @@ class TotpAuthModule(MultiFactorAuthModule): super().__init__(hass, config) self._users = None # type: Optional[Dict[str, str]] self._user_store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, private=True) + STORAGE_VERSION, STORAGE_KEY, private=True + ) self._init_lock = asyncio.Lock() @property @@ -93,14 +103,13 @@ class TotpAuthModule(MultiFactorAuthModule): """Save data.""" await self._user_store.async_save({STORAGE_USERS: self._users}) - def _add_ota_secret(self, user_id: str, - secret: Optional[str] = None) -> str: + def _add_ota_secret(self, user_id: str, secret: Optional[str] = None) -> str: """Create a ota_secret for user.""" import pyotp ota_secret = secret or pyotp.random_base32() # type: str - self._users[user_id] = ota_secret # type: ignore + self._users[user_id] = ota_secret # type: ignore return ota_secret async def async_setup_flow(self, user_id: str) -> SetupFlow: @@ -108,7 +117,7 @@ class TotpAuthModule(MultiFactorAuthModule): Mfa module should extend SetupFlow """ - user = await self.hass.auth.async_get_user(user_id) # type: ignore + user = await self.hass.auth.async_get_user(user_id) # type: ignore return TotpSetupFlow(self, self.input_schema, user) async def async_setup_user(self, user_id: str, setup_data: Any) -> str: @@ -117,7 +126,8 @@ class TotpAuthModule(MultiFactorAuthModule): await self._async_load() result = await self.hass.async_add_executor_job( - self._add_ota_secret, user_id, setup_data.get('secret')) + self._add_ota_secret, user_id, setup_data.get("secret") + ) await self._async_save() return result @@ -127,7 +137,7 @@ class TotpAuthModule(MultiFactorAuthModule): if self._users is None: await self._async_load() - if self._users.pop(user_id, None): # type: ignore + if self._users.pop(user_id, None): # type: ignore await self._async_save() async def async_is_user_setup(self, user_id: str) -> bool: @@ -135,10 +145,9 @@ class TotpAuthModule(MultiFactorAuthModule): if self._users is None: await self._async_load() - return user_id in self._users # type: ignore + return user_id in self._users # type: ignore - async def async_validate( - self, user_id: str, user_input: Dict[str, Any]) -> bool: + async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool: """Return True if validation passed.""" if self._users is None: await self._async_load() @@ -146,7 +155,8 @@ class TotpAuthModule(MultiFactorAuthModule): # user_input has been validate in caller # set INPUT_FIELD_CODE as vol.Required is not user friendly return await self.hass.async_add_executor_job( - self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, '')) + self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, "") + ) def _validate_2fa(self, user_id: str, code: str) -> bool: """Validate two factor authentication code.""" @@ -165,9 +175,9 @@ class TotpAuthModule(MultiFactorAuthModule): class TotpSetupFlow(SetupFlow): """Handler for the setup flow.""" - def __init__(self, auth_module: TotpAuthModule, - setup_schema: vol.Schema, - user: User) -> None: + def __init__( + self, auth_module: TotpAuthModule, setup_schema: vol.Schema, user: User + ) -> None: """Initialize the setup flow.""" super().__init__(auth_module, setup_schema, user.id) # to fix typing complaint @@ -178,8 +188,8 @@ class TotpSetupFlow(SetupFlow): self._image = None # type Optional[str] async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None) \ - -> Dict[str, Any]: + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. @@ -191,30 +201,31 @@ class TotpSetupFlow(SetupFlow): if user_input: verified = await self.hass.async_add_executor_job( # type: ignore - pyotp.TOTP(self._ota_secret).verify, user_input['code']) + pyotp.TOTP(self._ota_secret).verify, user_input["code"] + ) if verified: result = await self._auth_module.async_setup_user( - self._user_id, {'secret': self._ota_secret}) + self._user_id, {"secret": self._ota_secret} + ) return self.async_create_entry( - title=self._auth_module.name, - data={'result': result} + title=self._auth_module.name, data={"result": result} ) - errors['base'] = 'invalid_code' + errors["base"] = "invalid_code" else: hass = self._auth_module.hass - self._ota_secret, self._url, self._image = \ - await hass.async_add_executor_job( # type: ignore - _generate_secret_and_qr_code, str(self._user.name)) + self._ota_secret, self._url, self._image = await hass.async_add_executor_job( # type: ignore + _generate_secret_and_qr_code, str(self._user.name) + ) return self.async_show_form( - step_id='init', + step_id="init", data_schema=self._setup_schema, description_placeholders={ - 'code': self._ota_secret, - 'url': self._url, - 'qr_code': self._image + "code": self._ota_secret, + "url": self._url, + "qr_code": self._image, }, - errors=errors + errors=errors, ) diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 588d80047be..533d7672ee4 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -11,9 +11,9 @@ from . import permissions as perm_mdl from .const import GROUP_ID_ADMIN from .util import generate_secret -TOKEN_TYPE_NORMAL = 'normal' -TOKEN_TYPE_SYSTEM = 'system' -TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token' +TOKEN_TYPE_NORMAL = "normal" +TOKEN_TYPE_SYSTEM = "system" +TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token" @attr.s(slots=True) @@ -32,7 +32,7 @@ class User: name = attr.ib(type=str) # type: Optional[str] perm_lookup = attr.ib( - type=perm_mdl.PermissionLookup, cmp=False, + type=perm_mdl.PermissionLookup, cmp=False ) # type: perm_mdl.PermissionLookup id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) is_owner = attr.ib(type=bool, default=False) @@ -42,9 +42,7 @@ class User: groups = attr.ib(type=List, factory=list, cmp=False) # type: List[Group] # List of credentials of a user. - credentials = attr.ib( - type=list, factory=list, cmp=False - ) # type: List[Credentials] + credentials = attr.ib(type=list, factory=list, cmp=False) # type: List[Credentials] # Tokens associated with a user. refresh_tokens = attr.ib( @@ -52,10 +50,7 @@ class User: ) # type: Dict[str, RefreshToken] _permissions = attr.ib( - type=Optional[perm_mdl.PolicyPermissions], - init=False, - cmp=False, - default=None, + type=Optional[perm_mdl.PolicyPermissions], init=False, cmp=False, default=None ) @property @@ -68,9 +63,9 @@ class User: return self._permissions self._permissions = perm_mdl.PolicyPermissions( - perm_mdl.merge_policies([ - group.policy for group in self.groups]), - self.perm_lookup) + perm_mdl.merge_policies([group.policy for group in self.groups]), + self.perm_lookup, + ) return self._permissions @@ -80,8 +75,7 @@ class User: if self.is_owner: return True - return self.is_active and any( - gr.id == GROUP_ID_ADMIN for gr in self.groups) + return self.is_active and any(gr.id == GROUP_ID_ADMIN for gr in self.groups) def invalidate_permission_cache(self) -> None: """Invalidate permission cache.""" @@ -97,10 +91,13 @@ class RefreshToken: access_token_expiration = attr.ib(type=timedelta) client_name = attr.ib(type=Optional[str], default=None) client_icon = attr.ib(type=Optional[str], default=None) - token_type = attr.ib(type=str, default=TOKEN_TYPE_NORMAL, - validator=attr.validators.in_(( - TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, - TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN))) + token_type = attr.ib( + type=str, + default=TOKEN_TYPE_NORMAL, + validator=attr.validators.in_( + (TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN) + ), + ) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) created_at = attr.ib(type=datetime, factory=dt_util.utcnow) token = attr.ib(type=str, factory=lambda: generate_secret(64)) @@ -124,5 +121,4 @@ class Credentials: is_new = attr.ib(type=bool, default=True) -UserMeta = NamedTuple("UserMeta", - [('name', Optional[str]), ('is_active', bool)]) +UserMeta = NamedTuple("UserMeta", [("name", Optional[str]), ("is_active", bool)]) diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 0079f11447b..5680b0aecb2 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -1,8 +1,17 @@ """Permissions for Home Assistant.""" import logging from typing import ( # noqa: F401 - cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union, - TYPE_CHECKING) + cast, + Any, + Callable, + Dict, + List, + Mapping, + Set, + Tuple, + Union, + TYPE_CHECKING, +) import voluptuous as vol @@ -14,9 +23,7 @@ from .merge import merge_policies # noqa from .util import test_all -POLICY_SCHEMA = vol.Schema({ - vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA -}) +POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA}) _LOGGER = logging.getLogger(__name__) @@ -47,8 +54,7 @@ class AbstractPermissions: class PolicyPermissions(AbstractPermissions): """Handle permissions.""" - def __init__(self, policy: PolicyType, - perm_lookup: PermissionLookup) -> None: + def __init__(self, policy: PolicyType, perm_lookup: PermissionLookup) -> None: """Initialize the permission class.""" self._policy = policy self._perm_lookup = perm_lookup @@ -59,14 +65,12 @@ class PolicyPermissions(AbstractPermissions): def _entity_func(self) -> Callable[[str, str], bool]: """Return a function that can test entity access.""" - return compile_entities(self._policy.get(CAT_ENTITIES), - self._perm_lookup) + return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup) def __eq__(self, other: Any) -> bool: """Equals check.""" # pylint: disable=protected-access - return (isinstance(other, PolicyPermissions) and - other._policy == self._policy) + return isinstance(other, PolicyPermissions) and other._policy == self._policy class _OwnerPermissions(AbstractPermissions): diff --git a/homeassistant/auth/permissions/const.py b/homeassistant/auth/permissions/const.py index d390d010dee..e6c44036a7e 100644 --- a/homeassistant/auth/permissions/const.py +++ b/homeassistant/auth/permissions/const.py @@ -1,8 +1,8 @@ """Permission constants.""" -CAT_ENTITIES = 'entities' -CAT_CONFIG_ENTRIES = 'config_entries' -SUBCAT_ALL = 'all' +CAT_ENTITIES = "entities" +CAT_CONFIG_ENTRIES = "config_entries" +SUBCAT_ALL = "all" -POLICY_READ = 'read' -POLICY_CONTROL = 'control' -POLICY_EDIT = 'edit' +POLICY_READ = "read" +POLICY_CONTROL = "control" +POLICY_EDIT = "edit" diff --git a/homeassistant/auth/permissions/entities.py b/homeassistant/auth/permissions/entities.py index 3d7fc80307e..2708693743a 100644 --- a/homeassistant/auth/permissions/entities.py +++ b/homeassistant/auth/permissions/entities.py @@ -7,51 +7,59 @@ import voluptuous as vol from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT from .models import PermissionLookup from .types import CategoryType, SubCategoryDict, ValueType + # pylint: disable=unused-import from .util import SubCatLookupType, lookup_all, compile_policy # noqa -SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({ - vol.Optional(POLICY_READ): True, - vol.Optional(POLICY_CONTROL): True, - vol.Optional(POLICY_EDIT): True, -})) +SINGLE_ENTITY_SCHEMA = vol.Any( + True, + vol.Schema( + { + vol.Optional(POLICY_READ): True, + vol.Optional(POLICY_CONTROL): True, + vol.Optional(POLICY_EDIT): True, + } + ), +) -ENTITY_DOMAINS = 'domains' -ENTITY_AREAS = 'area_ids' -ENTITY_DEVICE_IDS = 'device_ids' -ENTITY_ENTITY_IDS = 'entity_ids' +ENTITY_DOMAINS = "domains" +ENTITY_AREAS = "area_ids" +ENTITY_DEVICE_IDS = "device_ids" +ENTITY_ENTITY_IDS = "entity_ids" -ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({ - str: SINGLE_ENTITY_SCHEMA -})) +ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({str: SINGLE_ENTITY_SCHEMA})) -ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({ - vol.Optional(SUBCAT_ALL): SINGLE_ENTITY_SCHEMA, - vol.Optional(ENTITY_AREAS): ENTITY_VALUES_SCHEMA, - vol.Optional(ENTITY_DEVICE_IDS): ENTITY_VALUES_SCHEMA, - vol.Optional(ENTITY_DOMAINS): ENTITY_VALUES_SCHEMA, - vol.Optional(ENTITY_ENTITY_IDS): ENTITY_VALUES_SCHEMA, -})) +ENTITY_POLICY_SCHEMA = vol.Any( + True, + vol.Schema( + { + vol.Optional(SUBCAT_ALL): SINGLE_ENTITY_SCHEMA, + vol.Optional(ENTITY_AREAS): ENTITY_VALUES_SCHEMA, + vol.Optional(ENTITY_DEVICE_IDS): ENTITY_VALUES_SCHEMA, + vol.Optional(ENTITY_DOMAINS): ENTITY_VALUES_SCHEMA, + vol.Optional(ENTITY_ENTITY_IDS): ENTITY_VALUES_SCHEMA, + } + ), +) -def _lookup_domain(perm_lookup: PermissionLookup, - domains_dict: SubCategoryDict, - entity_id: str) -> Optional[ValueType]: +def _lookup_domain( + perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str +) -> Optional[ValueType]: """Look up entity permissions by domain.""" return domains_dict.get(entity_id.split(".", 1)[0]) -def _lookup_area(perm_lookup: PermissionLookup, area_dict: SubCategoryDict, - entity_id: str) -> Optional[ValueType]: +def _lookup_area( + perm_lookup: PermissionLookup, area_dict: SubCategoryDict, entity_id: str +) -> Optional[ValueType]: """Look up entity permissions by area.""" entity_entry = perm_lookup.entity_registry.async_get(entity_id) if entity_entry is None or entity_entry.device_id is None: return None - device_entry = perm_lookup.device_registry.async_get( - entity_entry.device_id - ) + device_entry = perm_lookup.device_registry.async_get(entity_entry.device_id) if device_entry is None or device_entry.area_id is None: return None @@ -59,9 +67,9 @@ def _lookup_area(perm_lookup: PermissionLookup, area_dict: SubCategoryDict, return area_dict.get(device_entry.area_id) -def _lookup_device(perm_lookup: PermissionLookup, - devices_dict: SubCategoryDict, - entity_id: str) -> Optional[ValueType]: +def _lookup_device( + perm_lookup: PermissionLookup, devices_dict: SubCategoryDict, entity_id: str +) -> Optional[ValueType]: """Look up entity permissions by device.""" entity_entry = perm_lookup.entity_registry.async_get(entity_id) @@ -71,15 +79,16 @@ def _lookup_device(perm_lookup: PermissionLookup, return devices_dict.get(entity_entry.device_id) -def _lookup_entity_id(perm_lookup: PermissionLookup, - entities_dict: SubCategoryDict, - entity_id: str) -> Optional[ValueType]: +def _lookup_entity_id( + perm_lookup: PermissionLookup, entities_dict: SubCategoryDict, entity_id: str +) -> Optional[ValueType]: """Look up entity permission by entity id.""" return entities_dict.get(entity_id) -def compile_entities(policy: CategoryType, perm_lookup: PermissionLookup) \ - -> Callable[[str, str], bool]: +def compile_entities( + policy: CategoryType, perm_lookup: PermissionLookup +) -> Callable[[str, str], bool]: """Compile policy into a function that tests policy.""" subcategories = OrderedDict() # type: SubCatLookupType subcategories[ENTITY_ENTITY_IDS] = _lookup_entity_id diff --git a/homeassistant/auth/permissions/merge.py b/homeassistant/auth/permissions/merge.py index ec6375a0e3d..f8b3639ad5a 100644 --- a/homeassistant/auth/permissions/merge.py +++ b/homeassistant/auth/permissions/merge.py @@ -1,6 +1,5 @@ """Merging of policies.""" -from typing import ( # noqa: F401 - cast, Dict, List, Set) +from typing import cast, Dict, List, Set # noqa: F401 from .types import PolicyType, CategoryType @@ -14,8 +13,9 @@ def merge_policies(policies: List[PolicyType]) -> PolicyType: if category in seen: continue seen.add(category) - new_policy[category] = _merge_policies([ - policy.get(category) for policy in policies]) + new_policy[category] = _merge_policies( + [policy.get(category) for policy in policies] + ) cast(PolicyType, new_policy) return new_policy diff --git a/homeassistant/auth/permissions/models.py b/homeassistant/auth/permissions/models.py index 10a76a4ec73..31bea635bbe 100644 --- a/homeassistant/auth/permissions/models.py +++ b/homeassistant/auth/permissions/models.py @@ -5,17 +5,13 @@ import attr if TYPE_CHECKING: # pylint: disable=unused-import - from homeassistant.helpers import ( # noqa - entity_registry as ent_reg, - ) - from homeassistant.helpers import ( # noqa - device_registry as dev_reg, - ) + from homeassistant.helpers import entity_registry as ent_reg # noqa + from homeassistant.helpers import device_registry as dev_reg # noqa @attr.s(slots=True) class PermissionLookup: """Class to hold data for permission lookups.""" - entity_registry = attr.ib(type='ent_reg.EntityRegistry') - device_registry = attr.ib(type='dev_reg.DeviceRegistry') + entity_registry = attr.ib(type="ent_reg.EntityRegistry") + device_registry = attr.ib(type="dev_reg.DeviceRegistry") diff --git a/homeassistant/auth/permissions/system_policies.py b/homeassistant/auth/permissions/system_policies.py index bf65c0a85a6..b40400304cc 100644 --- a/homeassistant/auth/permissions/system_policies.py +++ b/homeassistant/auth/permissions/system_policies.py @@ -1,18 +1,8 @@ """System policies.""" from .const import CAT_ENTITIES, SUBCAT_ALL, POLICY_READ -ADMIN_POLICY = { - CAT_ENTITIES: True, -} +ADMIN_POLICY = {CAT_ENTITIES: True} -USER_POLICY = { - CAT_ENTITIES: True, -} +USER_POLICY = {CAT_ENTITIES: True} -READ_ONLY_POLICY = { - CAT_ENTITIES: { - SUBCAT_ALL: { - POLICY_READ: True - } - } -} +READ_ONLY_POLICY = {CAT_ENTITIES: {SUBCAT_ALL: {POLICY_READ: True}}} diff --git a/homeassistant/auth/permissions/types.py b/homeassistant/auth/permissions/types.py index 5479e59dcb6..6ce394ebb92 100644 --- a/homeassistant/auth/permissions/types.py +++ b/homeassistant/auth/permissions/types.py @@ -7,17 +7,13 @@ ValueType = Union[ # Example: entities.all = { read: true, control: true } Mapping[str, bool], bool, - None + None, ] # Example: entities.domains = { light: … } SubCategoryDict = Mapping[str, ValueType] -SubCategoryType = Union[ - SubCategoryDict, - bool, - None -] +SubCategoryType = Union[SubCategoryDict, bool, None] CategoryType = Union[ # Example: entities.domains @@ -25,7 +21,7 @@ CategoryType = Union[ # Example: entities.all Mapping[str, ValueType], bool, - None + None, ] # Example: { entities: … } diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 0d334c4a3ba..6b44cbf61d4 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -1,34 +1,34 @@ """Helpers to deal with permissions.""" from functools import wraps -from typing import Callable, Dict, List, Optional, Union, cast # noqa: F401 +from typing import Callable, Dict, List, Optional, cast # noqa: F401 from .const import SUBCAT_ALL from .models import PermissionLookup from .types import CategoryType, SubCategoryDict, ValueType -LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], - Optional[ValueType]] +LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], Optional[ValueType]] SubCatLookupType = Dict[str, LookupFunc] -def lookup_all(perm_lookup: PermissionLookup, lookup_dict: SubCategoryDict, - object_id: str) -> ValueType: +def lookup_all( + perm_lookup: PermissionLookup, lookup_dict: SubCategoryDict, object_id: str +) -> ValueType: """Look up permission for all.""" # In case of ALL category, lookup_dict IS the schema. return cast(ValueType, lookup_dict) def compile_policy( - policy: CategoryType, subcategories: SubCatLookupType, - perm_lookup: PermissionLookup - ) -> Callable[[str, str], bool]: # noqa + policy: CategoryType, subcategories: SubCatLookupType, perm_lookup: PermissionLookup +) -> Callable[[str, str], bool]: # noqa """Compile policy into a function that tests policy. Subcategories are mapping key -> lookup function, ordered by highest priority first. """ # None, False, empty dict if not policy: + def apply_policy_deny_all(entity_id: str, key: str) -> bool: """Decline all.""" return False @@ -36,6 +36,7 @@ def compile_policy( return apply_policy_deny_all if policy is True: + def apply_policy_allow_all(entity_id: str, key: str) -> bool: """Approve all.""" return True @@ -44,7 +45,7 @@ def compile_policy( assert isinstance(policy, dict) - funcs = [] # type: List[Callable[[str, str], Union[None, bool]]] + funcs = [] # type: List[Callable[[str, str], Optional[bool]]] for key, lookup_func in subcategories.items(): lookup_value = policy.get(key) @@ -54,8 +55,7 @@ def compile_policy( return lambda object_id, key: True if lookup_value is not None: - funcs.append(_gen_dict_test_func( - perm_lookup, lookup_func, lookup_value)) + funcs.append(_gen_dict_test_func(perm_lookup, lookup_func, lookup_value)) if len(funcs) == 1: func = funcs[0] @@ -79,15 +79,13 @@ def compile_policy( def _gen_dict_test_func( - perm_lookup: PermissionLookup, - lookup_func: LookupFunc, - lookup_dict: SubCategoryDict - ) -> Callable[[str, str], Optional[bool]]: # noqa + perm_lookup: PermissionLookup, lookup_func: LookupFunc, lookup_dict: SubCategoryDict +) -> Callable[[str, str], Optional[bool]]: # noqa """Generate a lookup function.""" + def test_value(object_id: str, key: str) -> Optional[bool]: """Test if permission is allowed based on the keys.""" - schema = lookup_func( - perm_lookup, lookup_dict, object_id) # type: ValueType + schema = lookup_func(perm_lookup, lookup_dict, object_id) # type: ValueType if schema is None or isinstance(schema, bool): return schema diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 8828782c886..c720cf0df64 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -19,25 +19,29 @@ from ..const import MFA_SESSION_EXPIRATION from ..models import Credentials, User, UserMeta # noqa: F401 _LOGGER = logging.getLogger(__name__) -DATA_REQS = 'auth_prov_reqs_processed' +DATA_REQS = "auth_prov_reqs_processed" AUTH_PROVIDERS = Registry() -AUTH_PROVIDER_SCHEMA = vol.Schema({ - vol.Required(CONF_TYPE): str, - vol.Optional(CONF_NAME): str, - # Specify ID if you have two auth providers for same type. - vol.Optional(CONF_ID): str, -}, extra=vol.ALLOW_EXTRA) +AUTH_PROVIDER_SCHEMA = vol.Schema( + { + vol.Required(CONF_TYPE): str, + vol.Optional(CONF_NAME): str, + # Specify ID if you have two auth providers for same type. + vol.Optional(CONF_ID): str, + }, + extra=vol.ALLOW_EXTRA, +) class AuthProvider: """Provider of user authentication.""" - DEFAULT_TITLE = 'Unnamed auth provider' + DEFAULT_TITLE = "Unnamed auth provider" - def __init__(self, hass: HomeAssistant, store: AuthStore, - config: Dict[str, Any]) -> None: + def __init__( + self, hass: HomeAssistant, store: AuthStore, config: Dict[str, Any] + ) -> None: """Initialize an auth provider.""" self.hass = hass self.store = store @@ -73,22 +77,22 @@ class AuthProvider: credentials for user in users for credentials in user.credentials - if (credentials.auth_provider_type == self.type and - credentials.auth_provider_id == self.id) + if ( + credentials.auth_provider_type == self.type + and credentials.auth_provider_id == self.id + ) ] @callback def async_create_credentials(self, data: Dict[str, str]) -> Credentials: """Create credentials.""" return Credentials( - auth_provider_type=self.type, - auth_provider_id=self.id, - data=data, + auth_provider_type=self.type, auth_provider_id=self.id, data=data ) # Implement by extending class - async def async_login_flow(self, context: Optional[Dict]) -> 'LoginFlow': + async def async_login_flow(self, context: Optional[Dict]) -> "LoginFlow": """Return the data flow for logging in with auth provider. Auth provider should extend LoginFlow and return an instance. @@ -96,22 +100,28 @@ class AuthProvider: raise NotImplementedError async def async_get_or_create_credentials( - self, flow_result: Dict[str, str]) -> Credentials: + self, flow_result: Dict[str, str] + ) -> Credentials: """Get credentials based on the flow result.""" raise NotImplementedError async def async_user_meta_for_credentials( - self, credentials: Credentials) -> UserMeta: + self, credentials: Credentials + ) -> UserMeta: """Return extra user metadata for credentials. Will be used to populate info when creating a new user. """ raise NotImplementedError + async def async_initialize(self) -> None: + """Initialize the auth provider.""" + pass + async def auth_provider_from_config( - hass: HomeAssistant, store: AuthStore, - config: Dict[str, Any]) -> AuthProvider: + hass: HomeAssistant, store: AuthStore, config: Dict[str, Any] +) -> AuthProvider: """Initialize an auth provider from a config.""" provider_name = config[CONF_TYPE] module = await load_auth_provider_module(hass, provider_name) @@ -119,25 +129,31 @@ async def auth_provider_from_config( try: config = module.CONFIG_SCHEMA(config) # type: ignore except vol.Invalid as err: - _LOGGER.error('Invalid configuration for auth provider %s: %s', - provider_name, humanize_error(config, err)) + _LOGGER.error( + "Invalid configuration for auth provider %s: %s", + provider_name, + humanize_error(config, err), + ) raise return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore async def load_auth_provider_module( - hass: HomeAssistant, provider: str) -> types.ModuleType: + hass: HomeAssistant, provider: str +) -> types.ModuleType: """Load an auth provider.""" try: module = importlib.import_module( - 'homeassistant.auth.providers.{}'.format(provider)) + "homeassistant.auth.providers.{}".format(provider) + ) except ImportError as err: - _LOGGER.error('Unable to load auth provider %s: %s', provider, err) - raise HomeAssistantError('Unable to load auth provider {}: {}'.format( - provider, err)) + _LOGGER.error("Unable to load auth provider %s: %s", provider, err) + raise HomeAssistantError( + "Unable to load auth provider {}: {}".format(provider, err) + ) - if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'): + if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"): return module processed = hass.data.get(DATA_REQS) @@ -150,12 +166,13 @@ async def load_auth_provider_module( # https://github.com/python/mypy/issues/1424 reqs = module.REQUIREMENTS # type: ignore req_success = await requirements.async_process_requirements( - hass, 'auth provider {}'.format(provider), reqs) + hass, "auth provider {}".format(provider), reqs + ) if not req_success: raise HomeAssistantError( - 'Unable to process requirements of auth provider {}'.format( - provider)) + "Unable to process requirements of auth provider {}".format(provider) + ) processed.add(provider) return module @@ -175,8 +192,8 @@ class LoginFlow(data_entry_flow.FlowHandler): self.user = None # type: Optional[User] async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None) \ - -> Dict[str, Any]: + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: """Handle the first step of login flow. Return self.async_show_form(step_id='init') if user_input is None. @@ -185,80 +202,75 @@ class LoginFlow(data_entry_flow.FlowHandler): raise NotImplementedError async def async_step_select_mfa_module( - self, user_input: Optional[Dict[str, str]] = None) \ - -> Dict[str, Any]: + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: """Handle the step of select mfa module.""" errors = {} if user_input is not None: - auth_module = user_input.get('multi_factor_auth_module') + auth_module = user_input.get("multi_factor_auth_module") if auth_module in self.available_mfa_modules: self._auth_module_id = auth_module return await self.async_step_mfa() - errors['base'] = 'invalid_auth_module' + errors["base"] = "invalid_auth_module" if len(self.available_mfa_modules) == 1: self._auth_module_id = list(self.available_mfa_modules.keys())[0] return await self.async_step_mfa() return self.async_show_form( - step_id='select_mfa_module', - data_schema=vol.Schema({ - 'multi_factor_auth_module': vol.In(self.available_mfa_modules) - }), + step_id="select_mfa_module", + data_schema=vol.Schema( + {"multi_factor_auth_module": vol.In(self.available_mfa_modules)} + ), errors=errors, ) async def async_step_mfa( - self, user_input: Optional[Dict[str, str]] = None) \ - -> Dict[str, Any]: + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: """Handle the step of mfa validation.""" assert self.user errors = {} - auth_module = self._auth_manager.get_auth_mfa_module( - self._auth_module_id) + auth_module = self._auth_manager.get_auth_mfa_module(self._auth_module_id) if auth_module is None: # Given an invalid input to async_step_select_mfa_module # will show invalid_auth_module error return await self.async_step_select_mfa_module(user_input={}) - if user_input is None and hasattr(auth_module, - 'async_initialize_login_mfa_step'): + if user_input is None and hasattr( + auth_module, "async_initialize_login_mfa_step" + ): try: await auth_module.async_initialize_login_mfa_step(self.user.id) except HomeAssistantError: - _LOGGER.exception('Error initializing MFA step') - return self.async_abort(reason='unknown_error') + _LOGGER.exception("Error initializing MFA step") + return self.async_abort(reason="unknown_error") if user_input is not None: expires = self.created_at + MFA_SESSION_EXPIRATION if dt_util.utcnow() > expires: - return self.async_abort( - reason='login_expired' - ) + return self.async_abort(reason="login_expired") - result = await auth_module.async_validate( - self.user.id, user_input) + result = await auth_module.async_validate(self.user.id, user_input) if not result: - errors['base'] = 'invalid_code' + errors["base"] = "invalid_code" self.invalid_mfa_times += 1 if self.invalid_mfa_times >= auth_module.MAX_RETRY_TIME > 0: - return self.async_abort( - reason='too_many_retry' - ) + return self.async_abort(reason="too_many_retry") if not errors: return await self.async_finish(self.user) description_placeholders = { - 'mfa_module_name': auth_module.name, - 'mfa_module_id': auth_module.id, + "mfa_module_name": auth_module.name, + "mfa_module_id": auth_module.id, } # type: Dict[str, Optional[str]] return self.async_show_form( - step_id='mfa', + step_id="mfa", data_schema=auth_module.input_schema, description_placeholders=description_placeholders, errors=errors, @@ -266,7 +278,4 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_finish(self, flow_result: Any) -> Dict: """Handle the pass of login flow.""" - return self.async_create_entry( - title=self._auth_provider.name, - data=flow_result - ) + return self.async_create_entry(title=self._auth_provider.name, data=flow_result) diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 9cec34c1340..cdf1a533412 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -19,15 +19,16 @@ CONF_COMMAND = "command" CONF_ARGS = "args" CONF_META = "meta" -CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ - vol.Required(CONF_COMMAND): vol.All( - str, - os.path.normpath, - msg="must be an absolute path" - ), - vol.Optional(CONF_ARGS, default=None): vol.Any(vol.DefaultTo(list), [str]), - vol.Optional(CONF_META, default=False): bool, -}, extra=vol.PREVENT_EXTRA) +CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( + { + vol.Required(CONF_COMMAND): vol.All( + str, os.path.normpath, msg="must be an absolute path" + ), + vol.Optional(CONF_ARGS, default=None): vol.Any(vol.DefaultTo(list), [str]), + vol.Optional(CONF_META, default=False): bool, + }, + extra=vol.PREVENT_EXTRA, +) _LOGGER = logging.getLogger(__name__) @@ -60,29 +61,27 @@ class CommandLineAuthProvider(AuthProvider): async def async_validate_login(self, username: str, password: str) -> None: """Validate a username and password.""" - env = { - "username": username, - "password": password, - } + env = {"username": username, "password": password} try: # pylint: disable=no-member process = await asyncio.subprocess.create_subprocess_exec( - self.config[CONF_COMMAND], *self.config[CONF_ARGS], + self.config[CONF_COMMAND], + *self.config[CONF_ARGS], env=env, - stdout=asyncio.subprocess.PIPE - if self.config[CONF_META] else None, + stdout=asyncio.subprocess.PIPE if self.config[CONF_META] else None, ) - stdout, _ = (await process.communicate()) + stdout, _ = await process.communicate() except OSError as err: # happens when command doesn't exist or permission is denied - _LOGGER.error("Error while authenticating %r: %s", - username, err) + _LOGGER.error("Error while authenticating %r: %s", username, err) raise InvalidAuthError if process.returncode != 0: - _LOGGER.error("User %r failed to authenticate, command exited " - "with code %d.", - username, process.returncode) + _LOGGER.error( + "User %r failed to authenticate, command exited " "with code %d.", + username, + process.returncode, + ) raise InvalidAuthError if self.config[CONF_META]: @@ -103,7 +102,7 @@ class CommandLineAuthProvider(AuthProvider): self._user_meta[username] = meta async def async_get_or_create_credentials( - self, flow_result: Dict[str, str] + self, flow_result: Dict[str, str] ) -> Credentials: """Get credentials based on the flow result.""" username = flow_result["username"] @@ -112,29 +111,24 @@ class CommandLineAuthProvider(AuthProvider): return credential # Create new credentials. - return self.async_create_credentials({ - "username": username, - }) + return self.async_create_credentials({"username": username}) async def async_user_meta_for_credentials( - self, credentials: Credentials + self, credentials: Credentials ) -> UserMeta: """Return extra user metadata for credentials. Currently, only name is supported. """ meta = self._user_meta.get(credentials.data["username"], {}) - return UserMeta( - name=meta.get("name"), - is_active=True, - ) + return UserMeta(name=meta.get("name"), is_active=True) class CommandLineLoginFlow(LoginFlow): """Handler for the login flow.""" async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None + self, user_input: Optional[Dict[str, str]] = None ) -> Dict[str, Any]: """Handle the step of the form.""" errors = {} @@ -142,10 +136,9 @@ class CommandLineLoginFlow(LoginFlow): if user_input is not None: user_input["username"] = user_input["username"].strip() try: - await cast(CommandLineAuthProvider, self._auth_provider) \ - .async_validate_login( - user_input["username"], user_input["password"] - ) + await cast( + CommandLineAuthProvider, self._auth_provider + ).async_validate_login(user_input["username"], user_input["password"]) except InvalidAuthError: errors["base"] = "invalid_auth" @@ -158,7 +151,5 @@ class CommandLineLoginFlow(LoginFlow): schema["password"] = str return self.async_show_form( - step_id="init", - data_schema=vol.Schema(schema), - errors=errors, + step_id="init", data_schema=vol.Schema(schema), errors=errors ) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 2187d272800..df38810fc29 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -19,14 +19,13 @@ from ..models import Credentials, UserMeta STORAGE_VERSION = 1 -STORAGE_KEY = 'auth_provider.homeassistant' +STORAGE_KEY = "auth_provider.homeassistant" def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]: """Disallow ID in config.""" if CONF_ID in conf: - raise vol.Invalid( - 'ID is not allowed for the homeassistant auth provider.') + raise vol.Invalid("ID is not allowed for the homeassistant auth provider.") return conf @@ -51,8 +50,9 @@ class Data: def __init__(self, hass: HomeAssistant) -> None: """Initialize the user data store.""" self.hass = hass - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, - private=True) + self._store = hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY, private=True + ) self._data = None # type: Optional[Dict[str, Any]] # Legacy mode will allow usernames to start/end with whitespace # and will compare usernames case-insensitive. @@ -72,14 +72,12 @@ class Data: data = await self._store.async_load() if data is None: - data = { - 'users': [] - } + data = {"users": []} seen = set() # type: Set[str] - for user in data['users']: - username = user['username'] + for user in data["users"]: + username = user["username"] # check if we have duplicates folded = username.casefold() @@ -90,7 +88,9 @@ class Data: logging.getLogger(__name__).warning( "Home Assistant auth provider is running in legacy mode " "because we detected usernames that are case-insensitive" - "equivalent. Please change the username: '%s'.", username) + "equivalent. Please change the username: '%s'.", + username, + ) break @@ -103,7 +103,9 @@ class Data: logging.getLogger(__name__).warning( "Home Assistant auth provider is running in legacy mode " "because we detected usernames that start or end in a " - "space. Please change the username: '%s'.", username) + "space. Please change the username: '%s'.", + username, + ) break @@ -112,7 +114,7 @@ class Data: @property def users(self) -> List[Dict[str, str]]: """Return users.""" - return self._data['users'] # type: ignore + return self._data["users"] # type: ignore def validate_login(self, username: str, password: str) -> None: """Validate a username and password. @@ -120,32 +122,30 @@ class Data: Raises InvalidAuth if auth invalid. """ username = self.normalize_username(username) - dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO' + dummy = b"$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO" found = None # Compare all users to avoid timing attacks. for user in self.users: - if self.normalize_username(user['username']) == username: + if self.normalize_username(user["username"]) == username: found = user if found is None: # check a hash to make timing the same as if user was found - bcrypt.checkpw(b'foo', - dummy) + bcrypt.checkpw(b"foo", dummy) raise InvalidAuth - user_hash = base64.b64decode(found['password']) + user_hash = base64.b64decode(found["password"]) # bcrypt.checkpw is timing-safe - if not bcrypt.checkpw(password.encode(), - user_hash): + if not bcrypt.checkpw(password.encode(), user_hash): raise InvalidAuth # pylint: disable=no-self-use def hash_password(self, password: str, for_storage: bool = False) -> bytes: """Encode a password.""" - hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) \ - # type: bytes + hashed: bytes = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) + if for_storage: hashed = base64.b64encode(hashed) return hashed @@ -154,14 +154,17 @@ class Data: """Add a new authenticated user/pass.""" username = self.normalize_username(username) - if any(self.normalize_username(user['username']) == username - for user in self.users): + if any( + self.normalize_username(user["username"]) == username for user in self.users + ): raise InvalidUser - self.users.append({ - 'username': username, - 'password': self.hash_password(password, True).decode(), - }) + self.users.append( + { + "username": username, + "password": self.hash_password(password, True).decode(), + } + ) @callback def async_remove_auth(self, username: str) -> None: @@ -170,7 +173,7 @@ class Data: index = None for i, user in enumerate(self.users): - if self.normalize_username(user['username']) == username: + if self.normalize_username(user["username"]) == username: index = i break @@ -187,9 +190,8 @@ class Data: username = self.normalize_username(username) for user in self.users: - if self.normalize_username(user['username']) == username: - user['password'] = self.hash_password( - new_password, True).decode() + if self.normalize_username(user["username"]) == username: + user["password"] = self.hash_password(new_password, True).decode() break else: raise InvalidUser @@ -199,11 +201,11 @@ class Data: await self._store.async_save(self._data) -@AUTH_PROVIDERS.register('homeassistant') +@AUTH_PROVIDERS.register("homeassistant") class HassAuthProvider(AuthProvider): """Auth provider based on a local storage of users in HASS config dir.""" - DEFAULT_TITLE = 'Home Assistant Local' + DEFAULT_TITLE = "Home Assistant Local" def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize an Home Assistant auth provider.""" @@ -221,8 +223,7 @@ class HassAuthProvider(AuthProvider): await data.async_load() self.data = data - async def async_login_flow( - self, context: Optional[Dict]) -> LoginFlow: + async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow: """Return a flow to login.""" return HassLoginFlow(self) @@ -233,41 +234,41 @@ class HassAuthProvider(AuthProvider): assert self.data is not None await self.hass.async_add_executor_job( - self.data.validate_login, username, password) + self.data.validate_login, username, password + ) async def async_get_or_create_credentials( - self, flow_result: Dict[str, str]) -> Credentials: + self, flow_result: Dict[str, str] + ) -> Credentials: """Get credentials based on the flow result.""" if self.data is None: await self.async_initialize() assert self.data is not None norm_username = self.data.normalize_username - username = norm_username(flow_result['username']) + username = norm_username(flow_result["username"]) for credential in await self.async_credentials(): - if norm_username(credential.data['username']) == username: + if norm_username(credential.data["username"]) == username: return credential # Create new credentials. - return self.async_create_credentials({ - 'username': username - }) + return self.async_create_credentials({"username": username}) async def async_user_meta_for_credentials( - self, credentials: Credentials) -> UserMeta: + self, credentials: Credentials + ) -> UserMeta: """Get extra info for this credential.""" - return UserMeta(name=credentials.data['username'], is_active=True) + return UserMeta(name=credentials.data["username"], is_active=True) - async def async_will_remove_credentials( - self, credentials: Credentials) -> None: + async def async_will_remove_credentials(self, credentials: Credentials) -> None: """When credentials get removed, also remove the auth.""" if self.data is None: await self.async_initialize() assert self.data is not None try: - self.data.async_remove_auth(credentials.data['username']) + self.data.async_remove_auth(credentials.data["username"]) await self.data.async_save() except InvalidUser: # Can happen if somehow we didn't clean up a credential @@ -278,29 +279,27 @@ class HassLoginFlow(LoginFlow): """Handler for the login flow.""" async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None) \ - -> Dict[str, Any]: + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: """Handle the step of the form.""" errors = {} if user_input is not None: try: - await cast(HassAuthProvider, self._auth_provider)\ - .async_validate_login(user_input['username'], - user_input['password']) + await cast(HassAuthProvider, self._auth_provider).async_validate_login( + user_input["username"], user_input["password"] + ) except InvalidAuth: - errors['base'] = 'invalid_auth' + errors["base"] = "invalid_auth" if not errors: - user_input.pop('password') + user_input.pop("password") return await self.async_finish(user_input) schema = OrderedDict() # type: Dict[str, type] - schema['username'] = str - schema['password'] = str + schema["username"] = str + schema["password"] = str return self.async_show_form( - step_id='init', - data_schema=vol.Schema(schema), - errors=errors, + step_id="init", data_schema=vol.Schema(schema), errors=errors ) diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index 72e3dfe140a..35524c3f5fc 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -12,23 +12,25 @@ from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow from ..models import Credentials, UserMeta -USER_SCHEMA = vol.Schema({ - vol.Required('username'): str, - vol.Required('password'): str, - vol.Optional('name'): str, -}) +USER_SCHEMA = vol.Schema( + { + vol.Required("username"): str, + vol.Required("password"): str, + vol.Optional("name"): str, + } +) -CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ - vol.Required('users'): [USER_SCHEMA] -}, extra=vol.PREVENT_EXTRA) +CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( + {vol.Required("users"): [USER_SCHEMA]}, extra=vol.PREVENT_EXTRA +) class InvalidAuthError(HomeAssistantError): """Raised when submitting invalid authentication.""" -@AUTH_PROVIDERS.register('insecure_example') +@AUTH_PROVIDERS.register("insecure_example") class ExampleAuthProvider(AuthProvider): """Example auth provider based on hardcoded usernames and passwords.""" @@ -42,47 +44,48 @@ class ExampleAuthProvider(AuthProvider): user = None # Compare all users to avoid timing attacks. - for usr in self.config['users']: - if hmac.compare_digest(username.encode('utf-8'), - usr['username'].encode('utf-8')): + for usr in self.config["users"]: + if hmac.compare_digest( + username.encode("utf-8"), usr["username"].encode("utf-8") + ): user = usr if user is None: # Do one more compare to make timing the same as if user was found. - hmac.compare_digest(password.encode('utf-8'), - password.encode('utf-8')) + hmac.compare_digest(password.encode("utf-8"), password.encode("utf-8")) raise InvalidAuthError - if not hmac.compare_digest(user['password'].encode('utf-8'), - password.encode('utf-8')): + if not hmac.compare_digest( + user["password"].encode("utf-8"), password.encode("utf-8") + ): raise InvalidAuthError async def async_get_or_create_credentials( - self, flow_result: Dict[str, str]) -> Credentials: + self, flow_result: Dict[str, str] + ) -> Credentials: """Get credentials based on the flow result.""" - username = flow_result['username'] + username = flow_result["username"] for credential in await self.async_credentials(): - if credential.data['username'] == username: + if credential.data["username"] == username: return credential # Create new credentials. - return self.async_create_credentials({ - 'username': username - }) + return self.async_create_credentials({"username": username}) async def async_user_meta_for_credentials( - self, credentials: Credentials) -> UserMeta: + self, credentials: Credentials + ) -> UserMeta: """Return extra user metadata for credentials. Will be used to populate info when creating a new user. """ - username = credentials.data['username'] + username = credentials.data["username"] name = None - for user in self.config['users']: - if user['username'] == username: - name = user.get('name') + for user in self.config["users"]: + if user["username"] == username: + name = user.get("name") break return UserMeta(name=name, is_active=True) @@ -92,29 +95,27 @@ class ExampleLoginFlow(LoginFlow): """Handler for the login flow.""" async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None) \ - -> Dict[str, Any]: + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: """Handle the step of the form.""" errors = {} if user_input is not None: try: - cast(ExampleAuthProvider, self._auth_provider)\ - .async_validate_login(user_input['username'], - user_input['password']) + cast(ExampleAuthProvider, self._auth_provider).async_validate_login( + user_input["username"], user_input["password"] + ) except InvalidAuthError: - errors['base'] = 'invalid_auth' + errors["base"] = "invalid_auth" if not errors: - user_input.pop('password') + user_input.pop("password") return await self.async_finish(user_input) schema = OrderedDict() # type: Dict[str, type] - schema['username'] = str - schema['password'] = str + schema["username"] = str + schema["password"] = str return self.async_show_form( - step_id='init', - data_schema=vol.Schema(schema), - errors=errors, + step_id="init", data_schema=vol.Schema(schema), errors=errors ) diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index e85d831a325..018886388df 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -16,27 +16,26 @@ from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow from .. import AuthManager from ..models import Credentials, UserMeta, User -AUTH_PROVIDER_TYPE = 'legacy_api_password' -CONF_API_PASSWORD = 'api_password' +AUTH_PROVIDER_TYPE = "legacy_api_password" +CONF_API_PASSWORD = "api_password" -CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ - vol.Required(CONF_API_PASSWORD): cv.string, -}, extra=vol.PREVENT_EXTRA) +CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( + {vol.Required(CONF_API_PASSWORD): cv.string}, extra=vol.PREVENT_EXTRA +) -LEGACY_USER_NAME = 'Legacy API password user' +LEGACY_USER_NAME = "Legacy API password user" class InvalidAuthError(HomeAssistantError): """Raised when submitting invalid authentication.""" -async def async_validate_password(hass: HomeAssistant, password: str)\ - -> Optional[User]: +async def async_validate_password(hass: HomeAssistant, password: str) -> Optional[User]: """Return a user if password is valid. None if not.""" auth = cast(AuthManager, hass.auth) # type: ignore providers = auth.get_auth_providers(AUTH_PROVIDER_TYPE) if not providers: - raise ValueError('Legacy API password provider not found') + raise ValueError("Legacy API password provider not found") try: provider = cast(LegacyApiPasswordAuthProvider, providers[0]) @@ -52,7 +51,7 @@ async def async_validate_password(hass: HomeAssistant, password: str)\ class LegacyApiPasswordAuthProvider(AuthProvider): """An auth provider support legacy api_password.""" - DEFAULT_TITLE = 'Legacy API Password' + DEFAULT_TITLE = "Legacy API Password" @property def api_password(self) -> str: @@ -68,12 +67,14 @@ class LegacyApiPasswordAuthProvider(AuthProvider): """Validate password.""" api_password = str(self.config[CONF_API_PASSWORD]) - if not hmac.compare_digest(api_password.encode('utf-8'), - password.encode('utf-8')): + if not hmac.compare_digest( + api_password.encode("utf-8"), password.encode("utf-8") + ): raise InvalidAuthError async def async_get_or_create_credentials( - self, flow_result: Dict[str, str]) -> Credentials: + self, flow_result: Dict[str, str] + ) -> Credentials: """Return credentials for this login.""" credentials = await self.async_credentials() if credentials: @@ -82,7 +83,8 @@ class LegacyApiPasswordAuthProvider(AuthProvider): return self.async_create_credentials({}) async def async_user_meta_for_credentials( - self, credentials: Credentials) -> UserMeta: + self, credentials: Credentials + ) -> UserMeta: """ Return info for the user. @@ -95,23 +97,22 @@ class LegacyLoginFlow(LoginFlow): """Handler for the login flow.""" async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None) \ - -> Dict[str, Any]: + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: """Handle the step of the form.""" errors = {} if user_input is not None: try: - cast(LegacyApiPasswordAuthProvider, self._auth_provider)\ - .async_validate_login(user_input['password']) + cast( + LegacyApiPasswordAuthProvider, self._auth_provider + ).async_validate_login(user_input["password"]) except InvalidAuthError: - errors['base'] = 'invalid_auth' + errors["base"] = "invalid_auth" if not errors: return await self.async_finish({}) return self.async_show_form( - step_id='init', - data_schema=vol.Schema({'password': str}), - errors=errors, + step_id="init", data_schema=vol.Schema({"password": str}), errors=errors ) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index e8161a2bfb6..f71be436acf 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -3,8 +3,7 @@ It shows list of users if access from trusted network. Abort login flow if not access from trusted network. """ -from ipaddress import ip_network, IPv4Address, IPv6Address, IPv4Network,\ - IPv6Network +from ipaddress import ip_network, IPv4Address, IPv6Address, IPv4Network, IPv6Network from typing import Any, Dict, List, Optional, Union, cast import voluptuous as vol @@ -18,27 +17,32 @@ from ..models import Credentials, UserMeta IPAddress = Union[IPv4Address, IPv6Address] IPNetwork = Union[IPv4Network, IPv6Network] -CONF_TRUSTED_NETWORKS = 'trusted_networks' -CONF_TRUSTED_USERS = 'trusted_users' -CONF_GROUP = 'group' -CONF_ALLOW_BYPASS_LOGIN = 'allow_bypass_login' +CONF_TRUSTED_NETWORKS = "trusted_networks" +CONF_TRUSTED_USERS = "trusted_users" +CONF_GROUP = "group" +CONF_ALLOW_BYPASS_LOGIN = "allow_bypass_login" -CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ - vol.Required(CONF_TRUSTED_NETWORKS): vol.All( - cv.ensure_list, [ip_network] - ), - vol.Optional(CONF_TRUSTED_USERS, default={}): vol.Schema( - # we only validate the format of user_id or group_id - {ip_network: vol.All( - cv.ensure_list, - [vol.Or( - cv.uuid4_hex, - vol.Schema({vol.Required(CONF_GROUP): cv.uuid4_hex}), - )], - )} - ), - vol.Optional(CONF_ALLOW_BYPASS_LOGIN, default=False): cv.boolean, -}, extra=vol.PREVENT_EXTRA) +CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( + { + vol.Required(CONF_TRUSTED_NETWORKS): vol.All(cv.ensure_list, [ip_network]), + vol.Optional(CONF_TRUSTED_USERS, default={}): vol.Schema( + # we only validate the format of user_id or group_id + { + ip_network: vol.All( + cv.ensure_list, + [ + vol.Or( + cv.uuid4_hex, + vol.Schema({vol.Required(CONF_GROUP): cv.uuid4_hex}), + ) + ], + ) + } + ), + vol.Optional(CONF_ALLOW_BYPASS_LOGIN, default=False): cv.boolean, + }, + extra=vol.PREVENT_EXTRA, +) class InvalidAuthError(HomeAssistantError): @@ -49,14 +53,14 @@ class InvalidUserError(HomeAssistantError): """Raised when try to login as invalid user.""" -@AUTH_PROVIDERS.register('trusted_networks') +@AUTH_PROVIDERS.register("trusted_networks") class TrustedNetworksAuthProvider(AuthProvider): """Trusted Networks auth provider. Allow passwordless access from trusted network. """ - DEFAULT_TITLE = 'Trusted Networks' + DEFAULT_TITLE = "Trusted Networks" @property def trusted_networks(self) -> List[IPNetwork]: @@ -76,49 +80,58 @@ class TrustedNetworksAuthProvider(AuthProvider): async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow: """Return a flow to login.""" assert context is not None - ip_addr = cast(IPAddress, context.get('ip_address')) + ip_addr = cast(IPAddress, context.get("ip_address")) users = await self.store.async_get_users() - available_users = [user for user in users - if not user.system_generated and user.is_active] + available_users = [ + user for user in users if not user.system_generated and user.is_active + ] for ip_net, user_or_group_list in self.trusted_users.items(): if ip_addr in ip_net: - user_list = [user_id for user_id in user_or_group_list - if isinstance(user_id, str)] - group_list = [group[CONF_GROUP] for group in user_or_group_list - if isinstance(group, dict)] - flattened_group_list = [group for sublist in group_list - for group in sublist] + user_list = [ + user_id + for user_id in user_or_group_list + if isinstance(user_id, str) + ] + group_list = [ + group[CONF_GROUP] + for group in user_or_group_list + if isinstance(group, dict) + ] + flattened_group_list = [ + group for sublist in group_list for group in sublist + ] available_users = [ - user for user in available_users - if (user.id in user_list or - any([group.id in flattened_group_list - for group in user.groups])) + user + for user in available_users + if ( + user.id in user_list + or any( + [group.id in flattened_group_list for group in user.groups] + ) + ) ] break return TrustedNetworksLoginFlow( self, ip_addr, - { - user.id: user.name for user in available_users - }, + {user.id: user.name for user in available_users}, self.config[CONF_ALLOW_BYPASS_LOGIN], ) async def async_get_or_create_credentials( - self, flow_result: Dict[str, str]) -> Credentials: + self, flow_result: Dict[str, str] + ) -> Credentials: """Get credentials based on the flow result.""" - user_id = flow_result['user'] + user_id = flow_result["user"] users = await self.store.async_get_users() for user in users: - if (not user.system_generated and - user.is_active and - user.id == user_id): + if not user.system_generated and user.is_active and user.id == user_id: for credential in await self.async_credentials(): - if credential.data['user_id'] == user_id: + if credential.data["user_id"] == user_id: return credential - cred = self.async_create_credentials({'user_id': user_id}) + cred = self.async_create_credentials({"user_id": user_id}) await self.store.async_link_user(user, cred) return cred @@ -126,7 +139,8 @@ class TrustedNetworksAuthProvider(AuthProvider): raise InvalidUserError async def async_user_meta_for_credentials( - self, credentials: Credentials) -> UserMeta: + self, credentials: Credentials + ) -> UserMeta: """Return extra user metadata for credentials. Trusted network auth provider should never create new user. @@ -141,20 +155,24 @@ class TrustedNetworksAuthProvider(AuthProvider): Raise InvalidAuthError if trusted_networks is not configured. """ if not self.trusted_networks: - raise InvalidAuthError('trusted_networks is not configured') + raise InvalidAuthError("trusted_networks is not configured") - if not any(ip_addr in trusted_network for trusted_network - in self.trusted_networks): - raise InvalidAuthError('Not in trusted_networks') + if not any( + ip_addr in trusted_network for trusted_network in self.trusted_networks + ): + raise InvalidAuthError("Not in trusted_networks") class TrustedNetworksLoginFlow(LoginFlow): """Handler for the login flow.""" - def __init__(self, auth_provider: TrustedNetworksAuthProvider, - ip_addr: IPAddress, - available_users: Dict[str, Optional[str]], - allow_bypass_login: bool) -> None: + def __init__( + self, + auth_provider: TrustedNetworksAuthProvider, + ip_addr: IPAddress, + available_users: Dict[str, Optional[str]], + allow_bypass_login: bool, + ) -> None: """Initialize the login flow.""" super().__init__(auth_provider) self._available_users = available_users @@ -162,27 +180,26 @@ class TrustedNetworksLoginFlow(LoginFlow): self._allow_bypass_login = allow_bypass_login async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None) \ - -> Dict[str, Any]: + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: """Handle the step of the form.""" try: - cast(TrustedNetworksAuthProvider, self._auth_provider)\ - .async_validate_access(self._ip_address) + cast( + TrustedNetworksAuthProvider, self._auth_provider + ).async_validate_access(self._ip_address) except InvalidAuthError: - return self.async_abort( - reason='not_whitelisted' - ) + return self.async_abort(reason="not_whitelisted") if user_input is not None: return await self.async_finish(user_input) if self._allow_bypass_login and len(self._available_users) == 1: - return await self.async_finish({ - 'user': next(iter(self._available_users.keys())) - }) + return await self.async_finish( + {"user": next(iter(self._available_users.keys()))} + ) return self.async_show_form( - step_id='init', - data_schema=vol.Schema({'user': vol.In(self._available_users)}), + step_id="init", + data_schema=vol.Schema({"user": vol.In(self._available_users)}), ) diff --git a/homeassistant/auth/util.py b/homeassistant/auth/util.py index 402caae4618..83834fa7683 100644 --- a/homeassistant/auth/util.py +++ b/homeassistant/auth/util.py @@ -10,4 +10,4 @@ def generate_secret(entropy: int = 32) -> str: Event loop friendly. """ - return binascii.hexlify(os.urandom(entropy)).decode('ascii') + return binascii.hexlify(os.urandom(entropy)).decode("ascii") diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 79e5ec248ae..b0eab0da0f3 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -20,32 +20,33 @@ from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) -ERROR_LOG_FILENAME = 'home-assistant.log' +ERROR_LOG_FILENAME = "home-assistant.log" # hass.data key for logging information. -DATA_LOGGING = 'logging' +DATA_LOGGING = "logging" -DEBUGGER_INTEGRATIONS = {'ptvsd', } -CORE_INTEGRATIONS = ('homeassistant', 'persistent_notification') -LOGGING_INTEGRATIONS = {'logger', 'system_log'} +DEBUGGER_INTEGRATIONS = {"ptvsd"} +CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") +LOGGING_INTEGRATIONS = {"logger", "system_log"} STAGE_1_INTEGRATIONS = { # To record data - 'recorder', + "recorder", # To make sure we forward data to other instances - 'mqtt_eventstream', + "mqtt_eventstream", } -async def async_from_config_dict(config: Dict[str, Any], - hass: core.HomeAssistant, - config_dir: Optional[str] = None, - enable_log: bool = True, - verbose: bool = False, - skip_pip: bool = False, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False) \ - -> Optional[core.HomeAssistant]: +async def async_from_config_dict( + config: Dict[str, Any], + hass: core.HomeAssistant, + config_dir: Optional[str] = None, + enable_log: bool = True, + verbose: bool = False, + skip_pip: bool = False, + log_rotate_days: Any = None, + log_file: Any = None, + log_no_color: bool = False, +) -> Optional[core.HomeAssistant]: """Try to configure Home Assistant from a configuration dictionary. Dynamically loads required components and its dependencies. @@ -54,28 +55,30 @@ async def async_from_config_dict(config: Dict[str, Any], start = time() if enable_log: - async_enable_logging(hass, verbose, log_rotate_days, log_file, - log_no_color) + async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) hass.config.skip_pip = skip_pip if skip_pip: - _LOGGER.warning("Skipping pip installation of required modules. " - "This may cause issues") + _LOGGER.warning( + "Skipping pip installation of required modules. " "This may cause issues" + ) core_config = config.get(core.DOMAIN, {}) - api_password = config.get('http', {}).get('api_password') - trusted_networks = config.get('http', {}).get('trusted_networks') + api_password = config.get("http", {}).get("api_password") + trusted_networks = config.get("http", {}).get("trusted_networks") try: await conf_util.async_process_ha_core_config( - hass, core_config, api_password, trusted_networks) + hass, core_config, api_password, trusted_networks + ) except vol.Invalid as config_err: - conf_util.async_log_exception( - config_err, 'homeassistant', core_config, hass) + conf_util.async_log_exception(config_err, "homeassistant", core_config, hass) return None except HomeAssistantError: - _LOGGER.error("Home Assistant core failed to initialize. " - "Further initialization aborted") + _LOGGER.error( + "Home Assistant core failed to initialize. " + "Further initialization aborted" + ) return None # Make a copy because we are mutating it. @@ -83,7 +86,8 @@ async def async_from_config_dict(config: Dict[str, Any], # Merge packages await conf_util.merge_packages_config( - hass, config, core_config.get(conf_util.CONF_PACKAGES, {})) + hass, config, core_config.get(conf_util.CONF_PACKAGES, {}) + ) hass.config_entries = config_entries.ConfigEntries(hass, config) await hass.config_entries.async_initialize() @@ -91,26 +95,20 @@ async def async_from_config_dict(config: Dict[str, Any], await _async_set_up_integrations(hass, config) stop = time() - _LOGGER.info("Home Assistant initialized in %.2fs", stop-start) - - if sys.version_info[:3] < (3, 6, 0): - hass.components.persistent_notification.async_create( - "Python 3.5 support is deprecated and will " - "be removed in the first release after August 1. Please " - "upgrade Python.", "Python version", "python_version" - ) + _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) return hass -async def async_from_config_file(config_path: str, - hass: core.HomeAssistant, - verbose: bool = False, - skip_pip: bool = True, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False)\ - -> Optional[core.HomeAssistant]: +async def async_from_config_file( + config_path: str, + hass: core.HomeAssistant, + verbose: bool = False, + skip_pip: bool = True, + log_rotate_days: Any = None, + log_file: Any = None, + log_no_color: bool = False, +) -> Optional[core.HomeAssistant]: """Read the configuration file and try to start all the functionality. Will add functionality to 'hass' parameter. @@ -123,15 +121,14 @@ async def async_from_config_file(config_path: str, if not is_virtual_env(): await async_mount_local_lib_path(config_dir) - async_enable_logging(hass, verbose, log_rotate_days, log_file, - log_no_color) + async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) - await hass.async_add_executor_job( - conf_util.process_ha_config_upgrade, hass) + await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass) try: config_dict = await hass.async_add_executor_job( - conf_util.load_yaml_config_file, config_path) + conf_util.load_yaml_config_file, config_path + ) except HomeAssistantError as err: _LOGGER.error("Error loading %s: %s", config_path, err) return None @@ -139,43 +136,48 @@ async def async_from_config_file(config_path: str, clear_secret_cache() return await async_from_config_dict( - config_dict, hass, enable_log=False, skip_pip=skip_pip) + config_dict, hass, enable_log=False, skip_pip=skip_pip + ) @core.callback -def async_enable_logging(hass: core.HomeAssistant, - verbose: bool = False, - log_rotate_days: Optional[int] = None, - log_file: Optional[str] = None, - log_no_color: bool = False) -> None: +def async_enable_logging( + hass: core.HomeAssistant, + verbose: bool = False, + log_rotate_days: Optional[int] = None, + log_file: Optional[str] = None, + log_no_color: bool = False, +) -> None: """Set up the logging. This method must be run in the event loop. """ - fmt = ("%(asctime)s %(levelname)s (%(threadName)s) " - "[%(name)s] %(message)s") - datefmt = '%Y-%m-%d %H:%M:%S' + fmt = "%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s" + datefmt = "%Y-%m-%d %H:%M:%S" if not log_no_color: try: from colorlog import ColoredFormatter + # basicConfig must be called after importing colorlog in order to # ensure that the handlers it sets up wraps the correct streams. logging.basicConfig(level=logging.INFO) colorfmt = "%(log_color)s{}%(reset)s".format(fmt) - logging.getLogger().handlers[0].setFormatter(ColoredFormatter( - colorfmt, - datefmt=datefmt, - reset=True, - log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'red', - } - )) + logging.getLogger().handlers[0].setFormatter( + ColoredFormatter( + colorfmt, + datefmt=datefmt, + reset=True, + log_colors={ + "DEBUG": "cyan", + "INFO": "green", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red", + }, + ) + ) except ImportError: pass @@ -184,9 +186,9 @@ def async_enable_logging(hass: core.HomeAssistant, logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO) # Suppress overly verbose logs from libraries that aren't helpful - logging.getLogger('requests').setLevel(logging.WARNING) - logging.getLogger('urllib3').setLevel(logging.WARNING) - logging.getLogger('aiohttp.access').setLevel(logging.WARNING) + logging.getLogger("requests").setLevel(logging.WARNING) + logging.getLogger("urllib3").setLevel(logging.WARNING) + logging.getLogger("aiohttp.access").setLevel(logging.WARNING) # Log errors to a file if we have write access to file or config dir if log_file is None: @@ -199,16 +201,16 @@ def async_enable_logging(hass: core.HomeAssistant, # Check if we can write to the error log if it exists or that # we can create files in the containing directory if not. - if (err_path_exists and os.access(err_log_path, os.W_OK)) or \ - (not err_path_exists and os.access(err_dir, os.W_OK)): + if (err_path_exists and os.access(err_log_path, os.W_OK)) or ( + not err_path_exists and os.access(err_dir, os.W_OK) + ): if log_rotate_days: err_handler = logging.handlers.TimedRotatingFileHandler( - err_log_path, when='midnight', - backupCount=log_rotate_days) # type: logging.FileHandler + err_log_path, when="midnight", backupCount=log_rotate_days + ) # type: logging.FileHandler else: - err_handler = logging.FileHandler( - err_log_path, mode='w', delay=True) + err_handler = logging.FileHandler(err_log_path, mode="w", delay=True) err_handler.setLevel(logging.INFO if verbose else logging.WARNING) err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt)) @@ -217,21 +219,19 @@ def async_enable_logging(hass: core.HomeAssistant, async def async_stop_async_handler(_: Any) -> None: """Cleanup async handler.""" - logging.getLogger('').removeHandler(async_handler) # type: ignore + logging.getLogger("").removeHandler(async_handler) # type: ignore await async_handler.async_close(blocking=True) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) - logger = logging.getLogger('') + logger = logging.getLogger("") logger.addHandler(async_handler) # type: ignore logger.setLevel(logging.INFO) # Save the log file location for access by other components. hass.data[DATA_LOGGING] = err_log_path else: - _LOGGER.error( - "Unable to set up error log %s (access denied)", err_log_path) + _LOGGER.error("Unable to set up error log %s (access denied)", err_log_path) async def async_mount_local_lib_path(config_dir: str) -> str: @@ -239,7 +239,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str: This function is a coroutine. """ - deps_dir = os.path.join(config_dir, 'deps') + deps_dir = os.path.join(config_dir, "deps") lib_dir = await async_get_user_site(deps_dir) if lib_dir not in sys.path: sys.path.insert(0, lib_dir) @@ -250,21 +250,21 @@ async def async_mount_local_lib_path(config_dir: str) -> str: def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]: """Get domains of components to set up.""" # Filter out the repeating and common config section [homeassistant] - domains = set(key.split(' ')[0] for key in config.keys() - if key != core.DOMAIN) + domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN) # Add config entry domains domains.update(hass.config_entries.async_domains()) # type: ignore # Make sure the Hass.io component is loaded - if 'HASSIO' in os.environ: - domains.add('hassio') + if "HASSIO" in os.environ: + domains.add("hassio") return domains async def _async_set_up_integrations( - hass: core.HomeAssistant, config: Dict[str, Any]) -> None: + hass: core.HomeAssistant, config: Dict[str, Any] +) -> None: """Set up all the integrations.""" domains = _get_domains(hass, config) @@ -272,27 +272,33 @@ async def _async_set_up_integrations( debuggers = domains & DEBUGGER_INTEGRATIONS if debuggers: _LOGGER.debug("Starting up debuggers %s", debuggers) - await asyncio.gather(*[ - async_setup_component(hass, domain, config) - for domain in debuggers]) + await asyncio.gather( + *(async_setup_component(hass, domain, config) for domain in debuggers) + ) domains -= DEBUGGER_INTEGRATIONS # Resolve all dependencies of all components so we can find the logging # and integrations that need faster initialization. - resolved_domains_task = asyncio.gather(*[ - loader.async_component_dependencies(hass, domain) - for domain in domains - ], return_exceptions=True) + resolved_domains_task = asyncio.gather( + *(loader.async_component_dependencies(hass, domain) for domain in domains), + return_exceptions=True, + ) # Set up core. _LOGGER.debug("Setting up %s", CORE_INTEGRATIONS) - if not all(await asyncio.gather(*[ - async_setup_component(hass, domain, config) - for domain in CORE_INTEGRATIONS - ])): - _LOGGER.error("Home Assistant core failed to initialize. " - "Further initialization aborted") + if not all( + await asyncio.gather( + *( + async_setup_component(hass, domain, config) + for domain in CORE_INTEGRATIONS + ) + ) + ): + _LOGGER.error( + "Home Assistant core failed to initialize. " + "Further initialization aborted" + ) return _LOGGER.debug("Home Assistant core initialized") @@ -312,36 +318,32 @@ async def _async_set_up_integrations( if logging_domains: _LOGGER.info("Setting up %s", logging_domains) - await asyncio.gather(*[ - async_setup_component(hass, domain, config) - for domain in logging_domains - ]) + await asyncio.gather( + *(async_setup_component(hass, domain, config) for domain in logging_domains) + ) # Kick off loading the registries. They don't need to be awaited. asyncio.gather( hass.helpers.device_registry.async_get_registry(), hass.helpers.entity_registry.async_get_registry(), - hass.helpers.area_registry.async_get_registry()) + hass.helpers.area_registry.async_get_registry(), + ) if stage_1_domains: - await asyncio.gather(*[ - async_setup_component(hass, domain, config) - for domain in stage_1_domains - ]) + await asyncio.gather( + *(async_setup_component(hass, domain, config) for domain in stage_1_domains) + ) # Load all integrations after_dependencies = {} # type: Dict[str, Set[str]] - for int_or_exc in await asyncio.gather(*[ - loader.async_get_integration(hass, domain) - for domain in stage_2_domains - ], return_exceptions=True): + for int_or_exc in await asyncio.gather( + *(loader.async_get_integration(hass, domain) for domain in stage_2_domains), + return_exceptions=True, + ): # Exceptions are handled in async_setup_component. - if (isinstance(int_or_exc, loader.Integration) and - int_or_exc.after_dependencies): - after_dependencies[int_or_exc.domain] = set( - int_or_exc.after_dependencies - ) + if isinstance(int_or_exc, loader.Integration) and int_or_exc.after_dependencies: + after_dependencies[int_or_exc.domain] = set(int_or_exc.after_dependencies) last_load = None while stage_2_domains: @@ -351,8 +353,7 @@ async def _async_set_up_integrations( after_deps = after_dependencies.get(domain) # Load if integration has no after_dependencies or they are # all loaded - if (not after_deps or - not after_deps-hass.config.components): + if not after_deps or not after_deps - hass.config.components: domains_to_load.add(domain) if not domains_to_load or domains_to_load == last_load: @@ -360,10 +361,9 @@ async def _async_set_up_integrations( _LOGGER.debug("Setting up %s", domains_to_load) - await asyncio.gather(*[ - async_setup_component(hass, domain, config) - for domain in domains_to_load - ]) + await asyncio.gather( + *(async_setup_component(hass, domain, config) for domain in domains_to_load) + ) last_load = domains_to_load stage_2_domains -= domains_to_load @@ -373,10 +373,9 @@ async def _async_set_up_integrations( if stage_2_domains: _LOGGER.debug("Final set up: %s", stage_2_domains) - await asyncio.gather(*[ - async_setup_component(hass, domain, config) - for domain in stage_2_domains - ]) + await asyncio.gather( + *(async_setup_component(hass, domain, config) for domain in stage_2_domains) + ) # Wrap up startup await hass.async_block_till_done() diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 2a95b2b9116..8c14276ccc9 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -31,11 +31,10 @@ def is_on(hass, entity_id=None): component = getattr(hass.components, domain) except ImportError: - _LOGGER.error('Failed to call %s.is_on: component not found', - domain) + _LOGGER.error("Failed to call %s.is_on: component not found", domain) continue - if not hasattr(component, 'is_on'): + if not hasattr(component, "is_on"): _LOGGER.warning("Integration %s has no is_on method.", domain) continue diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 3a64a5e31f0..f43cbc50f98 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -6,9 +6,18 @@ from requests.exceptions import HTTPError, ConnectTimeout import voluptuous as vol from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME, - CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS, - EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) + ATTR_ATTRIBUTION, + ATTR_DATE, + ATTR_TIME, + ATTR_ENTITY_ID, + CONF_USERNAME, + CONF_PASSWORD, + CONF_EXCLUDE, + CONF_NAME, + CONF_LIGHTS, + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_START, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity @@ -17,77 +26,88 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Data provided by goabode.com" -CONF_POLLING = 'polling' +CONF_POLLING = "polling" -DOMAIN = 'abode' -DEFAULT_CACHEDB = './abodepy_cache.pickle' +DOMAIN = "abode" +DEFAULT_CACHEDB = "./abodepy_cache.pickle" -NOTIFICATION_ID = 'abode_notification' -NOTIFICATION_TITLE = 'Abode Security Setup' +NOTIFICATION_ID = "abode_notification" +NOTIFICATION_TITLE = "Abode Security Setup" -EVENT_ABODE_ALARM = 'abode_alarm' -EVENT_ABODE_ALARM_END = 'abode_alarm_end' -EVENT_ABODE_AUTOMATION = 'abode_automation' -EVENT_ABODE_FAULT = 'abode_panel_fault' -EVENT_ABODE_RESTORE = 'abode_panel_restore' +EVENT_ABODE_ALARM = "abode_alarm" +EVENT_ABODE_ALARM_END = "abode_alarm_end" +EVENT_ABODE_AUTOMATION = "abode_automation" +EVENT_ABODE_FAULT = "abode_panel_fault" +EVENT_ABODE_RESTORE = "abode_panel_restore" -SERVICE_SETTINGS = 'change_setting' -SERVICE_CAPTURE_IMAGE = 'capture_image' -SERVICE_TRIGGER = 'trigger_quick_action' +SERVICE_SETTINGS = "change_setting" +SERVICE_CAPTURE_IMAGE = "capture_image" +SERVICE_TRIGGER = "trigger_quick_action" -ATTR_DEVICE_ID = 'device_id' -ATTR_DEVICE_NAME = 'device_name' -ATTR_DEVICE_TYPE = 'device_type' -ATTR_EVENT_CODE = 'event_code' -ATTR_EVENT_NAME = 'event_name' -ATTR_EVENT_TYPE = 'event_type' -ATTR_EVENT_UTC = 'event_utc' -ATTR_SETTING = 'setting' -ATTR_USER_NAME = 'user_name' -ATTR_VALUE = 'value' +ATTR_DEVICE_ID = "device_id" +ATTR_DEVICE_NAME = "device_name" +ATTR_DEVICE_TYPE = "device_type" +ATTR_EVENT_CODE = "event_code" +ATTR_EVENT_NAME = "event_name" +ATTR_EVENT_TYPE = "event_type" +ATTR_EVENT_UTC = "event_utc" +ATTR_SETTING = "setting" +ATTR_USER_NAME = "user_name" +ATTR_VALUE = "value" ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str]) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_POLLING, default=False): cv.boolean, - vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, - vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_POLLING, default=False): cv.boolean, + vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, + vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -CHANGE_SETTING_SCHEMA = vol.Schema({ - vol.Required(ATTR_SETTING): cv.string, - vol.Required(ATTR_VALUE): cv.string -}) +CHANGE_SETTING_SCHEMA = vol.Schema( + {vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string} +) -CAPTURE_IMAGE_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, -}) +CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) -TRIGGER_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, -}) +TRIGGER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) ABODE_PLATFORMS = [ - 'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover', - 'camera', 'light', 'sensor' + "alarm_control_panel", + "binary_sensor", + "lock", + "switch", + "cover", + "camera", + "light", + "sensor", ] class AbodeSystem: """Abode System class.""" - def __init__(self, username, password, cache, - name, polling, exclude, lights): + def __init__(self, username, password, cache, name, polling, exclude, lights): """Initialize the system.""" import abodepy + self.abode = abodepy.Abode( - username, password, auto_login=True, get_devices=True, - get_automations=True, cache_path=cache) + username, + password, + auto_login=True, + get_devices=True, + get_automations=True, + cache_path=cache, + ) self.name = name self.polling = polling self.exclude = exclude @@ -106,9 +126,9 @@ class AbodeSystem: """Check if a switch device is configured as a light.""" import abodepy.helpers.constants as CONST - return (device.generic_type == CONST.TYPE_LIGHT or - (device.generic_type == CONST.TYPE_SWITCH and - device.device_id in self.lights)) + return device.generic_type == CONST.TYPE_LIGHT or ( + device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights + ) def setup(hass, config): @@ -126,16 +146,18 @@ def setup(hass, config): try: cache = hass.config.path(DEFAULT_CACHEDB) hass.data[DOMAIN] = AbodeSystem( - username, password, cache, name, polling, exclude, lights) + username, password, cache, name, polling, exclude, lights + ) except (AbodeException, ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Abode: %s", str(ex)) hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), + "Error: {}
" + "You will need to restart hass after fixing." + "".format(ex), title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) + notification_id=NOTIFICATION_ID, + ) return False setup_hass_services(hass) @@ -166,8 +188,11 @@ def setup_hass_services(hass): """Capture a new image.""" entity_ids = call.data.get(ATTR_ENTITY_ID) - target_devices = [device for device in hass.data[DOMAIN].devices - if device.entity_id in entity_ids] + target_devices = [ + device + for device in hass.data[DOMAIN].devices + if device.entity_id in entity_ids + ] for device in target_devices: device.capture() @@ -176,27 +201,31 @@ def setup_hass_services(hass): """Trigger a quick action.""" entity_ids = call.data.get(ATTR_ENTITY_ID, None) - target_devices = [device for device in hass.data[DOMAIN].devices - if device.entity_id in entity_ids] + target_devices = [ + device + for device in hass.data[DOMAIN].devices + if device.entity_id in entity_ids + ] for device in target_devices: device.trigger() hass.services.register( - DOMAIN, SERVICE_SETTINGS, change_setting, - schema=CHANGE_SETTING_SCHEMA) + DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA + ) hass.services.register( - DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, - schema=CAPTURE_IMAGE_SCHEMA) + DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA + ) hass.services.register( - DOMAIN, SERVICE_TRIGGER, trigger_quick_action, - schema=TRIGGER_SCHEMA) + DOMAIN, SERVICE_TRIGGER, trigger_quick_action, schema=TRIGGER_SCHEMA + ) def setup_hass_events(hass): """Home Assistant start and stop callbacks.""" + def startup(event): """Listen for push events.""" hass.data[DOMAIN].abode.events.start() @@ -222,28 +251,32 @@ def setup_abode_events(hass): def event_callback(event, event_json): """Handle an event callback from Abode.""" data = { - ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ''), - ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ''), - ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ''), - ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ''), - ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ''), - ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ''), - ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ''), - ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ''), - ATTR_DATE: event_json.get(ATTR_DATE, ''), - ATTR_TIME: event_json.get(ATTR_TIME, ''), + ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ""), + ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ""), + ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ""), + ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ""), + ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ""), + ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ""), + ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ""), + ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ""), + ATTR_DATE: event_json.get(ATTR_DATE, ""), + ATTR_TIME: event_json.get(ATTR_TIME, ""), } hass.bus.fire(event, data) - events = [TIMELINE.ALARM_GROUP, TIMELINE.ALARM_END_GROUP, - TIMELINE.PANEL_FAULT_GROUP, TIMELINE.PANEL_RESTORE_GROUP, - TIMELINE.AUTOMATION_GROUP] + events = [ + TIMELINE.ALARM_GROUP, + TIMELINE.ALARM_END_GROUP, + TIMELINE.PANEL_FAULT_GROUP, + TIMELINE.PANEL_RESTORE_GROUP, + TIMELINE.AUTOMATION_GROUP, + ] for event in events: hass.data[DOMAIN].abode.events.add_event_callback( - event, - partial(event_callback, event)) + event, partial(event_callback, event) + ) class AbodeDevice(Entity): @@ -258,7 +291,8 @@ class AbodeDevice(Entity): """Subscribe Abode events.""" self.hass.async_add_job( self._data.abode.events.add_device_callback, - self._device.device_id, self._update_callback + self._device.device_id, + self._update_callback, ) @property @@ -280,10 +314,10 @@ class AbodeDevice(Entity): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - 'device_id': self._device.device_id, - 'battery_low': self._device.battery_low, - 'no_response': self._device.no_response, - 'device_type': self._device.type + "device_id": self._device.device_id, + "battery_low": self._device.battery_low, + "no_response": self._device.no_response, + "device_type": self._device.type, } def _update_callback(self, device): @@ -305,7 +339,8 @@ class AbodeAutomation(Entity): if self._event: self.hass.async_add_job( self._data.abode.events.add_event_callback, - self._event, self._update_callback + self._event, + self._update_callback, ) @property @@ -327,9 +362,9 @@ class AbodeAutomation(Entity): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - 'automation_id': self._automation.automation_id, - 'type': self._automation.type, - 'sub_type': self._automation.sub_type + "automation_id": self._automation.automation_id, + "type": self._automation.type, + "sub_type": self._automation.sub_type, } def _update_callback(self, device): diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index d1d75b7417e..c5c10e65302 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -3,14 +3,17 @@ import logging import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( - ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED) + ATTR_ATTRIBUTION, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, +) from . import ATTRIBUTION, DOMAIN as ABODE_DOMAIN, AbodeDevice _LOGGER = logging.getLogger(__name__) -ICON = 'mdi:security' +ICON = "mdi:security" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -72,7 +75,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - 'device_id': self._device.device_id, - 'battery_backup': self._device.battery, - 'cellular_backup': self._device.is_cellular, + "device_id": self._device.device_id, + "battery_backup": self._device.battery, + "cellular_backup": self._device.is_cellular, } diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index e3f74e9f4ec..e37f6a465a4 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -15,9 +15,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = hass.data[ABODE_DOMAIN] - device_types = [CONST.TYPE_CONNECTIVITY, CONST.TYPE_MOISTURE, - CONST.TYPE_MOTION, CONST.TYPE_OCCUPANCY, - CONST.TYPE_OPENING] + device_types = [ + CONST.TYPE_CONNECTIVITY, + CONST.TYPE_MOISTURE, + CONST.TYPE_MOTION, + CONST.TYPE_OCCUPANCY, + CONST.TYPE_OPENING, + ] devices = [] for device in data.abode.get_devices(generic_type=device_types): @@ -26,13 +30,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices.append(AbodeBinarySensor(data, device)) - for automation in data.abode.get_automations( - generic_type=CONST.TYPE_QUICK_ACTION): + for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION): if data.is_automation_excluded(automation): continue - devices.append(AbodeQuickActionBinarySensor( - data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)) + devices.append( + AbodeQuickActionBinarySensor( + data, automation, TIMELINE.AUTOMATION_EDIT_GROUP + ) + ) data.devices.extend(devices) diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index d0e4e833029..95755a644e2 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -49,7 +49,8 @@ class AbodeCamera(AbodeDevice, Camera): self.hass.async_add_job( self._data.abode.events.add_timeline_callback, - self._event, self._capture_callback + self._event, + self._capture_callback, ) def capture(self): @@ -66,8 +67,7 @@ class AbodeCamera(AbodeDevice, Camera): """Attempt to download the most recent capture.""" if self._device.image_url: try: - self._response = requests.get( - self._device.image_url, stream=True) + self._response = requests.get(self._device.image_url, stream=True) self._response.raise_for_status() except requests.HTTPError as err: diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 6b3e5025c51..8e6691560e5 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -3,10 +3,18 @@ import logging from math import ceil from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + Light, +) from homeassistant.util.color import ( - color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin) + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) from . import DOMAIN as ABODE_DOMAIN, AbodeDevice @@ -42,8 +50,8 @@ class AbodeLight(AbodeDevice, Light): """Turn on the light.""" if ATTR_COLOR_TEMP in kwargs and self._device.is_color_capable: self._device.set_color_temp( - int(color_temperature_mired_to_kelvin( - kwargs[ATTR_COLOR_TEMP]))) + int(color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])) + ) if ATTR_HS_COLOR in kwargs and self._device.is_color_capable: self._device.set_color(kwargs[ATTR_HS_COLOR]) diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index b7e8fc1a118..ba28eab79c7 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -2,7 +2,10 @@ import logging from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, +) from . import DOMAIN as ABODE_DOMAIN, AbodeDevice @@ -10,9 +13,9 @@ _LOGGER = logging.getLogger(__name__) # Sensor types: Name, icon SENSOR_TYPES = { - 'temp': ['Temperature', DEVICE_CLASS_TEMPERATURE], - 'humidity': ['Humidity', DEVICE_CLASS_HUMIDITY], - 'lux': ['Lux', DEVICE_CLASS_ILLUMINANCE], + "temp": ["Temperature", DEVICE_CLASS_TEMPERATURE], + "humidity": ["Humidity", DEVICE_CLASS_HUMIDITY], + "lux": ["Lux", DEVICE_CLASS_ILLUMINANCE], } @@ -42,8 +45,9 @@ class AbodeSensor(AbodeDevice): """Initialize a sensor for an Abode device.""" super().__init__(data, device) self._sensor_type = sensor_type - self._name = '{0} {1}'.format( - self._device.name, SENSOR_TYPES[self._sensor_type][0]) + self._name = "{0} {1}".format( + self._device.name, SENSOR_TYPES[self._sensor_type][0] + ) self._device_class = SENSOR_TYPES[self._sensor_type][1] @property @@ -59,19 +63,19 @@ class AbodeSensor(AbodeDevice): @property def state(self): """Return the state of the sensor.""" - if self._sensor_type == 'temp': + if self._sensor_type == "temp": return self._device.temp - if self._sensor_type == 'humidity': + if self._sensor_type == "humidity": return self._device.humidity - if self._sensor_type == 'lux': + if self._sensor_type == "lux": return self._device.lux @property def unit_of_measurement(self): """Return the units of measurement.""" - if self._sensor_type == 'temp': + if self._sensor_type == "temp": return self._device.temp_unit - if self._sensor_type == 'humidity': + if self._sensor_type == "humidity": return self._device.humidity_unit - if self._sensor_type == 'lux': + if self._sensor_type == "lux": return self._device.lux_unit diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 74d1ea57bad..82a550df1a5 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -25,13 +25,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices.append(AbodeSwitch(data, device)) # Get all Abode automations that can be enabled/disabled - for automation in data.abode.get_automations( - generic_type=CONST.TYPE_AUTOMATION): + for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION): if data.is_automation_excluded(automation): continue - devices.append(AbodeAutomationSwitch( - data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)) + devices.append( + AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP) + ) data.devices.extend(devices) diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 242f3f4a009..558cf84d0e1 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -4,50 +4,58 @@ import re import voluptuous as vol -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA from homeassistant.const import ( - STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_NAME, CONF_FILENAME) + STATE_ON, + STATE_OFF, + STATE_UNKNOWN, + CONF_NAME, + CONF_FILENAME, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_TIMEOUT = 'timeout' -CONF_WRITE_TIMEOUT = 'write_timeout' +CONF_TIMEOUT = "timeout" +CONF_WRITE_TIMEOUT = "write_timeout" -DEFAULT_NAME = 'Acer Projector' +DEFAULT_NAME = "Acer Projector" DEFAULT_TIMEOUT = 1 DEFAULT_WRITE_TIMEOUT = 1 -ECO_MODE = 'ECO Mode' +ECO_MODE = "ECO Mode" -ICON = 'mdi:projector' +ICON = "mdi:projector" -INPUT_SOURCE = 'Input Source' +INPUT_SOURCE = "Input Source" -LAMP = 'Lamp' -LAMP_HOURS = 'Lamp Hours' +LAMP = "Lamp" +LAMP_HOURS = "Lamp Hours" -MODEL = 'Model' +MODEL = "Model" # Commands known to the projector CMD_DICT = { - LAMP: '* 0 Lamp ?\r', - LAMP_HOURS: '* 0 Lamp\r', - INPUT_SOURCE: '* 0 Src ?\r', - ECO_MODE: '* 0 IR 052\r', - MODEL: '* 0 IR 035\r', - STATE_ON: '* 0 IR 001\r', - STATE_OFF: '* 0 IR 002\r', + LAMP: "* 0 Lamp ?\r", + LAMP_HOURS: "* 0 Lamp\r", + INPUT_SOURCE: "* 0 Src ?\r", + ECO_MODE: "* 0 IR 052\r", + MODEL: "* 0 IR 035\r", + STATE_ON: "* 0 IR 001\r", + STATE_OFF: "* 0 IR 002\r", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_FILENAME): cv.isdevice, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT): - cv.positive_int, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_FILENAME): cv.isdevice, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional( + CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT + ): cv.positive_int, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -66,9 +74,10 @@ class AcerSwitch(SwitchDevice): def __init__(self, serial_port, name, timeout, write_timeout, **kwargs): """Init of the Acer projector.""" import serial + self.ser = serial.Serial( - port=serial_port, timeout=timeout, write_timeout=write_timeout, - **kwargs) + port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs + ) self._serial_port = serial_port self._name = name self._state = False @@ -82,6 +91,7 @@ class AcerSwitch(SwitchDevice): def _write_read(self, msg): """Write to the projector and read the return.""" import serial + ret = "" # Sometimes the projector won't answer for no reason or the projector # was disconnected during runtime. @@ -89,14 +99,14 @@ class AcerSwitch(SwitchDevice): try: if not self.ser.is_open: self.ser.open() - msg = msg.encode('utf-8') + msg = msg.encode("utf-8") self.ser.write(msg) # Size is an experience value there is no real limit. # AFAIK there is no limit and no end character so we will usually # need to wait for timeout - ret = self.ser.read_until(size=20).decode('utf-8') + ret = self.ser.read_until(size=20).decode("utf-8") except serial.SerialException: - _LOGGER.error('Problem communicating with %s', self._serial_port) + _LOGGER.error("Problem communicating with %s", self._serial_port) self.ser.close() return ret @@ -104,7 +114,7 @@ class AcerSwitch(SwitchDevice): """Write msg, obtain answer and format output.""" # answers are formatted as ***\answer\r*** awns = self._write_read(msg) - match = re.search(r'\r(.+)\r', awns) + match = re.search(r"\r(.+)\r", awns) if match: return match.group(1) return STATE_UNKNOWN @@ -133,10 +143,10 @@ class AcerSwitch(SwitchDevice): """Get the latest state from the projector.""" msg = CMD_DICT[LAMP] awns = self._write_read_format(msg) - if awns == 'Lamp 1': + if awns == "Lamp 1": self._state = True self._available = True - elif awns == 'Lamp 0': + elif awns == "Lamp 0": self._state = False self._available = True else: diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index 3f0c8786794..e07dd2622be 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -8,22 +8,28 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME _LOGGER = logging.getLogger(__name__) _LEASES_REGEX = re.compile( - r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})' + - r'\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' + - r'\svalid\sfor:\s(?P(-?\d+))' + - r'\ssec') + r"(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})" + + r"\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))" + + r"\svalid\sfor:\s(?P(-?\d+))" + + r"\ssec" +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + } +) def get_scanner(hass, config): @@ -32,7 +38,7 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -Device = namedtuple('Device', ['mac', 'ip', 'last_update']) +Device = namedtuple("Device", ["mac", "ip", "last_update"]) class ActiontecDeviceScanner(DeviceScanner): @@ -75,9 +81,11 @@ class ActiontecDeviceScanner(DeviceScanner): actiontec_data = self.get_actiontec_data() if not actiontec_data: return False - self.last_results = [Device(data['mac'], name, now) - for name, data in actiontec_data.items() - if data['timevalid'] > -60] + self.last_results = [ + Device(data["mac"], name, now) + for name, data in actiontec_data.items() + if data["timevalid"] > -60 + ] _LOGGER.info("Scan successful") return True @@ -85,17 +93,16 @@ class ActiontecDeviceScanner(DeviceScanner): """Retrieve data from Actiontec MI424WR and return parsed result.""" try: telnet = telnetlib.Telnet(self.host) - telnet.read_until(b'Username: ') - telnet.write((self.username + '\n').encode('ascii')) - telnet.read_until(b'Password: ') - telnet.write((self.password + '\n').encode('ascii')) - prompt = telnet.read_until( - b'Wireless Broadband Router> ').split(b'\n')[-1] - telnet.write('firewall mac_cache_dump\n'.encode('ascii')) - telnet.write('\n'.encode('ascii')) + telnet.read_until(b"Username: ") + telnet.write((self.username + "\n").encode("ascii")) + telnet.read_until(b"Password: ") + telnet.write((self.password + "\n").encode("ascii")) + prompt = telnet.read_until(b"Wireless Broadband Router> ").split(b"\n")[-1] + telnet.write("firewall mac_cache_dump\n".encode("ascii")) + telnet.write("\n".encode("ascii")) telnet.read_until(prompt) - leases_result = telnet.read_until(prompt).split(b'\n')[1:-1] - telnet.write('exit\n'.encode('ascii')) + leases_result = telnet.read_until(prompt).split(b"\n")[1:-1] + telnet.write("exit\n".encode("ascii")) except EOFError: _LOGGER.exception("Unexpected response from router") return @@ -105,11 +112,11 @@ class ActiontecDeviceScanner(DeviceScanner): devices = {} for lease in leases_result: - match = _LEASES_REGEX.search(lease.decode('utf-8')) + match = _LEASES_REGEX.search(lease.decode("utf-8")) if match is not None: - devices[match.group('ip')] = { - 'ip': match.group('ip'), - 'mac': match.group('mac').upper(), - 'timevalid': int(match.group('timevalid')) - } + devices[match.group("ip")] = { + "ip": match.group("ip"), + "mac": match.group("mac").upper(), + "timevalid": int(match.group("timevalid")), + } return devices diff --git a/homeassistant/components/adguard/.translations/bg.json b/homeassistant/components/adguard/.translations/bg.json new file mode 100644 index 00000000000..826244544b5 --- /dev/null +++ b/homeassistant/components/adguard/.translations/bg.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.", + "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home." + }, + "error": { + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435." + }, + "step": { + "hassio_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 AdGuard Home, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430: {addon} ?", + "title": "AdGuard Home \u0447\u0440\u0435\u0437 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430" + }, + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "verify_ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0430\u0434\u0435\u0436\u0434\u0435\u043d \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0412\u0430\u0448\u0438\u044f AdGuard Home, \u0437\u0430 \u0434\u0430 \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0435 \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u0435 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b.", + "title": "\u0421\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0412\u0430\u0448\u0438\u044f AdGuard Home." + } + }, + "title": "AdGuard Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/da.json b/homeassistant/components/adguard/.translations/da.json new file mode 100644 index 00000000000..0f854db0be6 --- /dev/null +++ b/homeassistant/components/adguard/.translations/da.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "Opdaterede eksisterende konfiguration.", + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af AdGuard Home." + }, + "error": { + "connection_error": "Forbindelse mislykkedes." + }, + "step": { + "hassio_confirm": { + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Adguard Home, der leveres af Hass.io add-on: {addon}?", + "title": "AdGuard Home via Hass.io add-on" + }, + "user": { + "data": { + "host": "V\u00e6rt", + "password": "Adgangskode", + "port": "Port", + "ssl": "AdGuard Home bruger et SSL-certifikat", + "username": "Brugernavn", + "verify_ssl": "AdGuard Home bruger et korrekt certifikat" + }, + "description": "Konfigurer din AdGuard Home instans for at tillade overv\u00e5gning og kontrol.", + "title": "Link AdGuard Home." + } + }, + "title": "AdGuard Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/de.json b/homeassistant/components/adguard/.translations/de.json index dd385adbab4..b1fbbfa85ab 100644 --- a/homeassistant/components/adguard/.translations/de.json +++ b/homeassistant/components/adguard/.translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.", "single_instance_allowed": "Es ist nur eine einzige Konfiguration von AdGuard Home zul\u00e4ssig." }, "error": { diff --git a/homeassistant/components/adguard/.translations/es-419.json b/homeassistant/components/adguard/.translations/es-419.json index c3d57832cf4..d62402f2eee 100644 --- a/homeassistant/components/adguard/.translations/es-419.json +++ b/homeassistant/components/adguard/.translations/es-419.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente.", "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, "error": { diff --git a/homeassistant/components/adguard/.translations/es.json b/homeassistant/components/adguard/.translations/es.json new file mode 100644 index 00000000000..971d38f9ab2 --- /dev/null +++ b/homeassistant/components/adguard/.translations/es.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/fr.json b/homeassistant/components/adguard/.translations/fr.json new file mode 100644 index 00000000000..338a5a77dad --- /dev/null +++ b/homeassistant/components/adguard/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour." + }, + "error": { + "connection_error": "\u00c9chec de connexion." + }, + "step": { + "hassio_confirm": { + "title": "AdGuard Home via le module compl\u00e9mentaire Hass.io" + }, + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "ssl": "AdGuard Home utilise un certificat SSL", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/sv.json b/homeassistant/components/adguard/.translations/sv.json index b4bd7f7481b..22bd81e3e97 100644 --- a/homeassistant/components/adguard/.translations/sv.json +++ b/homeassistant/components/adguard/.translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "existing_instance_updated": "Uppdaterade existerande konfiguration.", "single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten." }, "error": { diff --git a/homeassistant/components/adguard/.translations/zh-Hans.json b/homeassistant/components/adguard/.translations/zh-Hans.json new file mode 100644 index 00000000000..7c52a9d1ac0 --- /dev/null +++ b/homeassistant/components/adguard/.translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "\u66f4\u65b0\u4e86\u73b0\u6709\u914d\u7f6e\u3002" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", + "username": "\u7528\u6237\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 15b8b9978f6..ba716ae0f9c 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -6,13 +6,27 @@ from adguardhome import AdGuardHome, AdGuardHomeError import voluptuous as vol from homeassistant.components.adguard.const import ( - CONF_FORCE, DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN, - SERVICE_ADD_URL, SERVICE_DISABLE_URL, SERVICE_ENABLE_URL, SERVICE_REFRESH, - SERVICE_REMOVE_URL) + CONF_FORCE, + DATA_ADGUARD_CLIENT, + DATA_ADGUARD_VERION, + DOMAIN, + SERVICE_ADD_URL, + SERVICE_DISABLE_URL, + SERVICE_ENABLE_URL, + SERVICE_REFRESH, + SERVICE_REMOVE_URL, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_URL, - CONF_USERNAME, CONF_VERIFY_SSL) + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity @@ -34,9 +48,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up AdGuard Home from a config entry.""" session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]) adguard = AdGuardHome( @@ -52,7 +64,7 @@ async def async_setup_entry( hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard - for component in 'sensor', 'switch': + for component in "sensor", "switch": hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) @@ -98,9 +110,7 @@ async def async_setup_entry( return True -async def async_unload_entry( - hass: HomeAssistantType, entry: ConfigType -) -> bool: +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: """Unload AdGuard Home config entry.""" hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) @@ -108,7 +118,7 @@ async def async_unload_entry( hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) hass.services.async_remove(DOMAIN, SERVICE_REFRESH) - for component in 'sensor', 'switch': + for component in "sensor", "switch": await hass.config_entries.async_forward_entry_unload(entry, component) del hass.data[DOMAIN] @@ -166,15 +176,10 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity): def device_info(self) -> Dict[str, Any]: """Return device information about this AdGuard Home instance.""" return { - 'identifiers': { - ( - DOMAIN, - self.adguard.host, - self.adguard.port, - self.adguard.base_path, - ) + "identifiers": { + (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) }, - 'name': 'AdGuard Home', - 'manufacturer': 'AdGuard Team', - 'sw_version': self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION), + "name": "AdGuard Home", + "manufacturer": "AdGuard Team", + "sw_version": self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION), } diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index 9ef789f83a8..5a096aeceed 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -8,8 +8,13 @@ from homeassistant import config_entries from homeassistant.components.adguard.const import DOMAIN from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, - CONF_VERIFY_SSL) + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) @@ -31,7 +36,7 @@ class AdGuardHomeFlowHandler(ConfigFlow): async def _show_setup_form(self, errors=None): """Show the setup form to the user.""" return self.async_show_form( - step_id='user', + step_id="user", data_schema=vol.Schema( { vol.Required(CONF_HOST): str, @@ -48,10 +53,8 @@ class AdGuardHomeFlowHandler(ConfigFlow): async def _show_hassio_form(self, errors=None): """Show the Hass.io confirmation form to the user.""" return self.async_show_form( - step_id='hassio_confirm', - description_placeholders={ - 'addon': self._hassio_discovery['addon'] - }, + step_id="hassio_confirm", + description_placeholders={"addon": self._hassio_discovery["addon"]}, data_schema=vol.Schema({}), errors=errors or {}, ) @@ -59,16 +62,14 @@ class AdGuardHomeFlowHandler(ConfigFlow): async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" if self._async_current_entries(): - return self.async_abort(reason='single_instance_allowed') + return self.async_abort(reason="single_instance_allowed") if user_input is None: return await self._show_setup_form(user_input) errors = {} - session = async_get_clientsession( - self.hass, user_input[CONF_VERIFY_SSL] - ) + session = async_get_clientsession(self.hass, user_input[CONF_VERIFY_SSL]) adguard = AdGuardHome( user_input[CONF_HOST], @@ -84,7 +85,7 @@ class AdGuardHomeFlowHandler(ConfigFlow): try: await adguard.version() except AdGuardHomeConnectionError: - errors['base'] = 'connection_error' + errors["base"] = "connection_error" return await self._show_setup_form(errors) return self.async_create_entry( @@ -112,25 +113,30 @@ class AdGuardHomeFlowHandler(ConfigFlow): cur_entry = entries[0] - if (cur_entry.data[CONF_HOST] == user_input[CONF_HOST] and - cur_entry.data[CONF_PORT] == user_input[CONF_PORT]): - return self.async_abort(reason='single_instance_allowed') + if ( + cur_entry.data[CONF_HOST] == user_input[CONF_HOST] + and cur_entry.data[CONF_PORT] == user_input[CONF_PORT] + ): + return self.async_abort(reason="single_instance_allowed") is_loaded = cur_entry.state == config_entries.ENTRY_STATE_LOADED if is_loaded: await self.hass.config_entries.async_unload(cur_entry.entry_id) - self.hass.config_entries.async_update_entry(cur_entry, data={ - **cur_entry.data, - CONF_HOST: user_input[CONF_HOST], - CONF_PORT: user_input[CONF_PORT], - }) + self.hass.config_entries.async_update_entry( + cur_entry, + data={ + **cur_entry.data, + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + }, + ) if is_loaded: await self.hass.config_entries.async_setup(cur_entry.entry_id) - return self.async_abort(reason='existing_instance_updated') + return self.async_abort(reason="existing_instance_updated") async def async_step_hassio_confirm(self, user_input=None): """Confirm Hass.io discovery.""" @@ -152,11 +158,11 @@ class AdGuardHomeFlowHandler(ConfigFlow): try: await adguard.version() except AdGuardHomeConnectionError: - errors['base'] = 'connection_error' + errors["base"] = "connection_error" return await self._show_hassio_form(errors) return self.async_create_entry( - title=self._hassio_discovery['addon'], + title=self._hassio_discovery["addon"], data={ CONF_HOST: self._hassio_discovery[CONF_HOST], CONF_PORT: self._hassio_discovery[CONF_PORT], diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index 6bbabdafaf1..c77d76a70cf 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -1,14 +1,14 @@ """Constants for the AdGuard Home integration.""" -DOMAIN = 'adguard' +DOMAIN = "adguard" -DATA_ADGUARD_CLIENT = 'adguard_client' -DATA_ADGUARD_VERION = 'adguard_version' +DATA_ADGUARD_CLIENT = "adguard_client" +DATA_ADGUARD_VERION = "adguard_version" -CONF_FORCE = 'force' +CONF_FORCE = "force" -SERVICE_ADD_URL = 'add_url' -SERVICE_DISABLE_URL = 'disable_url' -SERVICE_ENABLE_URL = 'enable_url' -SERVICE_REFRESH = 'refresh' -SERVICE_REMOVE_URL = 'remove_url' +SERVICE_ADD_URL = "add_url" +SERVICE_DISABLE_URL = "disable_url" +SERVICE_ENABLE_URL = "enable_url" +SERVICE_REFRESH = "refresh" +SERVICE_REMOVE_URL = "remove_url" diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index abb5309b449..17e53270f25 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -6,7 +6,10 @@ from adguardhome import AdGuardHomeConnectionError from homeassistant.components.adguard import AdGuardHomeDeviceEntity from homeassistant.components.adguard.const import ( - DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN) + DATA_ADGUARD_CLIENT, + DATA_ADGUARD_VERION, + DOMAIN, +) from homeassistant.config_entries import ConfigEntry from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.typing import HomeAssistantType @@ -18,7 +21,7 @@ PARALLEL_UPDATES = 4 async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: """Set up AdGuard Home sensor based on a config entry.""" adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT] @@ -48,12 +51,7 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity): """Defines a AdGuard Home sensor.""" def __init__( - self, - adguard, - name: str, - icon: str, - measurement: str, - unit_of_measurement: str, + self, adguard, name: str, icon: str, measurement: str, unit_of_measurement: str ) -> None: """Initialize AdGuard Home sensor.""" self._state = None @@ -65,12 +63,12 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity): @property def unique_id(self) -> str: """Return the unique ID for this sensor.""" - return '_'.join( + return "_".join( [ DOMAIN, self.adguard.host, str(self.adguard.port), - 'sensor', + "sensor", self.measurement, ] ) @@ -92,11 +90,7 @@ class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): def __init__(self, adguard): """Initialize AdGuard Home sensor.""" super().__init__( - adguard, - 'AdGuard DNS Queries', - 'mdi:magnify', - 'dns_queries', - 'queries', + adguard, "AdGuard DNS Queries", "mdi:magnify", "dns_queries", "queries" ) async def _adguard_update(self) -> None: @@ -111,10 +105,10 @@ class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): """Initialize AdGuard Home sensor.""" super().__init__( adguard, - 'AdGuard DNS Queries Blocked', - 'mdi:magnify-close', - 'blocked_filtering', - 'queries', + "AdGuard DNS Queries Blocked", + "mdi:magnify-close", + "blocked_filtering", + "queries", ) async def _adguard_update(self) -> None: @@ -129,10 +123,10 @@ class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): """Initialize AdGuard Home sensor.""" super().__init__( adguard, - 'AdGuard DNS Queries Blocked Ratio', - 'mdi:magnify-close', - 'blocked_percentage', - '%', + "AdGuard DNS Queries Blocked Ratio", + "mdi:magnify-close", + "blocked_percentage", + "%", ) async def _adguard_update(self) -> None: @@ -148,10 +142,10 @@ class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): """Initialize AdGuard Home sensor.""" super().__init__( adguard, - 'AdGuard Parental Control Blocked', - 'mdi:human-male-girl', - 'blocked_parental', - 'requests', + "AdGuard Parental Control Blocked", + "mdi:human-male-girl", + "blocked_parental", + "requests", ) async def _adguard_update(self) -> None: @@ -166,10 +160,10 @@ class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): """Initialize AdGuard Home sensor.""" super().__init__( adguard, - 'AdGuard Safe Browsing Blocked', - 'mdi:shield-half-full', - 'blocked_safebrowsing', - 'requests', + "AdGuard Safe Browsing Blocked", + "mdi:shield-half-full", + "blocked_safebrowsing", + "requests", ) async def _adguard_update(self) -> None: @@ -184,10 +178,10 @@ class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): """Initialize AdGuard Home sensor.""" super().__init__( adguard, - 'Searches Safe Search Enforced', - 'mdi:shield-search', - 'enforced_safesearch', - 'requests', + "Searches Safe Search Enforced", + "mdi:shield-search", + "enforced_safesearch", + "requests", ) async def _adguard_update(self) -> None: @@ -202,10 +196,10 @@ class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): """Initialize AdGuard Home sensor.""" super().__init__( adguard, - 'AdGuard Average Processing Speed', - 'mdi:speedometer', - 'average_speed', - 'ms', + "AdGuard Average Processing Speed", + "mdi:speedometer", + "average_speed", + "ms", ) async def _adguard_update(self) -> None: @@ -220,11 +214,7 @@ class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): def __init__(self, adguard): """Initialize AdGuard Home sensor.""" super().__init__( - adguard, - 'AdGuard Rules Count', - 'mdi:counter', - 'rules_count', - 'rules', + adguard, "AdGuard Rules Count", "mdi:counter", "rules_count", "rules" ) async def _adguard_update(self) -> None: diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 601bf25b5b0..39cd1ef028d 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -6,7 +6,10 @@ from adguardhome import AdGuardHomeConnectionError, AdGuardHomeError from homeassistant.components.adguard import AdGuardHomeDeviceEntity from homeassistant.components.adguard.const import ( - DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN) + DATA_ADGUARD_CLIENT, + DATA_ADGUARD_VERION, + DOMAIN, +) from homeassistant.config_entries import ConfigEntry from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import ToggleEntity @@ -19,7 +22,7 @@ PARALLEL_UPDATES = 1 async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: """Set up AdGuard Home switch based on a config entry.""" adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT] @@ -54,14 +57,8 @@ class AdGuardHomeSwitch(ToggleEntity, AdGuardHomeDeviceEntity): @property def unique_id(self) -> str: """Return the unique ID for this sensor.""" - return '_'.join( - [ - DOMAIN, - self.adguard.host, - str(self.adguard.port), - 'switch', - self._key, - ] + return "_".join( + [DOMAIN, self.adguard.host, str(self.adguard.port), "switch", self._key] ) @property @@ -74,9 +71,7 @@ class AdGuardHomeSwitch(ToggleEntity, AdGuardHomeDeviceEntity): try: await self._adguard_turn_off() except AdGuardHomeError: - _LOGGER.error( - "An error occurred while turning off AdGuard Home switch." - ) + _LOGGER.error("An error occurred while turning off AdGuard Home switch.") self._available = False async def _adguard_turn_off(self) -> None: @@ -88,9 +83,7 @@ class AdGuardHomeSwitch(ToggleEntity, AdGuardHomeDeviceEntity): try: await self._adguard_turn_on() except AdGuardHomeError: - _LOGGER.error( - "An error occurred while turning on AdGuard Home switch." - ) + _LOGGER.error("An error occurred while turning on AdGuard Home switch.") self._available = False async def _adguard_turn_on(self) -> None: @@ -104,7 +97,7 @@ class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): def __init__(self, adguard) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, "AdGuard Protection", 'mdi:shield-check', 'protection' + adguard, "AdGuard Protection", "mdi:shield-check", "protection" ) async def _adguard_turn_off(self) -> None: @@ -126,7 +119,7 @@ class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): def __init__(self, adguard) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, "AdGuard Parental Control", 'mdi:shield-check', 'parental' + adguard, "AdGuard Parental Control", "mdi:shield-check", "parental" ) async def _adguard_turn_off(self) -> None: @@ -148,7 +141,7 @@ class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): def __init__(self, adguard) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, "AdGuard Safe Search", 'mdi:shield-check', 'safesearch' + adguard, "AdGuard Safe Search", "mdi:shield-check", "safesearch" ) async def _adguard_turn_off(self) -> None: @@ -170,10 +163,7 @@ class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): def __init__(self, adguard) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, - "AdGuard Safe Browsing", - 'mdi:shield-check', - 'safebrowsing', + adguard, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing" ) async def _adguard_turn_off(self) -> None: @@ -194,9 +184,7 @@ class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): def __init__(self, adguard) -> None: """Initialize AdGuard Home switch.""" - super().__init__( - adguard, "AdGuard Filtering", 'mdi:shield-check', 'filtering' - ) + super().__init__(adguard, "AdGuard Filtering", "mdi:shield-check", "filtering") async def _adguard_turn_off(self) -> None: """Turn off the switch.""" @@ -216,9 +204,7 @@ class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): def __init__(self, adguard) -> None: """Initialize AdGuard Home switch.""" - super().__init__( - adguard, "AdGuard Query Log", 'mdi:shield-check', 'querylog' - ) + super().__init__(adguard, "AdGuard Query Log", "mdi:shield-check", "querylog") async def _adguard_turn_off(self) -> None: """Turn off the switch.""" diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 920a2a034d7..1b4f11c7cc1 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -10,57 +10,76 @@ import async_timeout import voluptuous as vol from homeassistant.const import ( - CONF_DEVICE, CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_STOP) + CONF_DEVICE, + CONF_IP_ADDRESS, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -DATA_ADS = 'data_ads' +DATA_ADS = "data_ads" # Supported Types -ADSTYPE_BOOL = 'bool' -ADSTYPE_BYTE = 'byte' -ADSTYPE_DINT = 'dint' -ADSTYPE_INT = 'int' -ADSTYPE_UDINT = 'udint' -ADSTYPE_UINT = 'uint' +ADSTYPE_BOOL = "bool" +ADSTYPE_BYTE = "byte" +ADSTYPE_DINT = "dint" +ADSTYPE_INT = "int" +ADSTYPE_UDINT = "udint" +ADSTYPE_UINT = "uint" -CONF_ADS_FACTOR = 'factor' -CONF_ADS_TYPE = 'adstype' -CONF_ADS_VALUE = 'value' -CONF_ADS_VAR = 'adsvar' -CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness' -CONF_ADS_VAR_POSITION = 'adsvar_position' +CONF_ADS_FACTOR = "factor" +CONF_ADS_TYPE = "adstype" +CONF_ADS_VALUE = "value" +CONF_ADS_VAR = "adsvar" +CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness" +CONF_ADS_VAR_POSITION = "adsvar_position" -STATE_KEY_STATE = 'state' -STATE_KEY_BRIGHTNESS = 'brightness' -STATE_KEY_POSITION = 'position' +STATE_KEY_STATE = "state" +STATE_KEY_BRIGHTNESS = "brightness" +STATE_KEY_POSITION = "position" -DOMAIN = 'ads' +DOMAIN = "ads" -SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name' +SERVICE_WRITE_DATA_BY_NAME = "write_data_by_name" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICE): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Optional(CONF_IP_ADDRESS): cv.string, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_DEVICE): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Optional(CONF_IP_ADDRESS): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({ - vol.Required(CONF_ADS_TYPE): - vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE, ADSTYPE_BOOL, - ADSTYPE_DINT, ADSTYPE_UDINT]), - vol.Required(CONF_ADS_VALUE): vol.Coerce(int), - vol.Required(CONF_ADS_VAR): cv.string, -}) +SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema( + { + vol.Required(CONF_ADS_TYPE): vol.In( + [ + ADSTYPE_INT, + ADSTYPE_UINT, + ADSTYPE_BYTE, + ADSTYPE_BOOL, + ADSTYPE_DINT, + ADSTYPE_UDINT, + ] + ), + vol.Required(CONF_ADS_VALUE): vol.Coerce(int), + vol.Required(CONF_ADS_VAR): cv.string, + } +) def setup(hass, config): """Set up the ADS component.""" import pyads + conf = config[DOMAIN] net_id = conf.get(CONF_DEVICE) @@ -91,7 +110,10 @@ def setup(hass, config): except pyads.ADSError: _LOGGER.error( "Could not connect to ADS host (netid=%s, ip=%s, port=%s)", - net_id, ip_address, port) + net_id, + ip_address, + port, + ) return False hass.data[DATA_ADS] = ads @@ -109,15 +131,18 @@ def setup(hass, config): _LOGGER.error(err) hass.services.register( - DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name, - schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME) + DOMAIN, + SERVICE_WRITE_DATA_BY_NAME, + handle_write_data_by_name, + schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME, + ) return True # Tuple to hold data needed for notification NotificationItem = namedtuple( - 'NotificationItem', 'hnotify huser name plc_datatype callback' + "NotificationItem", "hnotify huser name plc_datatype callback" ) @@ -137,15 +162,17 @@ class AdsHub: def shutdown(self, *args, **kwargs): """Shutdown ADS connection.""" import pyads + _LOGGER.debug("Shutting down ADS") for notification_item in self._notification_items.values(): _LOGGER.debug( "Deleting device notification %d, %d", - notification_item.hnotify, notification_item.huser) + notification_item.hnotify, + notification_item.huser, + ) try: self._client.del_device_notification( - notification_item.hnotify, - notification_item.huser + notification_item.hnotify, notification_item.huser ) except pyads.ADSError as err: _LOGGER.error(err) @@ -161,6 +188,7 @@ class AdsHub: def write_by_name(self, name, value, plc_datatype): """Write a value to the device.""" import pyads + with self._lock: try: return self._client.write_by_name(name, value, plc_datatype) @@ -170,6 +198,7 @@ class AdsHub: def read_by_name(self, name, plc_datatype): """Read a value from the device.""" import pyads + with self._lock: try: return self._client.read_by_name(name, plc_datatype) @@ -179,22 +208,25 @@ class AdsHub: def add_device_notification(self, name, plc_datatype, callback): """Add a notification to the ADS devices.""" import pyads + attr = pyads.NotificationAttrib(ctypes.sizeof(plc_datatype)) with self._lock: try: hnotify, huser = self._client.add_device_notification( - name, attr, self._device_notification_callback) + name, attr, self._device_notification_callback + ) except pyads.ADSError as err: _LOGGER.error("Error subscribing to %s: %s", name, err) else: hnotify = int(hnotify) self._notification_items[hnotify] = NotificationItem( - hnotify, huser, name, plc_datatype, callback) + hnotify, huser, name, plc_datatype, callback + ) _LOGGER.debug( - "Added device notification %d for variable %s", - hnotify, name) + "Added device notification %d for variable %s", hnotify, name + ) def _device_notification_callback(self, notification, name): """Handle device notifications.""" @@ -213,17 +245,17 @@ class AdsHub: # Parse data to desired datatype if notification_item.plc_datatype == self.PLCTYPE_BOOL: - value = bool(struct.unpack('' - 'You will need to restart hass after fixing.' - ''.format(ex), + "Error: {}
" + "You will need to restart hass after fixing." + "".format(ex), title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) + notification_id=NOTIFICATION_ID, + ) class AladdinDevice(CoverDevice): @@ -57,15 +67,15 @@ class AladdinDevice(CoverDevice): def __init__(self, acc, device): """Initialize the cover.""" self._acc = acc - self._device_id = device['device_id'] - self._number = device['door_number'] - self._name = device['name'] - self._status = STATES_MAP.get(device['status']) + self._device_id = device["device_id"] + self._number = device["door_number"] + self._name = device["name"] + self._status = STATES_MAP.get(device["status"]) @property def device_class(self): """Define this cover as a garage door.""" - return 'garage' + return "garage" @property def supported_features(self): @@ -75,7 +85,7 @@ class AladdinDevice(CoverDevice): @property def unique_id(self): """Return a unique ID.""" - return '{}-{}'.format(self._device_id, self._number) + return "{}-{}".format(self._device_id, self._number) @property def name(self): diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 47f92bfe641..ab0b810ee83 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -5,60 +5,65 @@ import logging import voluptuous as vol from homeassistant.const import ( - ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, - SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) + ATTR_CODE, + ATTR_CODE_FORMAT, + SERVICE_ALARM_TRIGGER, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, +) from homeassistant.helpers.config_validation import ( # noqa - PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) + ENTITY_SERVICE_SCHEMA, + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -DOMAIN = 'alarm_control_panel' +DOMAIN = "alarm_control_panel" SCAN_INTERVAL = timedelta(seconds=30) -ATTR_CHANGED_BY = 'changed_by' -FORMAT_TEXT = 'text' -FORMAT_NUMBER = 'number' -ATTR_CODE_ARM_REQUIRED = 'code_arm_required' +ATTR_CHANGED_BY = "changed_by" +FORMAT_TEXT = "text" +FORMAT_NUMBER = "number" +ATTR_CODE_ARM_REQUIRED = "code_arm_required" -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" -ALARM_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Optional(ATTR_CODE): cv.string, -}) +ALARM_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Optional(ATTR_CODE): cv.string} +) async def async_setup(hass, config): """Track states and offer events for sensors.""" component = hass.data[DOMAIN] = EntityComponent( - logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) + logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL + ) await component.async_setup(config) component.async_register_entity_service( - SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, - 'async_alarm_disarm' + SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm" ) component.async_register_entity_service( - SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, - 'async_alarm_arm_home' + SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, "async_alarm_arm_home" ) component.async_register_entity_service( - SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, - 'async_alarm_arm_away' + SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, "async_alarm_arm_away" ) component.async_register_entity_service( - SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, - 'async_alarm_arm_night' + SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, "async_alarm_arm_night" ) component.async_register_entity_service( - SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA, - 'async_alarm_arm_custom_bypass' + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_custom_bypass", ) component.async_register_entity_service( - SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, - 'async_alarm_trigger' + SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, "async_alarm_trigger" ) return True @@ -157,8 +162,7 @@ class AlarmControlPanel(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_executor_job( - self.alarm_arm_custom_bypass, code) + return self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code) @property def state_attributes(self): @@ -166,6 +170,6 @@ class AlarmControlPanel(Entity): state_attr = { ATTR_CODE_FORMAT: self.code_format, ATTR_CHANGED_BY: self.changed_by, - ATTR_CODE_ARM_REQUIRED: self.code_arm_required + ATTR_CODE_ARM_REQUIRED: self.code_arm_required, } return state_attr diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index b4d1a2e0b9f..e0ff80ae9fa 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -12,85 +12,105 @@ from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA _LOGGER = logging.getLogger(__name__) -DOMAIN = 'alarmdecoder' +DOMAIN = "alarmdecoder" -DATA_AD = 'alarmdecoder' +DATA_AD = "alarmdecoder" -CONF_DEVICE = 'device' -CONF_DEVICE_BAUD = 'baudrate' -CONF_DEVICE_PATH = 'path' -CONF_DEVICE_PORT = 'port' -CONF_DEVICE_TYPE = 'type' -CONF_PANEL_DISPLAY = 'panel_display' -CONF_ZONE_NAME = 'name' -CONF_ZONE_TYPE = 'type' -CONF_ZONE_LOOP = 'loop' -CONF_ZONE_RFID = 'rfid' -CONF_ZONES = 'zones' -CONF_RELAY_ADDR = 'relayaddr' -CONF_RELAY_CHAN = 'relaychan' +CONF_DEVICE = "device" +CONF_DEVICE_BAUD = "baudrate" +CONF_DEVICE_PATH = "path" +CONF_DEVICE_PORT = "port" +CONF_DEVICE_TYPE = "type" +CONF_PANEL_DISPLAY = "panel_display" +CONF_ZONE_NAME = "name" +CONF_ZONE_TYPE = "type" +CONF_ZONE_LOOP = "loop" +CONF_ZONE_RFID = "rfid" +CONF_ZONES = "zones" +CONF_RELAY_ADDR = "relayaddr" +CONF_RELAY_CHAN = "relaychan" -DEFAULT_DEVICE_TYPE = 'socket' -DEFAULT_DEVICE_HOST = 'localhost' +DEFAULT_DEVICE_TYPE = "socket" +DEFAULT_DEVICE_HOST = "localhost" DEFAULT_DEVICE_PORT = 10000 -DEFAULT_DEVICE_PATH = '/dev/ttyUSB0' +DEFAULT_DEVICE_PATH = "/dev/ttyUSB0" DEFAULT_DEVICE_BAUD = 115200 DEFAULT_PANEL_DISPLAY = False -DEFAULT_ZONE_TYPE = 'opening' +DEFAULT_ZONE_TYPE = "opening" -SIGNAL_PANEL_MESSAGE = 'alarmdecoder.panel_message' -SIGNAL_PANEL_ARM_AWAY = 'alarmdecoder.panel_arm_away' -SIGNAL_PANEL_ARM_HOME = 'alarmdecoder.panel_arm_home' -SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm' +SIGNAL_PANEL_MESSAGE = "alarmdecoder.panel_message" +SIGNAL_PANEL_ARM_AWAY = "alarmdecoder.panel_arm_away" +SIGNAL_PANEL_ARM_HOME = "alarmdecoder.panel_arm_home" +SIGNAL_PANEL_DISARM = "alarmdecoder.panel_disarm" -SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault' -SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore' -SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message' -SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message' +SIGNAL_ZONE_FAULT = "alarmdecoder.zone_fault" +SIGNAL_ZONE_RESTORE = "alarmdecoder.zone_restore" +SIGNAL_RFX_MESSAGE = "alarmdecoder.rfx_message" +SIGNAL_REL_MESSAGE = "alarmdecoder.rel_message" -DEVICE_SOCKET_SCHEMA = vol.Schema({ - vol.Required(CONF_DEVICE_TYPE): 'socket', - vol.Optional(CONF_HOST, default=DEFAULT_DEVICE_HOST): cv.string, - vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port}) +DEVICE_SOCKET_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_TYPE): "socket", + vol.Optional(CONF_HOST, default=DEFAULT_DEVICE_HOST): cv.string, + vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port, + } +) -DEVICE_SERIAL_SCHEMA = vol.Schema({ - vol.Required(CONF_DEVICE_TYPE): 'serial', - vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string, - vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string}) +DEVICE_SERIAL_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_TYPE): "serial", + vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string, + vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string, + } +) -DEVICE_USB_SCHEMA = vol.Schema({ - vol.Required(CONF_DEVICE_TYPE): 'usb'}) +DEVICE_USB_SCHEMA = vol.Schema({vol.Required(CONF_DEVICE_TYPE): "usb"}) -ZONE_SCHEMA = vol.Schema({ - vol.Required(CONF_ZONE_NAME): cv.string, - vol.Optional(CONF_ZONE_TYPE, - default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA), - vol.Optional(CONF_ZONE_RFID): cv.string, - vol.Optional(CONF_ZONE_LOOP): - vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), - vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation', - 'Relay address and channel must exist together'): cv.byte, - vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation', - 'Relay address and channel must exist together'): cv.byte}) +ZONE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ZONE_NAME): cv.string, + vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): vol.Any( + DEVICE_CLASSES_SCHEMA + ), + vol.Optional(CONF_ZONE_RFID): cv.string, + vol.Optional(CONF_ZONE_LOOP): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), + vol.Inclusive( + CONF_RELAY_ADDR, + "relaylocation", + "Relay address and channel must exist together", + ): cv.byte, + vol.Inclusive( + CONF_RELAY_CHAN, + "relaylocation", + "Relay address and channel must exist together", + ): cv.byte, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICE): vol.Any( - DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA, - DEVICE_USB_SCHEMA), - vol.Optional(CONF_PANEL_DISPLAY, - default=DEFAULT_PANEL_DISPLAY): cv.boolean, - vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_DEVICE): vol.Any( + DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA, DEVICE_USB_SCHEMA + ), + vol.Optional( + CONF_PANEL_DISPLAY, default=DEFAULT_PANEL_DISPLAY + ): cv.boolean, + vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): """Set up for the AlarmDecoder devices.""" from alarmdecoder import AlarmDecoder - from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice) + from alarmdecoder.devices import SocketDevice, SerialDevice, USBDevice conf = config.get(DOMAIN) @@ -115,13 +135,15 @@ def setup(hass, config): def open_connection(now=None): """Open a connection to AlarmDecoder.""" from alarmdecoder.util import NoDeviceError + nonlocal restart try: controller.open(baud) except NoDeviceError: _LOGGER.debug("Failed to connect. Retrying in 5 seconds") hass.helpers.event.track_point_in_time( - open_connection, dt_util.utcnow() + timedelta(seconds=5)) + open_connection, dt_util.utcnow() + timedelta(seconds=5) + ) return _LOGGER.debug("Established a connection with the alarmdecoder") restart = True @@ -137,39 +159,34 @@ def setup(hass, config): def handle_message(sender, message): """Handle message from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_PANEL_MESSAGE, message) + hass.helpers.dispatcher.dispatcher_send(SIGNAL_PANEL_MESSAGE, message) def handle_rfx_message(sender, message): """Handle RFX message from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_RFX_MESSAGE, message) + hass.helpers.dispatcher.dispatcher_send(SIGNAL_RFX_MESSAGE, message) def zone_fault_callback(sender, zone): """Handle zone fault from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_ZONE_FAULT, zone) + hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_FAULT, zone) def zone_restore_callback(sender, zone): """Handle zone restore from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_ZONE_RESTORE, zone) + hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_RESTORE, zone) def handle_rel_message(sender, message): """Handle relay message from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_REL_MESSAGE, message) + hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message) controller = False - if device_type == 'socket': + if device_type == "socket": host = device.get(CONF_HOST) port = device.get(CONF_DEVICE_PORT) controller = AlarmDecoder(SocketDevice(interface=(host, port))) - elif device_type == 'serial': + elif device_type == "serial": path = device.get(CONF_DEVICE_PATH) baud = device.get(CONF_DEVICE_BAUD) controller = AlarmDecoder(SerialDevice(interface=path)) - elif device_type == 'usb': + elif device_type == "usb": AlarmDecoder(USBDevice.find()) return False @@ -186,13 +203,12 @@ def setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) - load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config) + load_platform(hass, "alarm_control_panel", DOMAIN, conf, config) if zones: - load_platform( - hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config) + load_platform(hass, "binary_sensor", DOMAIN, {CONF_ZONES: zones}, config) if display: - load_platform(hass, 'sensor', DOMAIN, conf, config) + load_platform(hass, "sensor", DOMAIN, conf, config) return True diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 51645b516b9..42f839bcd60 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -5,18 +5,20 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( - ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) + ATTR_CODE, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) import homeassistant.helpers.config_validation as cv from . import DATA_AD, SIGNAL_PANEL_MESSAGE _LOGGER = logging.getLogger(__name__) -SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime' -ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({ - vol.Required(ATTR_CODE): cv.string, -}) +SERVICE_ALARM_TOGGLE_CHIME = "alarmdecoder_alarm_toggle_chime" +ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string}) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -30,8 +32,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device.alarm_toggle_chime(code) hass.services.register( - alarm.DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler, - schema=ALARM_TOGGLE_CHIME_SCHEMA) + alarm.DOMAIN, + SERVICE_ALARM_TOGGLE_CHIME, + alarm_toggle_chime_handler, + schema=ALARM_TOGGLE_CHIME_SCHEMA, + ) class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): @@ -55,7 +60,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_PANEL_MESSAGE, self._message_callback) + SIGNAL_PANEL_MESSAGE, self._message_callback + ) def _message_callback(self, message): """Handle received messages.""" @@ -104,15 +110,15 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): def device_state_attributes(self): """Return the state attributes.""" return { - 'ac_power': self._ac_power, - 'backlight_on': self._backlight_on, - 'battery_low': self._battery_low, - 'check_zone': self._check_zone, - 'chime': self._chime, - 'entry_delay_off': self._entry_delay_off, - 'programming_mode': self._programming_mode, - 'ready': self._ready, - 'zone_bypassed': self._zone_bypassed, + "ac_power": self._ac_power, + "backlight_on": self._backlight_on, + "battery_low": self._battery_low, + "check_zone": self._check_zone, + "chime": self._chime, + "entry_delay_off": self._entry_delay_off, + "programming_mode": self._programming_mode, + "ready": self._ready, + "zone_bypassed": self._zone_bypassed, } def alarm_disarm(self, code=None): diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 91ff8b381b5..bbcc4fd6eae 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -4,20 +4,30 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from . import ( - CONF_RELAY_ADDR, CONF_RELAY_CHAN, CONF_ZONE_LOOP, CONF_ZONE_NAME, - CONF_ZONE_RFID, CONF_ZONE_TYPE, CONF_ZONES, SIGNAL_REL_MESSAGE, - SIGNAL_RFX_MESSAGE, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE, ZONE_SCHEMA) + CONF_RELAY_ADDR, + CONF_RELAY_CHAN, + CONF_ZONE_LOOP, + CONF_ZONE_NAME, + CONF_ZONE_RFID, + CONF_ZONE_TYPE, + CONF_ZONES, + SIGNAL_REL_MESSAGE, + SIGNAL_RFX_MESSAGE, + SIGNAL_ZONE_FAULT, + SIGNAL_ZONE_RESTORE, + ZONE_SCHEMA, +) _LOGGER = logging.getLogger(__name__) -ATTR_RF_BIT0 = 'rf_bit0' -ATTR_RF_LOW_BAT = 'rf_low_battery' -ATTR_RF_SUPERVISED = 'rf_supervised' -ATTR_RF_BIT3 = 'rf_bit3' -ATTR_RF_LOOP3 = 'rf_loop3' -ATTR_RF_LOOP2 = 'rf_loop2' -ATTR_RF_LOOP4 = 'rf_loop4' -ATTR_RF_LOOP1 = 'rf_loop1' +ATTR_RF_BIT0 = "rf_bit0" +ATTR_RF_LOW_BAT = "rf_low_battery" +ATTR_RF_SUPERVISED = "rf_supervised" +ATTR_RF_BIT3 = "rf_bit3" +ATTR_RF_LOOP3 = "rf_loop3" +ATTR_RF_LOOP2 = "rf_loop2" +ATTR_RF_LOOP4 = "rf_loop4" +ATTR_RF_LOOP1 = "rf_loop1" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -34,8 +44,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): relay_addr = device_config_data.get(CONF_RELAY_ADDR) relay_chan = device_config_data.get(CONF_RELAY_CHAN) device = AlarmDecoderBinarySensor( - zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr, - relay_chan) + zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr, relay_chan + ) devices.append(device) add_entities(devices) @@ -46,8 +56,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AlarmDecoderBinarySensor(BinarySensorDevice): """Representation of an AlarmDecoder binary sensor.""" - def __init__(self, zone_number, zone_name, zone_type, zone_rfid, zone_loop, - relay_addr, relay_chan): + def __init__( + self, + zone_number, + zone_name, + zone_type, + zone_rfid, + zone_loop, + relay_addr, + relay_chan, + ): """Initialize the binary_sensor.""" self._zone_number = zone_number self._zone_type = zone_type @@ -62,16 +80,20 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_ZONE_FAULT, self._fault_callback) + SIGNAL_ZONE_FAULT, self._fault_callback + ) self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_ZONE_RESTORE, self._restore_callback) + SIGNAL_ZONE_RESTORE, self._restore_callback + ) self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_RFX_MESSAGE, self._rfx_message_callback) + SIGNAL_RFX_MESSAGE, self._rfx_message_callback + ) self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_REL_MESSAGE, self._rel_message_callback) + SIGNAL_REL_MESSAGE, self._rel_message_callback + ) @property def name(self): @@ -130,9 +152,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): def _rel_message_callback(self, message): """Update relay state.""" - if (self._relay_addr == message.address and - self._relay_chan == message.channel): - _LOGGER.debug("Relay %d:%d value:%d", message.address, - message.channel, message.value) + if self._relay_addr == message.address and self._relay_chan == message.channel: + _LOGGER.debug( + "Relay %d:%d value:%d", message.address, message.channel, message.value + ) self._state = message.value self.schedule_update_ha_state() diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 9fb37d62376..196e8d704e1 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -24,13 +24,14 @@ class AlarmDecoderSensor(Entity): """Initialize the alarm panel.""" self._display = "" self._state = None - self._icon = 'mdi:alarm-check' - self._name = 'Alarm Panel Display' + self._icon = "mdi:alarm-check" + self._name = "Alarm Panel Display" async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_PANEL_MESSAGE, self._message_callback) + SIGNAL_PANEL_MESSAGE, self._message_callback + ) def _message_callback(self, message): if self._display != message.text: diff --git a/homeassistant/components/alarmdotcom/alarm_control_panel.py b/homeassistant/components/alarmdotcom/alarm_control_panel.py index 5919bf84f41..f80e8d6eb1e 100644 --- a/homeassistant/components/alarmdotcom/alarm_control_panel.py +++ b/homeassistant/components/alarmdotcom/alarm_control_panel.py @@ -7,25 +7,32 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) + CONF_CODE, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Alarm.com' +DEFAULT_NAME = "Alarm.com" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_CODE): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_CODE): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a Alarm.com control panel.""" name = config.get(CONF_NAME) code = config.get(CONF_CODE) @@ -43,7 +50,8 @@ class AlarmDotCom(alarm.AlarmControlPanel): def __init__(self, hass, name, code, username, password): """Initialize the Alarm.com status.""" from pyalarmdotcom import Alarmdotcom - _LOGGER.debug('Setting up Alarm.com...') + + _LOGGER.debug("Setting up Alarm.com...") self._hass = hass self._name = name self._code = str(code) if code else None @@ -51,8 +59,7 @@ class AlarmDotCom(alarm.AlarmControlPanel): self._password = password self._websession = async_get_clientsession(self._hass) self._state = None - self._alarm = Alarmdotcom( - username, password, self._websession, hass.loop) + self._alarm = Alarmdotcom(username, password, self._websession, hass.loop) async def async_login(self): """Login to Alarm.com.""" @@ -73,27 +80,25 @@ class AlarmDotCom(alarm.AlarmControlPanel): """Return one or more digits/characters.""" if self._code is None: return None - if isinstance(self._code, str) and re.search('^\\d+$', self._code): + if isinstance(self._code, str) and re.search("^\\d+$", self._code): return alarm.FORMAT_NUMBER return alarm.FORMAT_TEXT @property def state(self): """Return the state of the device.""" - if self._alarm.state.lower() == 'disarmed': + if self._alarm.state.lower() == "disarmed": return STATE_ALARM_DISARMED - if self._alarm.state.lower() == 'armed stay': + if self._alarm.state.lower() == "armed stay": return STATE_ALARM_ARMED_HOME - if self._alarm.state.lower() == 'armed away': + if self._alarm.state.lower() == "armed away": return STATE_ALARM_ARMED_AWAY return None @property def device_state_attributes(self): """Return the state attributes.""" - return { - 'sensor_status': self._alarm.sensor_status - } + return {"sensor_status": self._alarm.sensor_status} async def async_alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index a5b6d26d4fd..420d730933c 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -7,51 +7,65 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, DOMAIN as DOMAIN_NOTIFY) + ATTR_MESSAGE, + ATTR_TITLE, + ATTR_DATA, + DOMAIN as DOMAIN_NOTIFY, +) from homeassistant.const import ( - CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, - SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) + CONF_ENTITY_ID, + STATE_IDLE, + CONF_NAME, + CONF_STATE, + STATE_ON, + STATE_OFF, + SERVICE_TURN_ON, + SERVICE_TURN_OFF, + SERVICE_TOGGLE, + ATTR_ENTITY_ID, +) from homeassistant.helpers import service, event from homeassistant.helpers.entity import ToggleEntity from homeassistant.util.dt import now _LOGGER = logging.getLogger(__name__) -DOMAIN = 'alert' -ENTITY_ID_FORMAT = DOMAIN + '.{}' +DOMAIN = "alert" +ENTITY_ID_FORMAT = DOMAIN + ".{}" -CONF_CAN_ACK = 'can_acknowledge' -CONF_NOTIFIERS = 'notifiers' -CONF_REPEAT = 'repeat' -CONF_SKIP_FIRST = 'skip_first' -CONF_ALERT_MESSAGE = 'message' -CONF_DONE_MESSAGE = 'done_message' -CONF_TITLE = 'title' -CONF_DATA = 'data' +CONF_CAN_ACK = "can_acknowledge" +CONF_NOTIFIERS = "notifiers" +CONF_REPEAT = "repeat" +CONF_SKIP_FIRST = "skip_first" +CONF_ALERT_MESSAGE = "message" +CONF_DONE_MESSAGE = "done_message" +CONF_TITLE = "title" +CONF_DATA = "data" DEFAULT_CAN_ACK = True DEFAULT_SKIP_FIRST = False -ALERT_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_STATE, default=STATE_ON): cv.string, - vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]), - vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean, - vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, - vol.Optional(CONF_ALERT_MESSAGE): cv.template, - vol.Optional(CONF_DONE_MESSAGE): cv.template, - vol.Optional(CONF_TITLE): cv.template, - vol.Optional(CONF_DATA): dict, - vol.Required(CONF_NOTIFIERS): cv.ensure_list}) +ALERT_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_STATE, default=STATE_ON): cv.string, + vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]), + vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean, + vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, + vol.Optional(CONF_ALERT_MESSAGE): cv.template, + vol.Optional(CONF_DONE_MESSAGE): cv.template, + vol.Optional(CONF_TITLE): cv.template, + vol.Optional(CONF_DATA): dict, + vol.Required(CONF_NOTIFIERS): cv.ensure_list, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA)}, extra=vol.ALLOW_EXTRA +) -ALERT_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, -}) +ALERT_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) def is_on(hass, entity_id): @@ -79,11 +93,23 @@ async def async_setup(hass, config): title_template = cfg.get(CONF_TITLE) data = cfg.get(CONF_DATA) - entities.append(Alert(hass, object_id, name, - watched_entity_id, alert_state, repeat, - skip_first, message_template, - done_message_template, notifiers, - can_ack, title_template, data)) + entities.append( + Alert( + hass, + object_id, + name, + watched_entity_id, + alert_state, + repeat, + skip_first, + message_template, + done_message_template, + notifiers, + can_ack, + title_template, + data, + ) + ) if not entities: return False @@ -107,14 +133,17 @@ async def async_setup(hass, config): # Setup service calls hass.services.async_register( - DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service, - schema=ALERT_SERVICE_SCHEMA) + DOMAIN, + SERVICE_TURN_OFF, + async_handle_alert_service, + schema=ALERT_SERVICE_SCHEMA, + ) hass.services.async_register( - DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, - schema=ALERT_SERVICE_SCHEMA) + DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA + ) hass.services.async_register( - DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, - schema=ALERT_SERVICE_SCHEMA) + DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA + ) tasks = [alert.async_update_ha_state() for alert in entities] if tasks: @@ -126,10 +155,22 @@ async def async_setup(hass, config): class Alert(ToggleEntity): """Representation of an alert.""" - def __init__(self, hass, entity_id, name, watched_entity_id, - state, repeat, skip_first, message_template, - done_message_template, notifiers, can_ack, title_template, - data): + def __init__( + self, + hass, + entity_id, + name, + watched_entity_id, + state, + repeat, + skip_first, + message_template, + done_message_template, + notifiers, + can_ack, + title_template, + data, + ): """Initialize the alert.""" self.hass = hass self._name = name @@ -162,7 +203,8 @@ class Alert(ToggleEntity): self.entity_id = ENTITY_ID_FORMAT.format(entity_id) event.async_track_state_change( - hass, watched_entity_id, self.watched_entity_change) + hass, watched_entity_id, self.watched_entity_change + ) @property def name(self): @@ -224,8 +266,9 @@ class Alert(ToggleEntity): """Schedule a notification.""" delay = self._delay[self._next_delay] next_msg = now() + delay - self._cancel = \ - event.async_track_point_in_time(self.hass, self._notify, next_msg) + self._cancel = event.async_track_point_in_time( + self.hass, self._notify, next_msg + ) self._next_delay = min(self._next_delay + 1, len(self._delay) - 1) async def _notify(self, *args): @@ -270,8 +313,7 @@ class Alert(ToggleEntity): _LOGGER.debug(msg_payload) for target in self._notifiers: - await self.hass.services.async_call( - DOMAIN_NOTIFY, target, msg_payload) + await self.hass.services.async_call(DOMAIN_NOTIFY, target, msg_payload) async def async_turn_on(self, **kwargs): """Async Unacknowledge alert.""" diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index a15d87175db..cb0d093bb48 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -9,45 +9,68 @@ from homeassistant.const import CONF_NAME from . import flash_briefings, intent, smart_home_http from .const import ( - CONF_AUDIO, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY_URL, - CONF_ENDPOINT, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, CONF_FILTER, - CONF_ENTITY_CONFIG, CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES) + CONF_AUDIO, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_DISPLAY_URL, + CONF_ENDPOINT, + CONF_TEXT, + CONF_TITLE, + CONF_UID, + DOMAIN, + CONF_FILTER, + CONF_ENTITY_CONFIG, + CONF_DESCRIPTION, + CONF_DISPLAY_CATEGORIES, +) _LOGGER = logging.getLogger(__name__) -CONF_FLASH_BRIEFINGS = 'flash_briefings' -CONF_SMART_HOME = 'smart_home' +CONF_FLASH_BRIEFINGS = "flash_briefings" +CONF_SMART_HOME = "smart_home" -ALEXA_ENTITY_SCHEMA = vol.Schema({ - vol.Optional(CONF_DESCRIPTION): cv.string, - vol.Optional(CONF_DISPLAY_CATEGORIES): cv.string, - vol.Optional(CONF_NAME): cv.string, -}) - -SMART_HOME_SCHEMA = vol.Schema({ - vol.Optional(CONF_ENDPOINT): cv.string, - vol.Optional(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, - vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA} -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: { - CONF_FLASH_BRIEFINGS: { - cv.string: vol.All(cv.ensure_list, [{ - vol.Optional(CONF_UID): cv.string, - vol.Required(CONF_TITLE): cv.template, - vol.Optional(CONF_AUDIO): cv.template, - vol.Required(CONF_TEXT, default=""): cv.template, - vol.Optional(CONF_DISPLAY_URL): cv.template, - }]), - }, - # vol.Optional here would mean we couldn't distinguish between an empty - # smart_home: and none at all. - CONF_SMART_HOME: vol.Any(SMART_HOME_SCHEMA, None), +ALEXA_ENTITY_SCHEMA = vol.Schema( + { + vol.Optional(CONF_DESCRIPTION): cv.string, + vol.Optional(CONF_DISPLAY_CATEGORIES): cv.string, + vol.Optional(CONF_NAME): cv.string, } -}, extra=vol.ALLOW_EXTRA) +) + +SMART_HOME_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ENDPOINT): cv.string, + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: { + CONF_FLASH_BRIEFINGS: { + cv.string: vol.All( + cv.ensure_list, + [ + { + vol.Optional(CONF_UID): cv.string, + vol.Required(CONF_TITLE): cv.template, + vol.Optional(CONF_AUDIO): cv.template, + vol.Required(CONF_TEXT, default=""): cv.template, + vol.Optional(CONF_DISPLAY_URL): cv.template, + } + ], + ) + }, + # vol.Optional here would mean we couldn't distinguish between an empty + # smart_home: and none at all. + CONF_SMART_HOME: vol.Any(SMART_HOME_SCHEMA, None), + } + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index dd61018d739..d4633d938ed 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -13,12 +13,10 @@ from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token" -LWA_HEADERS = { - "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" -} +LWA_HEADERS = {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"} PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300 -STORAGE_KEY = 'alexa_auth' +STORAGE_KEY = "alexa_auth" STORAGE_VERSION = 1 STORAGE_EXPIRE_TIME = "expire_time" STORAGE_ACCESS_TOKEN = "access_token" @@ -49,10 +47,12 @@ class Auth: "grant_type": "authorization_code", "code": accept_grant_code, "client_id": self.client_id, - "client_secret": self.client_secret + "client_secret": self.client_secret, } - _LOGGER.debug("Calling LWA to get the access token (first time), " - "with: %s", json.dumps(lwa_params)) + _LOGGER.debug( + "Calling LWA to get the access token (first time), " "with: %s", + json.dumps(lwa_params), + ) return await self._async_request_new_token(lwa_params) @@ -74,7 +74,7 @@ class Auth: "grant_type": "refresh_token", "refresh_token": self._prefs[STORAGE_REFRESH_TOKEN], "client_id": self.client_id, - "client_secret": self.client_secret + "client_secret": self.client_secret, } _LOGGER.debug("Calling LWA to refresh the access token.") @@ -88,7 +88,8 @@ class Auth: expire_time = dt.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME]) preemptive_expire_time = expire_time - timedelta( - seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS) + seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS + ) return dt.utcnow() < preemptive_expire_time @@ -97,10 +98,12 @@ class Auth: try: session = aiohttp_client.async_get_clientsession(self.hass) with async_timeout.timeout(10): - response = await session.post(LWA_TOKEN_URI, - headers=LWA_HEADERS, - data=lwa_params, - allow_redirects=True) + response = await session.post( + LWA_TOKEN_URI, + headers=LWA_HEADERS, + data=lwa_params, + allow_redirects=True, + ) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout calling LWA to get auth token.") @@ -121,8 +124,9 @@ class Auth: expires_in = response_json["expires_in"] expire_time = dt.utcnow() + timedelta(seconds=expires_in) - await self._async_update_preferences(access_token, refresh_token, - expire_time.isoformat()) + await self._async_update_preferences( + access_token, refresh_token, expire_time.isoformat() + ) return access_token @@ -134,11 +138,10 @@ class Auth: self._prefs = { STORAGE_ACCESS_TOKEN: None, STORAGE_REFRESH_TOKEN: None, - STORAGE_EXPIRE_TIME: None + STORAGE_EXPIRE_TIME: None, } - async def _async_update_preferences(self, access_token, refresh_token, - expire_time): + async def _async_update_preferences(self, access_token, refresh_token, expire_time): """Update user preferences.""" if self._prefs is None: await self.async_load_preferences() diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 61fc7e82e32..dfb97cd9db2 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -13,11 +13,7 @@ from homeassistant.const import ( STATE_UNLOCKED, ) import homeassistant.components.climate.const as climate -from homeassistant.components import ( - light, - fan, - cover, -) +from homeassistant.components import light, fan, cover import homeassistant.util.color as color_util from .const import ( @@ -85,35 +81,35 @@ class AlexaCapibility: def serialize_discovery(self): """Serialize according to the Discovery API.""" result = { - 'type': 'AlexaInterface', - 'interface': self.name(), - 'version': '3', - 'properties': { - 'supported': self.properties_supported(), - 'proactivelyReported': self.properties_proactively_reported(), - 'retrievable': self.properties_retrievable(), + "type": "AlexaInterface", + "interface": self.name(), + "version": "3", + "properties": { + "supported": self.properties_supported(), + "proactivelyReported": self.properties_proactively_reported(), + "retrievable": self.properties_retrievable(), }, } # pylint: disable=assignment-from-none supports_deactivation = self.supports_deactivation() if supports_deactivation is not None: - result['supportsDeactivation'] = supports_deactivation + result["supportsDeactivation"] = supports_deactivation return result def serialize_properties(self): """Return properties serialized for an API response.""" for prop in self.properties_supported(): - prop_name = prop['name'] + prop_name = prop["name"] # pylint: disable=assignment-from-no-return prop_value = self.get_property(prop_name) if prop_value is not None: yield { - 'name': prop_name, - 'namespace': self.name(), - 'value': prop_value, - 'timeOfSample': datetime.now().strftime(DATE_FORMAT), - 'uncertaintyInMilliseconds': 0 + "name": prop_name, + "namespace": self.name(), + "value": prop_value, + "timeOfSample": datetime.now().strftime(DATE_FORMAT), + "uncertaintyInMilliseconds": 0, } @@ -130,11 +126,11 @@ class AlexaEndpointHealth(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.EndpointHealth' + return "Alexa.EndpointHealth" def properties_supported(self): """Return what properties this entity supports.""" - return [{'name': 'connectivity'}] + return [{"name": "connectivity"}] def properties_proactively_reported(self): """Return True if properties asynchronously reported.""" @@ -146,12 +142,12 @@ class AlexaEndpointHealth(AlexaCapibility): def get_property(self, name): """Read and return a property.""" - if name != 'connectivity': + if name != "connectivity": raise UnsupportedProperty(name) if self.entity.state == STATE_UNAVAILABLE: - return {'value': 'UNREACHABLE'} - return {'value': 'OK'} + return {"value": "UNREACHABLE"} + return {"value": "OK"} class AlexaPowerController(AlexaCapibility): @@ -162,11 +158,11 @@ class AlexaPowerController(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.PowerController' + return "Alexa.PowerController" def properties_supported(self): """Return what properties this entity supports.""" - return [{'name': 'powerState'}] + return [{"name": "powerState"}] def properties_proactively_reported(self): """Return True if properties asynchronously reported.""" @@ -178,7 +174,7 @@ class AlexaPowerController(AlexaCapibility): def get_property(self, name): """Read and return a property.""" - if name != 'powerState': + if name != "powerState": raise UnsupportedProperty(name) if self.entity.domain == climate.DOMAIN: @@ -187,7 +183,7 @@ class AlexaPowerController(AlexaCapibility): else: is_on = self.entity.state != STATE_OFF - return 'ON' if is_on else 'OFF' + return "ON" if is_on else "OFF" class AlexaLockController(AlexaCapibility): @@ -198,11 +194,11 @@ class AlexaLockController(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.LockController' + return "Alexa.LockController" def properties_supported(self): """Return what properties this entity supports.""" - return [{'name': 'lockState'}] + return [{"name": "lockState"}] def properties_retrievable(self): """Return True if properties can be retrieved.""" @@ -214,14 +210,14 @@ class AlexaLockController(AlexaCapibility): def get_property(self, name): """Read and return a property.""" - if name != 'lockState': + if name != "lockState": raise UnsupportedProperty(name) if self.entity.state == STATE_LOCKED: - return 'LOCKED' + return "LOCKED" if self.entity.state == STATE_UNLOCKED: - return 'UNLOCKED' - return 'JAMMED' + return "UNLOCKED" + return "JAMMED" class AlexaSceneController(AlexaCapibility): @@ -237,7 +233,7 @@ class AlexaSceneController(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.SceneController' + return "Alexa.SceneController" class AlexaBrightnessController(AlexaCapibility): @@ -248,11 +244,11 @@ class AlexaBrightnessController(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.BrightnessController' + return "Alexa.BrightnessController" def properties_supported(self): """Return what properties this entity supports.""" - return [{'name': 'brightness'}] + return [{"name": "brightness"}] def properties_proactively_reported(self): """Return True if properties asynchronously reported.""" @@ -264,10 +260,10 @@ class AlexaBrightnessController(AlexaCapibility): def get_property(self, name): """Read and return a property.""" - if name != 'brightness': + if name != "brightness": raise UnsupportedProperty(name) - if 'brightness' in self.entity.attributes: - return round(self.entity.attributes['brightness'] / 255.0 * 100) + if "brightness" in self.entity.attributes: + return round(self.entity.attributes["brightness"] / 255.0 * 100) return 0 @@ -279,11 +275,11 @@ class AlexaColorController(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.ColorController' + return "Alexa.ColorController" def properties_supported(self): """Return what properties this entity supports.""" - return [{'name': 'color'}] + return [{"name": "color"}] def properties_retrievable(self): """Return True if properties can be retrieved.""" @@ -291,17 +287,15 @@ class AlexaColorController(AlexaCapibility): def get_property(self, name): """Read and return a property.""" - if name != 'color': + if name != "color": raise UnsupportedProperty(name) - hue, saturation = self.entity.attributes.get( - light.ATTR_HS_COLOR, (0, 0)) + hue, saturation = self.entity.attributes.get(light.ATTR_HS_COLOR, (0, 0)) return { - 'hue': hue, - 'saturation': saturation / 100.0, - 'brightness': self.entity.attributes.get( - light.ATTR_BRIGHTNESS, 0) / 255.0, + "hue": hue, + "saturation": saturation / 100.0, + "brightness": self.entity.attributes.get(light.ATTR_BRIGHTNESS, 0) / 255.0, } @@ -313,11 +307,11 @@ class AlexaColorTemperatureController(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.ColorTemperatureController' + return "Alexa.ColorTemperatureController" def properties_supported(self): """Return what properties this entity supports.""" - return [{'name': 'colorTemperatureInKelvin'}] + return [{"name": "colorTemperatureInKelvin"}] def properties_retrievable(self): """Return True if properties can be retrieved.""" @@ -325,11 +319,12 @@ class AlexaColorTemperatureController(AlexaCapibility): def get_property(self, name): """Read and return a property.""" - if name != 'colorTemperatureInKelvin': + if name != "colorTemperatureInKelvin": raise UnsupportedProperty(name) - if 'color_temp' in self.entity.attributes: + if "color_temp" in self.entity.attributes: return color_util.color_temperature_mired_to_kelvin( - self.entity.attributes['color_temp']) + self.entity.attributes["color_temp"] + ) return 0 @@ -341,11 +336,11 @@ class AlexaPercentageController(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.PercentageController' + return "Alexa.PercentageController" def properties_supported(self): """Return what properties this entity supports.""" - return [{'name': 'percentage'}] + return [{"name": "percentage"}] def properties_retrievable(self): """Return True if properties can be retrieved.""" @@ -353,7 +348,7 @@ class AlexaPercentageController(AlexaCapibility): def get_property(self, name): """Read and return a property.""" - if name != 'percentage': + if name != "percentage": raise UnsupportedProperty(name) if self.entity.domain == fan.DOMAIN: @@ -375,7 +370,7 @@ class AlexaSpeaker(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.Speaker' + return "Alexa.Speaker" class AlexaStepSpeaker(AlexaCapibility): @@ -386,7 +381,7 @@ class AlexaStepSpeaker(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.StepSpeaker' + return "Alexa.StepSpeaker" class AlexaPlaybackController(AlexaCapibility): @@ -397,7 +392,7 @@ class AlexaPlaybackController(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.PlaybackController' + return "Alexa.PlaybackController" class AlexaInputController(AlexaCapibility): @@ -408,7 +403,7 @@ class AlexaInputController(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.InputController' + return "Alexa.InputController" class AlexaTemperatureSensor(AlexaCapibility): @@ -424,11 +419,11 @@ class AlexaTemperatureSensor(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.TemperatureSensor' + return "Alexa.TemperatureSensor" def properties_supported(self): """Return what properties this entity supports.""" - return [{'name': 'temperature'}] + return [{"name": "temperature"}] def properties_proactively_reported(self): """Return True if properties asynchronously reported.""" @@ -440,19 +435,15 @@ class AlexaTemperatureSensor(AlexaCapibility): def get_property(self, name): """Read and return a property.""" - if name != 'temperature': + if name != "temperature": raise UnsupportedProperty(name) unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) temp = self.entity.state if self.entity.domain == climate.DOMAIN: unit = self.hass.config.units.temperature_unit - temp = self.entity.attributes.get( - climate.ATTR_CURRENT_TEMPERATURE) - return { - 'value': float(temp), - 'scale': API_TEMP_UNITS[unit], - } + temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE) + return {"value": float(temp), "scale": API_TEMP_UNITS[unit]} class AlexaContactSensor(AlexaCapibility): @@ -473,11 +464,11 @@ class AlexaContactSensor(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.ContactSensor' + return "Alexa.ContactSensor" def properties_supported(self): """Return what properties this entity supports.""" - return [{'name': 'detectionState'}] + return [{"name": "detectionState"}] def properties_proactively_reported(self): """Return True if properties asynchronously reported.""" @@ -489,12 +480,12 @@ class AlexaContactSensor(AlexaCapibility): def get_property(self, name): """Read and return a property.""" - if name != 'detectionState': + if name != "detectionState": raise UnsupportedProperty(name) if self.entity.state == STATE_ON: - return 'DETECTED' - return 'NOT_DETECTED' + return "DETECTED" + return "NOT_DETECTED" class AlexaMotionSensor(AlexaCapibility): @@ -510,11 +501,11 @@ class AlexaMotionSensor(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.MotionSensor' + return "Alexa.MotionSensor" def properties_supported(self): """Return what properties this entity supports.""" - return [{'name': 'detectionState'}] + return [{"name": "detectionState"}] def properties_proactively_reported(self): """Return True if properties asynchronously reported.""" @@ -526,12 +517,12 @@ class AlexaMotionSensor(AlexaCapibility): def get_property(self, name): """Read and return a property.""" - if name != 'detectionState': + if name != "detectionState": raise UnsupportedProperty(name) if self.entity.state == STATE_ON: - return 'DETECTED' - return 'NOT_DETECTED' + return "DETECTED" + return "NOT_DETECTED" class AlexaThermostatController(AlexaCapibility): @@ -547,17 +538,17 @@ class AlexaThermostatController(AlexaCapibility): def name(self): """Return the Alexa API name of this interface.""" - return 'Alexa.ThermostatController' + return "Alexa.ThermostatController" def properties_supported(self): """Return what properties this entity supports.""" - properties = [{'name': 'thermostatMode'}] + properties = [{"name": "thermostatMode"}] supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & climate.SUPPORT_TARGET_TEMPERATURE: - properties.append({'name': 'targetSetpoint'}) + properties.append({"name": "targetSetpoint"}) if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: - properties.append({'name': 'lowerSetpoint'}) - properties.append({'name': 'upperSetpoint'}) + properties.append({"name": "lowerSetpoint"}) + properties.append({"name": "upperSetpoint"}) return properties def properties_proactively_reported(self): @@ -570,7 +561,7 @@ class AlexaThermostatController(AlexaCapibility): def get_property(self, name): """Read and return a property.""" - if name == 'thermostatMode': + if name == "thermostatMode": preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE) if preset in API_THERMOSTAT_PRESETS: @@ -580,17 +571,19 @@ class AlexaThermostatController(AlexaCapibility): if mode is None: _LOGGER.error( "%s (%s) has unsupported state value '%s'", - self.entity.entity_id, type(self.entity), - self.entity.state) + self.entity.entity_id, + type(self.entity), + self.entity.state, + ) raise UnsupportedProperty(name) return mode unit = self.hass.config.units.temperature_unit - if name == 'targetSetpoint': + if name == "targetSetpoint": temp = self.entity.attributes.get(ATTR_TEMPERATURE) - elif name == 'lowerSetpoint': + elif name == "lowerSetpoint": temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW) - elif name == 'upperSetpoint': + elif name == "upperSetpoint": temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH) else: raise UnsupportedProperty(name) @@ -598,7 +591,4 @@ class AlexaThermostatController(AlexaCapibility): if temp is None: return None - return { - 'value': float(temp), - 'scale': API_TEMP_UNITS[unit], - } + return {"value": float(temp), "scale": API_TEMP_UNITS[unit]} diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index aacf017f911..83c7da41c16 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -1,78 +1,68 @@ """Constants for the Alexa integration.""" from collections import OrderedDict -from homeassistant.const import ( - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.components.climate import const as climate from homeassistant.components import fan -DOMAIN = 'alexa' +DOMAIN = "alexa" # Flash briefing constants -CONF_UID = 'uid' -CONF_TITLE = 'title' -CONF_AUDIO = 'audio' -CONF_TEXT = 'text' -CONF_DISPLAY_URL = 'display_url' +CONF_UID = "uid" +CONF_TITLE = "title" +CONF_AUDIO = "audio" +CONF_TEXT = "text" +CONF_DISPLAY_URL = "display_url" -CONF_FILTER = 'filter' -CONF_ENTITY_CONFIG = 'entity_config' -CONF_ENDPOINT = 'endpoint' -CONF_CLIENT_ID = 'client_id' -CONF_CLIENT_SECRET = 'client_secret' +CONF_FILTER = "filter" +CONF_ENTITY_CONFIG = "entity_config" +CONF_ENDPOINT = "endpoint" +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" -ATTR_UID = 'uid' -ATTR_UPDATE_DATE = 'updateDate' -ATTR_TITLE_TEXT = 'titleText' -ATTR_STREAM_URL = 'streamUrl' -ATTR_MAIN_TEXT = 'mainText' -ATTR_REDIRECTION_URL = 'redirectionURL' +ATTR_UID = "uid" +ATTR_UPDATE_DATE = "updateDate" +ATTR_TITLE_TEXT = "titleText" +ATTR_STREAM_URL = "streamUrl" +ATTR_MAIN_TEXT = "mainText" +ATTR_REDIRECTION_URL = "redirectionURL" -SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH' +SYN_RESOLUTION_MATCH = "ER_SUCCESS_MATCH" -DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z' +DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.0Z" -API_DIRECTIVE = 'directive' -API_ENDPOINT = 'endpoint' -API_EVENT = 'event' -API_CONTEXT = 'context' -API_HEADER = 'header' -API_PAYLOAD = 'payload' -API_SCOPE = 'scope' -API_CHANGE = 'change' +API_DIRECTIVE = "directive" +API_ENDPOINT = "endpoint" +API_EVENT = "event" +API_CONTEXT = "context" +API_HEADER = "header" +API_PAYLOAD = "payload" +API_SCOPE = "scope" +API_CHANGE = "change" -CONF_DESCRIPTION = 'description' -CONF_DISPLAY_CATEGORIES = 'display_categories' +CONF_DESCRIPTION = "description" +CONF_DISPLAY_CATEGORIES = "display_categories" -API_TEMP_UNITS = { - TEMP_FAHRENHEIT: 'FAHRENHEIT', - TEMP_CELSIUS: 'CELSIUS', -} +API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"} # Needs to be ordered dict for `async_api_set_thermostat_mode` which does a -# reverse mapping of this dict and we want to map the first occurrance of OFF +# reverse mapping of this dict and we want to map the first occurrence of OFF # back to HA state. -API_THERMOSTAT_MODES = OrderedDict([ - (climate.HVAC_MODE_HEAT, 'HEAT'), - (climate.HVAC_MODE_COOL, 'COOL'), - (climate.HVAC_MODE_HEAT_COOL, 'AUTO'), - (climate.HVAC_MODE_AUTO, 'AUTO'), - (climate.HVAC_MODE_OFF, 'OFF'), - (climate.HVAC_MODE_FAN_ONLY, 'OFF'), - (climate.HVAC_MODE_DRY, 'OFF'), -]) -API_THERMOSTAT_PRESETS = { - climate.PRESET_ECO: 'ECO' -} +API_THERMOSTAT_MODES = OrderedDict( + [ + (climate.HVAC_MODE_HEAT, "HEAT"), + (climate.HVAC_MODE_COOL, "COOL"), + (climate.HVAC_MODE_HEAT_COOL, "AUTO"), + (climate.HVAC_MODE_AUTO, "AUTO"), + (climate.HVAC_MODE_OFF, "OFF"), + (climate.HVAC_MODE_FAN_ONLY, "OFF"), + (climate.HVAC_MODE_DRY, "OFF"), + ] +) +API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} -PERCENTAGE_FAN_MAP = { - fan.SPEED_LOW: 33, - fan.SPEED_MEDIUM: 66, - fan.SPEED_HIGH: 100, -} +PERCENTAGE_FAN_MAP = {fan.SPEED_LOW: 33, fan.SPEED_MEDIUM: 66, fan.SPEED_HIGH: 100} class Cause: @@ -84,25 +74,25 @@ class Cause: # Indicates that the event was caused by a customer interaction with an # application. For example, a customer switches on a light, or locks a door # using the Alexa app or an app provided by a device vendor. - APP_INTERACTION = 'APP_INTERACTION' + APP_INTERACTION = "APP_INTERACTION" # Indicates that the event was caused by a physical interaction with an # endpoint. For example manually switching on a light or manually locking a # door lock - PHYSICAL_INTERACTION = 'PHYSICAL_INTERACTION' + PHYSICAL_INTERACTION = "PHYSICAL_INTERACTION" # Indicates that the event was caused by the periodic poll of an appliance, # which found a change in value. For example, you might poll a temperature # sensor every hour, and send the updated temperature to Alexa. - PERIODIC_POLL = 'PERIODIC_POLL' + PERIODIC_POLL = "PERIODIC_POLL" # Indicates that the event was caused by the application of a device rule. # For example, a customer configures a rule to switch on a light if a # motion sensor detects motion. In this case, Alexa receives an event from # the motion sensor, and another event from the light to indicate that its # state change was caused by the rule. - RULE_TRIGGER = 'RULE_TRIGGER' + RULE_TRIGGER = "RULE_TRIGGER" # Indicates that the event was caused by a voice interaction with Alexa. # For example a user speaking to their Echo device. - VOICE_INTERACTION = 'VOICE_INTERACTION' + VOICE_INTERACTION = "VOICE_INTERACTION" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index c7f4fd9b7ea..b060d35be90 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -14,8 +14,21 @@ from homeassistant.const import ( from homeassistant.util.decorator import Registry from homeassistant.components.climate import const as climate from homeassistant.components import ( - alert, automation, binary_sensor, cover, fan, group, - input_boolean, light, lock, media_player, scene, script, sensor, switch) + alert, + automation, + binary_sensor, + cover, + fan, + group, + input_boolean, + light, + lock, + media_player, + scene, + script, + sensor, + switch, +) from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES from .capabilities import ( @@ -129,7 +142,7 @@ class AlexaEntity: def alexa_id(self): """Return the Alexa API entity id.""" - return self.entity.entity_id.replace('.', '#') + return self.entity.entity_id.replace(".", "#") def display_categories(self): """Return a list of display categories.""" @@ -171,15 +184,13 @@ class AlexaEntity: def serialize_discovery(self): """Serialize the entity for discovery.""" return { - 'displayCategories': self.display_categories(), - 'cookie': {}, - 'endpointId': self.alexa_id(), - 'friendlyName': self.friendly_name(), - 'description': self.description(), - 'manufacturerName': 'Home Assistant', - 'capabilities': [ - i.serialize_discovery() for i in self.interfaces() - ] + "displayCategories": self.display_categories(), + "cookie": {}, + "endpointId": self.alexa_id(), + "friendlyName": self.friendly_name(), + "description": self.description(), + "manufacturerName": "Home Assistant", + "capabilities": [i.serialize_discovery() for i in self.interfaces()], } @@ -220,8 +231,10 @@ class GenericCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" - return [AlexaPowerController(self.entity), - AlexaEndpointHealth(self.hass, self.entity)] + return [ + AlexaPowerController(self.entity), + AlexaEndpointHealth(self.hass, self.entity), + ] @ENTITY_ADAPTERS.register(switch.DOMAIN) @@ -234,8 +247,10 @@ class SwitchCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" - return [AlexaPowerController(self.entity), - AlexaEndpointHealth(self.hass, self.entity)] + return [ + AlexaPowerController(self.entity), + AlexaEndpointHealth(self.hass, self.entity), + ] @ENTITY_ADAPTERS.register(climate.DOMAIN) @@ -249,8 +264,7 @@ class ClimateCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" # If we support two modes, one being off, we allow turning on too. - if (climate.HVAC_MODE_OFF in - self.entity.attributes[climate.ATTR_HVAC_MODES]): + if climate.HVAC_MODE_OFF in self.entity.attributes[climate.ATTR_HVAC_MODES]: yield AlexaPowerController(self.entity) yield AlexaThermostatController(self.hass, self.entity) @@ -324,8 +338,10 @@ class LockCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" - return [AlexaLockController(self.entity), - AlexaEndpointHealth(self.hass, self.entity)] + return [ + AlexaLockController(self.entity), + AlexaEndpointHealth(self.hass, self.entity), + ] @ENTITY_ADAPTERS.register(media_player.const.DOMAIN) @@ -345,16 +361,20 @@ class MediaPlayerCapabilities(AlexaEntity): if supported & media_player.const.SUPPORT_VOLUME_SET: yield AlexaSpeaker(self.entity) - step_volume_features = (media_player.const.SUPPORT_VOLUME_MUTE | - media_player.const.SUPPORT_VOLUME_STEP) + step_volume_features = ( + media_player.const.SUPPORT_VOLUME_MUTE + | media_player.const.SUPPORT_VOLUME_STEP + ) if supported & step_volume_features: yield AlexaStepSpeaker(self.entity) - playback_features = (media_player.const.SUPPORT_PLAY | - media_player.const.SUPPORT_PAUSE | - media_player.const.SUPPORT_STOP | - media_player.const.SUPPORT_NEXT_TRACK | - media_player.const.SUPPORT_PREVIOUS_TRACK) + playback_features = ( + media_player.const.SUPPORT_PLAY + | media_player.const.SUPPORT_PAUSE + | media_player.const.SUPPORT_STOP + | media_player.const.SUPPORT_NEXT_TRACK + | media_player.const.SUPPORT_PREVIOUS_TRACK + ) if supported & playback_features: yield AlexaPlaybackController(self.entity) @@ -369,7 +389,7 @@ class SceneCapabilities(AlexaEntity): def description(self): """Return the description of the entity.""" # Required description as per Amazon Scene docs - scene_fmt = '{} (Scene connected via Home Assistant)' + scene_fmt = "{} (Scene connected via Home Assistant)" return scene_fmt.format(AlexaEntity.description(self)) def default_display_categories(self): @@ -378,8 +398,7 @@ class SceneCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" - return [AlexaSceneController(self.entity, - supports_deactivation=False)] + return [AlexaSceneController(self.entity, supports_deactivation=False)] @ENTITY_ADAPTERS.register(script.DOMAIN) @@ -392,9 +411,8 @@ class ScriptCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" - can_cancel = bool(self.entity.attributes.get('can_cancel')) - return [AlexaSceneController(self.entity, - supports_deactivation=can_cancel)] + can_cancel = bool(self.entity.attributes.get("can_cancel")) + return [AlexaSceneController(self.entity, supports_deactivation=can_cancel)] @ENTITY_ADAPTERS.register(sensor.DOMAIN) @@ -410,10 +428,7 @@ class SensorCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" attrs = self.entity.attributes - if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in ( - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - ): + if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_FAHRENHEIT, TEMP_CELSIUS): yield AlexaTemperatureSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) @@ -422,8 +437,8 @@ class SensorCapabilities(AlexaEntity): class BinarySensorCapabilities(AlexaEntity): """Class to represent BinarySensor capabilities.""" - TYPE_CONTACT = 'contact' - TYPE_MOTION = 'motion' + TYPE_CONTACT = "contact" + TYPE_MOTION = "motion" def default_display_categories(self): """Return the display categories for this entity.""" @@ -446,12 +461,7 @@ class BinarySensorCapabilities(AlexaEntity): def get_type(self): """Return the type of binary sensor.""" attrs = self.entity.attributes - if attrs.get(ATTR_DEVICE_CLASS) in ( - 'door', - 'garage_door', - 'opening', - 'window', - ): + if attrs.get(ATTR_DEVICE_CLASS) in ("door", "garage_door", "opening", "window"): return self.TYPE_CONTACT - if attrs.get(ATTR_DEVICE_CLASS) == 'motion': + if attrs.get(ATTR_DEVICE_CLASS) == "motion": return self.TYPE_MOTION diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 76ec92edf8d..56202b23e20 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -35,12 +35,12 @@ class AlexaError(Exception): class AlexaInvalidEndpointError(AlexaError): """The endpoint in the request does not exist.""" - namespace = 'Alexa' - error_type = 'NO_SUCH_ENDPOINT' + namespace = "Alexa" + error_type = "NO_SUCH_ENDPOINT" def __init__(self, endpoint_id): """Initialize invalid endpoint error.""" - msg = 'The endpoint {} does not exist'.format(endpoint_id) + msg = "The endpoint {} does not exist".format(endpoint_id) AlexaError.__init__(self, msg) self.endpoint_id = endpoint_id @@ -48,38 +48,32 @@ class AlexaInvalidEndpointError(AlexaError): class AlexaInvalidValueError(AlexaError): """Class to represent InvalidValue errors.""" - namespace = 'Alexa' - error_type = 'INVALID_VALUE' + namespace = "Alexa" + error_type = "INVALID_VALUE" class AlexaUnsupportedThermostatModeError(AlexaError): """Class to represent UnsupportedThermostatMode errors.""" - namespace = 'Alexa.ThermostatController' - error_type = 'UNSUPPORTED_THERMOSTAT_MODE' + namespace = "Alexa.ThermostatController" + error_type = "UNSUPPORTED_THERMOSTAT_MODE" class AlexaTempRangeError(AlexaError): """Class to represent TempRange errors.""" - namespace = 'Alexa' - error_type = 'TEMPERATURE_VALUE_OUT_OF_RANGE' + namespace = "Alexa" + error_type = "TEMPERATURE_VALUE_OUT_OF_RANGE" def __init__(self, hass, temp, min_temp, max_temp): """Initialize TempRange error.""" unit = hass.config.units.temperature_unit temp_range = { - 'minimumValue': { - 'value': min_temp, - 'scale': API_TEMP_UNITS[unit], - }, - 'maximumValue': { - 'value': max_temp, - 'scale': API_TEMP_UNITS[unit], - }, + "minimumValue": {"value": min_temp, "scale": API_TEMP_UNITS[unit]}, + "maximumValue": {"value": max_temp, "scale": API_TEMP_UNITS[unit]}, } - payload = {'validRange': temp_range} - msg = 'The requested temperature {} is out of range'.format(temp) + payload = {"validRange": temp_range} + msg = "The requested temperature {} is out of range".format(temp) AlexaError.__init__(self, msg, payload) @@ -87,5 +81,5 @@ class AlexaTempRangeError(AlexaError): class AlexaBridgeUnreachableError(AlexaError): """Class to represent BridgeUnreachable errors.""" - namespace = 'Alexa' - error_type = 'BRIDGE_UNREACHABLE' + namespace = "Alexa" + error_type = "BRIDGE_UNREACHABLE" diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 537f04b20be..708d1592e4c 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -9,27 +9,36 @@ from homeassistant.core import callback from homeassistant.helpers import template from .const import ( - ATTR_MAIN_TEXT, ATTR_REDIRECTION_URL, ATTR_STREAM_URL, ATTR_TITLE_TEXT, - ATTR_UID, ATTR_UPDATE_DATE, CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, - CONF_TITLE, CONF_UID, DATE_FORMAT) + ATTR_MAIN_TEXT, + ATTR_REDIRECTION_URL, + ATTR_STREAM_URL, + ATTR_TITLE_TEXT, + ATTR_UID, + ATTR_UPDATE_DATE, + CONF_AUDIO, + CONF_DISPLAY_URL, + CONF_TEXT, + CONF_TITLE, + CONF_UID, + DATE_FORMAT, +) _LOGGER = logging.getLogger(__name__) -FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}' +FLASH_BRIEFINGS_API_ENDPOINT = "/api/alexa/flash_briefings/{briefing_id}" @callback def async_setup(hass, flash_briefing_config): """Activate Alexa component.""" - hass.http.register_view( - AlexaFlashBriefingView(hass, flash_briefing_config)) + hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config)) class AlexaFlashBriefingView(http.HomeAssistantView): """Handle Alexa Flash Briefing skill requests.""" url = FLASH_BRIEFINGS_API_ENDPOINT - name = 'api:alexa:flash_briefings' + name = "api:alexa:flash_briefings" def __init__(self, hass, flash_briefings): """Initialize Alexa view.""" @@ -40,13 +49,12 @@ class AlexaFlashBriefingView(http.HomeAssistantView): @callback def get(self, request, briefing_id): """Handle Alexa Flash Briefing request.""" - _LOGGER.debug("Received Alexa flash briefing request for: %s", - briefing_id) + _LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id) if self.flash_briefings.get(briefing_id) is None: err = "No configured Alexa flash briefing was found for: %s" _LOGGER.error(err, briefing_id) - return b'', 404 + return b"", 404 briefing = [] @@ -76,10 +84,8 @@ class AlexaFlashBriefingView(http.HomeAssistantView): output[ATTR_STREAM_URL] = item.get(CONF_AUDIO) if item.get(CONF_DISPLAY_URL) is not None: - if isinstance(item.get(CONF_DISPLAY_URL), - template.Template): - output[ATTR_REDIRECTION_URL] = \ - item[CONF_DISPLAY_URL].async_render() + if isinstance(item.get(CONF_DISPLAY_URL), template.Template): + output[ATTR_REDIRECTION_URL] = item[CONF_DISPLAY_URL].async_render() else: output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index b66fbf82c0f..cd5b56d60e2 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -7,29 +7,44 @@ from homeassistant import core as ha from homeassistant.components import cover, fan, group, light, media_player from homeassistant.components.climate import const as climate from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, SERVICE_LOCK, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, - SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, - SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_UNLOCK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, - SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + SERVICE_LOCK, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_STOP, + SERVICE_SET_COVER_POSITION, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_UNLOCK, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) import homeassistant.util.color as color_util from homeassistant.util.decorator import Registry from homeassistant.util.temperature import convert as convert_temperature -from .const import ( - API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause) +from .const import API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause from .entities import async_get_entities from .errors import ( - AlexaInvalidValueError, AlexaTempRangeError, - AlexaUnsupportedThermostatModeError) + AlexaInvalidValueError, + AlexaTempRangeError, + AlexaUnsupportedThermostatModeError, +) from .state_report import async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) HANDLERS = Registry() -@HANDLERS.register(('Alexa.Discovery', 'Discover')) +@HANDLERS.register(("Alexa.Discovery", "Discover")) async def async_api_discovery(hass, config, directive, context): """Create a API formatted discovery response. @@ -42,19 +57,19 @@ async def async_api_discovery(hass, config, directive, context): ] return directive.response( - name='Discover.Response', - namespace='Alexa.Discovery', - payload={'endpoints': discovery_endpoints}, + name="Discover.Response", + namespace="Alexa.Discovery", + payload={"endpoints": discovery_endpoints}, ) -@HANDLERS.register(('Alexa.Authorization', 'AcceptGrant')) +@HANDLERS.register(("Alexa.Authorization", "AcceptGrant")) async def async_api_accept_grant(hass, config, directive, context): """Create a API formatted AcceptGrant response. Async friendly. """ - auth_code = directive.payload['grant']['code'] + auth_code = directive.payload["grant"]["code"] _LOGGER.debug("AcceptGrant code: %s", auth_code) if config.supports_auth: @@ -64,12 +79,11 @@ async def async_api_accept_grant(hass, config, directive, context): await async_enable_proactive_mode(hass, config) return directive.response( - name='AcceptGrant.Response', - namespace='Alexa.Authorization', - payload={}) + name="AcceptGrant.Response", namespace="Alexa.Authorization", payload={} + ) -@HANDLERS.register(('Alexa.PowerController', 'TurnOn')) +@HANDLERS.register(("Alexa.PowerController", "TurnOn")) async def async_api_turn_on(hass, config, directive, context): """Process a turn on request.""" entity = directive.entity @@ -82,19 +96,22 @@ async def async_api_turn_on(hass, config, directive, context): service = cover.SERVICE_OPEN_COVER elif domain == media_player.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - power_features = (media_player.SUPPORT_TURN_ON | - media_player.SUPPORT_TURN_OFF) + power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF if not supported & power_features: service = media_player.SERVICE_MEDIA_PLAY - await hass.services.async_call(domain, service, { - ATTR_ENTITY_ID: entity.entity_id - }, blocking=False, context=context) + await hass.services.async_call( + domain, + service, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) return directive.response() -@HANDLERS.register(('Alexa.PowerController', 'TurnOff')) +@HANDLERS.register(("Alexa.PowerController", "TurnOff")) async def async_api_turn_off(hass, config, directive, context): """Process a turn off request.""" entity = directive.entity @@ -107,89 +124,104 @@ async def async_api_turn_off(hass, config, directive, context): service = cover.SERVICE_CLOSE_COVER elif domain == media_player.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - power_features = (media_player.SUPPORT_TURN_ON | - media_player.SUPPORT_TURN_OFF) + power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF if not supported & power_features: service = media_player.SERVICE_MEDIA_STOP - await hass.services.async_call(domain, service, { - ATTR_ENTITY_ID: entity.entity_id - }, blocking=False, context=context) + await hass.services.async_call( + domain, + service, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) return directive.response() -@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness')) +@HANDLERS.register(("Alexa.BrightnessController", "SetBrightness")) async def async_api_set_brightness(hass, config, directive, context): """Process a set brightness request.""" entity = directive.entity - brightness = int(directive.payload['brightness']) + brightness = int(directive.payload["brightness"]) - await hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_BRIGHTNESS_PCT: brightness, - }, blocking=False, context=context) + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness}, + blocking=False, + context=context, + ) return directive.response() -@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness')) +@HANDLERS.register(("Alexa.BrightnessController", "AdjustBrightness")) async def async_api_adjust_brightness(hass, config, directive, context): """Process an adjust brightness request.""" entity = directive.entity - brightness_delta = int(directive.payload['brightnessDelta']) + brightness_delta = int(directive.payload["brightnessDelta"]) # read current state try: current = math.floor( - int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100) + int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100 + ) except ZeroDivisionError: current = 0 # set brightness brightness = max(0, brightness_delta + current) - await hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_BRIGHTNESS_PCT: brightness, - }, blocking=False, context=context) + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness}, + blocking=False, + context=context, + ) return directive.response() -@HANDLERS.register(('Alexa.ColorController', 'SetColor')) +@HANDLERS.register(("Alexa.ColorController", "SetColor")) async def async_api_set_color(hass, config, directive, context): """Process a set color request.""" entity = directive.entity rgb = color_util.color_hsb_to_RGB( - float(directive.payload['color']['hue']), - float(directive.payload['color']['saturation']), - float(directive.payload['color']['brightness']) + float(directive.payload["color"]["hue"]), + float(directive.payload["color"]["saturation"]), + float(directive.payload["color"]["brightness"]), ) - await hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_RGB_COLOR: rgb, - }, blocking=False, context=context) + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_RGB_COLOR: rgb}, + blocking=False, + context=context, + ) return directive.response() -@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature')) +@HANDLERS.register(("Alexa.ColorTemperatureController", "SetColorTemperature")) async def async_api_set_color_temperature(hass, config, directive, context): """Process a set color temperature request.""" entity = directive.entity - kelvin = int(directive.payload['colorTemperatureInKelvin']) + kelvin = int(directive.payload["colorTemperatureInKelvin"]) - await hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_KELVIN: kelvin, - }, blocking=False, context=context) + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_KELVIN: kelvin}, + blocking=False, + context=context, + ) return directive.response() -@HANDLERS.register( - ('Alexa.ColorTemperatureController', 'DecreaseColorTemperature')) +@HANDLERS.register(("Alexa.ColorTemperatureController", "DecreaseColorTemperature")) async def async_api_decrease_color_temp(hass, config, directive, context): """Process a decrease color temperature request.""" entity = directive.entity @@ -197,16 +229,18 @@ async def async_api_decrease_color_temp(hass, config, directive, context): max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS)) value = min(max_mireds, current + 50) - await hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_COLOR_TEMP: value, - }, blocking=False, context=context) + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value}, + blocking=False, + context=context, + ) return directive.response() -@HANDLERS.register( - ('Alexa.ColorTemperatureController', 'IncreaseColorTemperature')) +@HANDLERS.register(("Alexa.ColorTemperatureController", "IncreaseColorTemperature")) async def async_api_increase_color_temp(hass, config, directive, context): """Process an increase color temperature request.""" entity = directive.entity @@ -214,63 +248,70 @@ async def async_api_increase_color_temp(hass, config, directive, context): min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS)) value = max(min_mireds, current - 50) - await hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_COLOR_TEMP: value, - }, blocking=False, context=context) + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value}, + blocking=False, + context=context, + ) return directive.response() -@HANDLERS.register(('Alexa.SceneController', 'Activate')) +@HANDLERS.register(("Alexa.SceneController", "Activate")) async def async_api_activate(hass, config, directive, context): """Process an activate request.""" entity = directive.entity domain = entity.domain - await hass.services.async_call(domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id - }, blocking=False, context=context) + await hass.services.async_call( + domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) payload = { - 'cause': {'type': Cause.VOICE_INTERACTION}, - 'timestamp': '%sZ' % (datetime.utcnow().isoformat(),) + "cause": {"type": Cause.VOICE_INTERACTION}, + "timestamp": "%sZ" % (datetime.utcnow().isoformat(),), } return directive.response( - name='ActivationStarted', - namespace='Alexa.SceneController', - payload=payload, + name="ActivationStarted", namespace="Alexa.SceneController", payload=payload ) -@HANDLERS.register(('Alexa.SceneController', 'Deactivate')) +@HANDLERS.register(("Alexa.SceneController", "Deactivate")) async def async_api_deactivate(hass, config, directive, context): """Process a deactivate request.""" entity = directive.entity domain = entity.domain - await hass.services.async_call(domain, SERVICE_TURN_OFF, { - ATTR_ENTITY_ID: entity.entity_id - }, blocking=False, context=context) + await hass.services.async_call( + domain, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) payload = { - 'cause': {'type': Cause.VOICE_INTERACTION}, - 'timestamp': '%sZ' % (datetime.utcnow().isoformat(),) + "cause": {"type": Cause.VOICE_INTERACTION}, + "timestamp": "%sZ" % (datetime.utcnow().isoformat(),), } return directive.response( - name='DeactivationStarted', - namespace='Alexa.SceneController', - payload=payload, + name="DeactivationStarted", namespace="Alexa.SceneController", payload=payload ) -@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage')) +@HANDLERS.register(("Alexa.PercentageController", "SetPercentage")) async def async_api_set_percentage(hass, config, directive, context): """Process a set percentage request.""" entity = directive.entity - percentage = int(directive.payload['percentage']) + percentage = int(directive.payload["percentage"]) service = None data = {ATTR_ENTITY_ID: entity.entity_id} @@ -291,16 +332,17 @@ async def async_api_set_percentage(hass, config, directive, context): data[cover.ATTR_POSITION] = percentage await hass.services.async_call( - entity.domain, service, data, blocking=False, context=context) + entity.domain, service, data, blocking=False, context=context + ) return directive.response() -@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage')) +@HANDLERS.register(("Alexa.PercentageController", "AdjustPercentage")) async def async_api_adjust_percentage(hass, config, directive, context): """Process an adjust percentage request.""" entity = directive.entity - percentage_delta = int(directive.payload['percentageDelta']) + percentage_delta = int(directive.payload["percentageDelta"]) service = None data = {ATTR_ENTITY_ID: entity.entity_id} @@ -338,44 +380,51 @@ async def async_api_adjust_percentage(hass, config, directive, context): data[cover.ATTR_POSITION] = max(0, percentage_delta + current) await hass.services.async_call( - entity.domain, service, data, blocking=False, context=context) + entity.domain, service, data, blocking=False, context=context + ) return directive.response() -@HANDLERS.register(('Alexa.LockController', 'Lock')) +@HANDLERS.register(("Alexa.LockController", "Lock")) async def async_api_lock(hass, config, directive, context): """Process a lock request.""" entity = directive.entity - await hass.services.async_call(entity.domain, SERVICE_LOCK, { - ATTR_ENTITY_ID: entity.entity_id - }, blocking=False, context=context) + await hass.services.async_call( + entity.domain, + SERVICE_LOCK, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) response = directive.response() - response.add_context_property({ - 'name': 'lockState', - 'namespace': 'Alexa.LockController', - 'value': 'LOCKED' - }) + response.add_context_property( + {"name": "lockState", "namespace": "Alexa.LockController", "value": "LOCKED"} + ) return response # Not supported by Alexa yet -@HANDLERS.register(('Alexa.LockController', 'Unlock')) +@HANDLERS.register(("Alexa.LockController", "Unlock")) async def async_api_unlock(hass, config, directive, context): """Process an unlock request.""" entity = directive.entity - await hass.services.async_call(entity.domain, SERVICE_UNLOCK, { - ATTR_ENTITY_ID: entity.entity_id - }, blocking=False, context=context) + await hass.services.async_call( + entity.domain, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) return directive.response() -@HANDLERS.register(('Alexa.Speaker', 'SetVolume')) +@HANDLERS.register(("Alexa.Speaker", "SetVolume")) async def async_api_set_volume(hass, config, directive, context): """Process a set volume request.""" - volume = round(float(directive.payload['volume'] / 100), 2) + volume = round(float(directive.payload["volume"] / 100), 2) entity = directive.entity data = { @@ -384,31 +433,31 @@ async def async_api_set_volume(hass, config, directive, context): } await hass.services.async_call( - entity.domain, SERVICE_VOLUME_SET, - data, blocking=False, context=context) + entity.domain, SERVICE_VOLUME_SET, data, blocking=False, context=context + ) return directive.response() -@HANDLERS.register(('Alexa.InputController', 'SelectInput')) +@HANDLERS.register(("Alexa.InputController", "SelectInput")) async def async_api_select_input(hass, config, directive, context): """Process a set input request.""" - media_input = directive.payload['input'] + media_input = directive.payload["input"] entity = directive.entity # attempt to map the ALL UPPERCASE payload name to a source - source_list = entity.attributes[ - media_player.const.ATTR_INPUT_SOURCE_LIST] or [] + source_list = entity.attributes[media_player.const.ATTR_INPUT_SOURCE_LIST] or [] for source in source_list: # response will always be space separated, so format the source in the # most likely way to find a match - formatted_source = source.lower().replace('-', ' ').replace('_', ' ') + formatted_source = source.lower().replace("-", " ").replace("_", " ") if formatted_source in media_input.lower(): media_input = source break else: - msg = 'failed to map input {} to a media source on {}'.format( - media_input, entity.entity_id) + msg = "failed to map input {} to a media source on {}".format( + media_input, entity.entity_id + ) raise AlexaInvalidValueError(msg) data = { @@ -417,20 +466,23 @@ async def async_api_select_input(hass, config, directive, context): } await hass.services.async_call( - entity.domain, media_player.SERVICE_SELECT_SOURCE, - data, blocking=False, context=context) + entity.domain, + media_player.SERVICE_SELECT_SOURCE, + data, + blocking=False, + context=context, + ) return directive.response() -@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume')) +@HANDLERS.register(("Alexa.Speaker", "AdjustVolume")) async def async_api_adjust_volume(hass, config, directive, context): """Process an adjust volume request.""" - volume_delta = int(directive.payload['volume']) + volume_delta = int(directive.payload["volume"]) entity = directive.entity - current_level = entity.attributes.get( - media_player.const.ATTR_MEDIA_VOLUME_LEVEL) + current_level = entity.attributes.get(media_player.const.ATTR_MEDIA_VOLUME_LEVEL) # read current state try: @@ -446,43 +498,41 @@ async def async_api_adjust_volume(hass, config, directive, context): } await hass.services.async_call( - entity.domain, SERVICE_VOLUME_SET, - data, blocking=False, context=context) + entity.domain, SERVICE_VOLUME_SET, data, blocking=False, context=context + ) return directive.response() -@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume')) +@HANDLERS.register(("Alexa.StepSpeaker", "AdjustVolume")) async def async_api_adjust_volume_step(hass, config, directive, context): """Process an adjust volume step request.""" # media_player volume up/down service does not support specifying steps # each component handles it differently e.g. via config. # For now we use the volumeSteps returned to figure out if we # should step up/down - volume_step = directive.payload['volumeSteps'] + volume_step = directive.payload["volumeSteps"] entity = directive.entity - data = { - ATTR_ENTITY_ID: entity.entity_id, - } + data = {ATTR_ENTITY_ID: entity.entity_id} if volume_step > 0: await hass.services.async_call( - entity.domain, SERVICE_VOLUME_UP, - data, blocking=False, context=context) + entity.domain, SERVICE_VOLUME_UP, data, blocking=False, context=context + ) elif volume_step < 0: await hass.services.async_call( - entity.domain, SERVICE_VOLUME_DOWN, - data, blocking=False, context=context) + entity.domain, SERVICE_VOLUME_DOWN, data, blocking=False, context=context + ) return directive.response() -@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute')) -@HANDLERS.register(('Alexa.Speaker', 'SetMute')) +@HANDLERS.register(("Alexa.StepSpeaker", "SetMute")) +@HANDLERS.register(("Alexa.Speaker", "SetMute")) async def async_api_set_mute(hass, config, directive, context): """Process a set mute request.""" - mute = bool(directive.payload['mute']) + mute = bool(directive.payload["mute"]) entity = directive.entity data = { @@ -491,83 +541,77 @@ async def async_api_set_mute(hass, config, directive, context): } await hass.services.async_call( - entity.domain, SERVICE_VOLUME_MUTE, - data, blocking=False, context=context) + entity.domain, SERVICE_VOLUME_MUTE, data, blocking=False, context=context + ) return directive.response() -@HANDLERS.register(('Alexa.PlaybackController', 'Play')) +@HANDLERS.register(("Alexa.PlaybackController", "Play")) async def async_api_play(hass, config, directive, context): """Process a play request.""" entity = directive.entity - data = { - ATTR_ENTITY_ID: entity.entity_id - } + data = {ATTR_ENTITY_ID: entity.entity_id} await hass.services.async_call( - entity.domain, SERVICE_MEDIA_PLAY, - data, blocking=False, context=context) + entity.domain, SERVICE_MEDIA_PLAY, data, blocking=False, context=context + ) return directive.response() -@HANDLERS.register(('Alexa.PlaybackController', 'Pause')) +@HANDLERS.register(("Alexa.PlaybackController", "Pause")) async def async_api_pause(hass, config, directive, context): """Process a pause request.""" entity = directive.entity - data = { - ATTR_ENTITY_ID: entity.entity_id - } + data = {ATTR_ENTITY_ID: entity.entity_id} await hass.services.async_call( - entity.domain, SERVICE_MEDIA_PAUSE, - data, blocking=False, context=context) + entity.domain, SERVICE_MEDIA_PAUSE, data, blocking=False, context=context + ) return directive.response() -@HANDLERS.register(('Alexa.PlaybackController', 'Stop')) +@HANDLERS.register(("Alexa.PlaybackController", "Stop")) async def async_api_stop(hass, config, directive, context): """Process a stop request.""" entity = directive.entity - data = { - ATTR_ENTITY_ID: entity.entity_id - } + data = {ATTR_ENTITY_ID: entity.entity_id} await hass.services.async_call( - entity.domain, SERVICE_MEDIA_STOP, - data, blocking=False, context=context) + entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context + ) return directive.response() -@HANDLERS.register(('Alexa.PlaybackController', 'Next')) +@HANDLERS.register(("Alexa.PlaybackController", "Next")) async def async_api_next(hass, config, directive, context): """Process a next request.""" entity = directive.entity - data = { - ATTR_ENTITY_ID: entity.entity_id - } + data = {ATTR_ENTITY_ID: entity.entity_id} await hass.services.async_call( - entity.domain, SERVICE_MEDIA_NEXT_TRACK, - data, blocking=False, context=context) + entity.domain, SERVICE_MEDIA_NEXT_TRACK, data, blocking=False, context=context + ) return directive.response() -@HANDLERS.register(('Alexa.PlaybackController', 'Previous')) +@HANDLERS.register(("Alexa.PlaybackController", "Previous")) async def async_api_previous(hass, config, directive, context): """Process a previous request.""" entity = directive.entity - data = { - ATTR_ENTITY_ID: entity.entity_id - } + data = {ATTR_ENTITY_ID: entity.entity_id} await hass.services.async_call( - entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK, - data, blocking=False, context=context) + entity.domain, + SERVICE_MEDIA_PREVIOUS_TRACK, + data, + blocking=False, + context=context, + ) return directive.response() @@ -576,11 +620,11 @@ def temperature_from_object(hass, temp_obj, interval=False): """Get temperature from Temperature object in requested unit.""" to_unit = hass.config.units.temperature_unit from_unit = TEMP_CELSIUS - temp = float(temp_obj['value']) + temp = float(temp_obj["value"]) - if temp_obj['scale'] == 'FAHRENHEIT': + if temp_obj["scale"] == "FAHRENHEIT": from_unit = TEMP_FAHRENHEIT - elif temp_obj['scale'] == 'KELVIN': + elif temp_obj["scale"] == "KELVIN": # convert to Celsius if absolute temperature if not interval: temp -= 273.15 @@ -588,7 +632,7 @@ def temperature_from_object(hass, temp_obj, interval=False): return convert_temperature(temp, from_unit, to_unit, interval) -@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature')) +@HANDLERS.register(("Alexa.ThermostatController", "SetTargetTemperature")) async def async_api_set_target_temp(hass, config, directive, context): """Process a set target temperature request.""" entity = directive.entity @@ -596,51 +640,59 @@ async def async_api_set_target_temp(hass, config, directive, context): max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) unit = hass.config.units.temperature_unit - data = { - ATTR_ENTITY_ID: entity.entity_id - } + data = {ATTR_ENTITY_ID: entity.entity_id} payload = directive.payload response = directive.response() - if 'targetSetpoint' in payload: - temp = temperature_from_object(hass, payload['targetSetpoint']) + if "targetSetpoint" in payload: + temp = temperature_from_object(hass, payload["targetSetpoint"]) if temp < min_temp or temp > max_temp: raise AlexaTempRangeError(hass, temp, min_temp, max_temp) data[ATTR_TEMPERATURE] = temp - response.add_context_property({ - 'name': 'targetSetpoint', - 'namespace': 'Alexa.ThermostatController', - 'value': {'value': temp, 'scale': API_TEMP_UNITS[unit]}, - }) - if 'lowerSetpoint' in payload: - temp_low = temperature_from_object(hass, payload['lowerSetpoint']) + response.add_context_property( + { + "name": "targetSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": temp, "scale": API_TEMP_UNITS[unit]}, + } + ) + if "lowerSetpoint" in payload: + temp_low = temperature_from_object(hass, payload["lowerSetpoint"]) if temp_low < min_temp or temp_low > max_temp: raise AlexaTempRangeError(hass, temp_low, min_temp, max_temp) data[climate.ATTR_TARGET_TEMP_LOW] = temp_low - response.add_context_property({ - 'name': 'lowerSetpoint', - 'namespace': 'Alexa.ThermostatController', - 'value': {'value': temp_low, 'scale': API_TEMP_UNITS[unit]}, - }) - if 'upperSetpoint' in payload: - temp_high = temperature_from_object(hass, payload['upperSetpoint']) + response.add_context_property( + { + "name": "lowerSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": temp_low, "scale": API_TEMP_UNITS[unit]}, + } + ) + if "upperSetpoint" in payload: + temp_high = temperature_from_object(hass, payload["upperSetpoint"]) if temp_high < min_temp or temp_high > max_temp: raise AlexaTempRangeError(hass, temp_high, min_temp, max_temp) data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high - response.add_context_property({ - 'name': 'upperSetpoint', - 'namespace': 'Alexa.ThermostatController', - 'value': {'value': temp_high, 'scale': API_TEMP_UNITS[unit]}, - }) + response.add_context_property( + { + "name": "upperSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": temp_high, "scale": API_TEMP_UNITS[unit]}, + } + ) await hass.services.async_call( - entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False, - context=context) + entity.domain, + climate.SERVICE_SET_TEMPERATURE, + data, + blocking=False, + context=context, + ) return response -@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature')) +@HANDLERS.register(("Alexa.ThermostatController", "AdjustTargetTemperature")) async def async_api_adjust_target_temp(hass, config, directive, context): """Process an adjust target temperature request.""" entity = directive.entity @@ -649,53 +701,50 @@ async def async_api_adjust_target_temp(hass, config, directive, context): unit = hass.config.units.temperature_unit temp_delta = temperature_from_object( - hass, directive.payload['targetSetpointDelta'], interval=True) + hass, directive.payload["targetSetpointDelta"], interval=True + ) target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta if target_temp < min_temp or target_temp > max_temp: raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp) - data = { - ATTR_ENTITY_ID: entity.entity_id, - ATTR_TEMPERATURE: target_temp, - } + data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp} response = directive.response() await hass.services.async_call( - entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False, - context=context) - response.add_context_property({ - 'name': 'targetSetpoint', - 'namespace': 'Alexa.ThermostatController', - 'value': {'value': target_temp, 'scale': API_TEMP_UNITS[unit]}, - }) + entity.domain, + climate.SERVICE_SET_TEMPERATURE, + data, + blocking=False, + context=context, + ) + response.add_context_property( + { + "name": "targetSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": target_temp, "scale": API_TEMP_UNITS[unit]}, + } + ) return response -@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode')) +@HANDLERS.register(("Alexa.ThermostatController", "SetThermostatMode")) async def async_api_set_thermostat_mode(hass, config, directive, context): """Process a set thermostat mode request.""" entity = directive.entity - mode = directive.payload['thermostatMode'] - mode = mode if isinstance(mode, str) else mode['value'] + mode = directive.payload["thermostatMode"] + mode = mode if isinstance(mode, str) else mode["value"] - data = { - ATTR_ENTITY_ID: entity.entity_id, - } + data = {ATTR_ENTITY_ID: entity.entity_id} - ha_preset = next( - (k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode), - None - ) + ha_preset = next((k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode), None) if ha_preset: presets = entity.attributes.get(climate.ATTR_PRESET_MODES, []) if ha_preset not in presets: - msg = 'The requested thermostat mode {} is not supported'.format( - ha_preset - ) + msg = "The requested thermostat mode {} is not supported".format(ha_preset) raise AlexaUnsupportedThermostatModeError(msg) service = climate.SERVICE_SET_PRESET_MODE @@ -703,14 +752,9 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): else: operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) - ha_mode = next( - (k for k, v in API_THERMOSTAT_MODES.items() if v == mode), - None - ) + ha_mode = next((k for k, v in API_THERMOSTAT_MODES.items() if v == mode), None) if ha_mode not in operation_list: - msg = 'The requested thermostat mode {} is not supported'.format( - mode - ) + msg = "The requested thermostat mode {} is not supported".format(mode) raise AlexaUnsupportedThermostatModeError(msg) service = climate.SERVICE_SET_HVAC_MODE @@ -718,18 +762,20 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): response = directive.response() await hass.services.async_call( - climate.DOMAIN, service, data, - blocking=False, context=context) - response.add_context_property({ - 'name': 'thermostatMode', - 'namespace': 'Alexa.ThermostatController', - 'value': mode, - }) + climate.DOMAIN, service, data, blocking=False, context=context + ) + response.add_context_property( + { + "name": "thermostatMode", + "namespace": "Alexa.ThermostatController", + "value": mode, + } + ) return response -@HANDLERS.register(('Alexa', 'ReportState')) +@HANDLERS.register(("Alexa", "ReportState")) async def async_api_reportstate(hass, config, directive, context): """Process a ReportState request.""" - return directive.response(name='StateReport') + return directive.response(name="StateReport") diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index b30a7238b3e..edeb6865aad 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -14,27 +14,24 @@ _LOGGER = logging.getLogger(__name__) HANDLERS = Registry() -INTENTS_API_ENDPOINT = '/api/alexa' +INTENTS_API_ENDPOINT = "/api/alexa" class SpeechType(enum.Enum): """The Alexa speech types.""" - plaintext = 'PlainText' - ssml = 'SSML' + plaintext = "PlainText" + ssml = "SSML" -SPEECH_MAPPINGS = { - 'plain': SpeechType.plaintext, - 'ssml': SpeechType.ssml, -} +SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml} class CardType(enum.Enum): """The Alexa card types.""" - simple = 'Simple' - link_account = 'LinkAccount' + simple = "Simple" + link_account = "LinkAccount" @callback @@ -51,44 +48,50 @@ class AlexaIntentsView(http.HomeAssistantView): """Handle Alexa requests.""" url = INTENTS_API_ENDPOINT - name = 'api:alexa' + name = "api:alexa" async def post(self, request): """Handle Alexa.""" - hass = request.app['hass'] + hass = request.app["hass"] message = await request.json() _LOGGER.debug("Received Alexa request: %s", message) try: response = await async_handle_message(hass, message) - return b'' if response is None else self.json(response) + return b"" if response is None else self.json(response) except UnknownRequest as err: _LOGGER.warning(str(err)) - return self.json(intent_error_response( - hass, message, str(err))) + return self.json(intent_error_response(hass, message, str(err))) except intent.UnknownIntent as err: _LOGGER.warning(str(err)) - return self.json(intent_error_response( - hass, message, - "This intent is not yet configured within Home Assistant.")) + return self.json( + intent_error_response( + hass, + message, + "This intent is not yet configured within Home Assistant.", + ) + ) except intent.InvalidSlotInfo as err: _LOGGER.error("Received invalid slot data from Alexa: %s", err) - return self.json(intent_error_response( - hass, message, - "Invalid slot information received for this intent.")) + return self.json( + intent_error_response( + hass, message, "Invalid slot information received for this intent." + ) + ) except intent.IntentError as err: _LOGGER.exception(str(err)) - return self.json(intent_error_response( - hass, message, "Error handling intent.")) + return self.json( + intent_error_response(hass, message, "Error handling intent.") + ) def intent_error_response(hass, message, error): """Return an Alexa response that will speak the error message.""" - alexa_intent_info = message.get('request').get('intent') + alexa_intent_info = message.get("request").get("intent") alexa_response = AlexaResponse(hass, alexa_intent_info) alexa_response.add_speech(SpeechType.plaintext, error) return alexa_response.as_dict() @@ -104,25 +107,25 @@ async def async_handle_message(hass, message): - intent.IntentError """ - req = message.get('request') - req_type = req['type'] + req = message.get("request") + req_type = req["type"] handler = HANDLERS.get(req_type) if not handler: - raise UnknownRequest('Received unknown request {}'.format(req_type)) + raise UnknownRequest("Received unknown request {}".format(req_type)) return await handler(hass, message) -@HANDLERS.register('SessionEndedRequest') +@HANDLERS.register("SessionEndedRequest") async def async_handle_session_end(hass, message): """Handle a session end request.""" return None -@HANDLERS.register('IntentRequest') -@HANDLERS.register('LaunchRequest') +@HANDLERS.register("IntentRequest") +@HANDLERS.register("LaunchRequest") async def async_handle_intent(hass, message): """Handle an intent request. @@ -132,33 +135,37 @@ async def async_handle_intent(hass, message): - intent.IntentError """ - req = message.get('request') - alexa_intent_info = req.get('intent') + req = message.get("request") + alexa_intent_info = req.get("intent") alexa_response = AlexaResponse(hass, alexa_intent_info) - if req['type'] == 'LaunchRequest': - intent_name = message.get('session', {}) \ - .get('application', {}) \ - .get('applicationId') + if req["type"] == "LaunchRequest": + intent_name = ( + message.get("session", {}).get("application", {}).get("applicationId") + ) else: - intent_name = alexa_intent_info['name'] + intent_name = alexa_intent_info["name"] intent_response = await intent.async_handle( - hass, DOMAIN, intent_name, - {key: {'value': value} for key, value - in alexa_response.variables.items()}) + hass, + DOMAIN, + intent_name, + {key: {"value": value} for key, value in alexa_response.variables.items()}, + ) for intent_speech, alexa_speech in SPEECH_MAPPINGS.items(): if intent_speech in intent_response.speech: alexa_response.add_speech( - alexa_speech, - intent_response.speech[intent_speech]['speech']) + alexa_speech, intent_response.speech[intent_speech]["speech"] + ) break - if 'simple' in intent_response.card: + if "simple" in intent_response.card: alexa_response.add_card( - CardType.simple, intent_response.card['simple']['title'], - intent_response.card['simple']['content']) + CardType.simple, + intent_response.card["simple"]["title"], + intent_response.card["simple"]["content"], + ) return alexa_response.as_dict() @@ -168,23 +175,23 @@ def resolve_slot_synonyms(key, request): # Default to the spoken slot value if more than one or none are found. For # reference to the request object structure, see the Alexa docs: # https://tinyurl.com/ybvm7jhs - resolved_value = request['value'] + resolved_value = request["value"] - if ('resolutions' in request and - 'resolutionsPerAuthority' in request['resolutions'] and - len(request['resolutions']['resolutionsPerAuthority']) >= 1): + if ( + "resolutions" in request + and "resolutionsPerAuthority" in request["resolutions"] + and len(request["resolutions"]["resolutionsPerAuthority"]) >= 1 + ): # Extract all of the possible values from each authority with a # successful match possible_values = [] - for entry in request['resolutions']['resolutionsPerAuthority']: - if entry['status']['code'] != SYN_RESOLUTION_MATCH: + for entry in request["resolutions"]["resolutionsPerAuthority"]: + if entry["status"]["code"] != SYN_RESOLUTION_MATCH: continue - possible_values.extend([item['value']['name'] - for item - in entry['values']]) + possible_values.extend([item["value"]["name"] for item in entry["values"]]) # If there is only one match use the resolved value, otherwise the # resolution cannot be determined, so use the spoken slot value @@ -192,9 +199,9 @@ def resolve_slot_synonyms(key, request): resolved_value = possible_values[0] else: _LOGGER.debug( - 'Found multiple synonym resolutions for slot value: {%s: %s}', + "Found multiple synonym resolutions for slot value: {%s: %s}", key, - request['value'] + request["value"], ) return resolved_value @@ -215,12 +222,12 @@ class AlexaResponse: # Intent is None if request was a LaunchRequest or SessionEndedRequest if intent_info is not None: - for key, value in intent_info.get('slots', {}).items(): + for key, value in intent_info.get("slots", {}).items(): # Only include slots with values - if 'value' not in value: + if "value" not in value: continue - _key = key.replace('.', '_') + _key = key.replace(".", "_") self.variables[_key] = resolve_slot_synonyms(key, value) @@ -228,9 +235,7 @@ class AlexaResponse: """Add a card to the response.""" assert self.card is None - card = { - "type": card_type.value - } + card = {"type": card_type.value} if card_type == CardType.link_account: self.card = card @@ -244,43 +249,36 @@ class AlexaResponse: """Add speech to the response.""" assert self.speech is None - key = 'ssml' if speech_type == SpeechType.ssml else 'text' + key = "ssml" if speech_type == SpeechType.ssml else "text" - self.speech = { - 'type': speech_type.value, - key: text - } + self.speech = {"type": speech_type.value, key: text} def add_reprompt(self, speech_type, text): """Add reprompt if user does not answer.""" assert self.reprompt is None - key = 'ssml' if speech_type == SpeechType.ssml else 'text' + key = "ssml" if speech_type == SpeechType.ssml else "text" self.reprompt = { - 'type': speech_type.value, - key: text.async_render(self.variables) + "type": speech_type.value, + key: text.async_render(self.variables), } def as_dict(self): """Return response in an Alexa valid dict.""" - response = { - 'shouldEndSession': self.should_end_session - } + response = {"shouldEndSession": self.should_end_session} if self.card is not None: - response['card'] = self.card + response["card"] = self.card if self.speech is not None: - response['outputSpeech'] = self.speech + response["outputSpeech"] = self.speech if self.reprompt is not None: - response['reprompt'] = { - 'outputSpeech': self.reprompt - } + response["reprompt"] = {"outputSpeech": self.reprompt} return { - 'version': '1.0', - 'sessionAttributes': self.session_attributes, - 'response': response, + "version": "1.0", + "sessionAttributes": self.session_attributes, + "response": response, } diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py index c1b0ac9c025..3195656ed09 100644 --- a/homeassistant/components/alexa/messages.py +++ b/homeassistant/components/alexa/messages.py @@ -23,8 +23,8 @@ class AlexaDirective: def __init__(self, request): """Initialize a directive.""" self._directive = request[API_DIRECTIVE] - self.namespace = self._directive[API_HEADER]['namespace'] - self.name = self._directive[API_HEADER]['name'] + self.namespace = self._directive[API_HEADER]["namespace"] + self.name = self._directive[API_HEADER]["name"] self.payload = self._directive[API_PAYLOAD] self.has_endpoint = API_ENDPOINT in self._directive @@ -44,27 +44,23 @@ class AlexaDirective: Will raise AlexaInvalidEndpointError if the endpoint in the request is malformed or nonexistant. """ - _endpoint_id = self._directive[API_ENDPOINT]['endpointId'] - self.entity_id = _endpoint_id.replace('#', '.') + _endpoint_id = self._directive[API_ENDPOINT]["endpointId"] + self.entity_id = _endpoint_id.replace("#", ".") self.entity = hass.states.get(self.entity_id) if not self.entity or not config.should_expose(self.entity_id): raise AlexaInvalidEndpointError(_endpoint_id) - self.endpoint = ENTITY_ADAPTERS[self.entity.domain]( - hass, config, self.entity) + self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity) - def response(self, - name='Response', - namespace='Alexa', - payload=None): + def response(self, name="Response", namespace="Alexa", payload=None): """Create an API formatted response. Async friendly. """ response = AlexaResponse(name, namespace, payload) - token = self._directive[API_HEADER].get('correlationToken') + token = self._directive[API_HEADER].get("correlationToken") if token: response.set_correlation_token(token) @@ -74,31 +70,30 @@ class AlexaDirective: return response def error( - self, - namespace='Alexa', - error_type='INTERNAL_ERROR', - error_message="", - payload=None + self, + namespace="Alexa", + error_type="INTERNAL_ERROR", + error_message="", + payload=None, ): """Create a API formatted error response. Async friendly. """ payload = payload or {} - payload['type'] = error_type - payload['message'] = error_message + payload["type"] = error_type + payload["message"] = error_message - _LOGGER.info("Request %s/%s error %s: %s", - self._directive[API_HEADER]['namespace'], - self._directive[API_HEADER]['name'], - error_type, error_message) - - return self.response( - name='ErrorResponse', - namespace=namespace, - payload=payload + _LOGGER.info( + "Request %s/%s error %s: %s", + self._directive[API_HEADER]["namespace"], + self._directive[API_HEADER]["name"], + error_type, + error_message, ) + return self.response(name="ErrorResponse", namespace=namespace, payload=payload) + class AlexaResponse: """Class to hold a response.""" @@ -109,10 +104,10 @@ class AlexaResponse: self._response = { API_EVENT: { API_HEADER: { - 'namespace': namespace, - 'name': name, - 'messageId': str(uuid4()), - 'payloadVersion': '3', + "namespace": namespace, + "name": name, + "messageId": str(uuid4()), + "payloadVersion": "3", }, API_PAYLOAD: payload, } @@ -121,12 +116,12 @@ class AlexaResponse: @property def name(self): """Return the name of this response.""" - return self._response[API_EVENT][API_HEADER]['name'] + return self._response[API_EVENT][API_HEADER]["name"] @property def namespace(self): """Return the namespace of this response.""" - return self._response[API_EVENT][API_HEADER]['namespace'] + return self._response[API_EVENT][API_HEADER]["namespace"] def set_correlation_token(self, token): """Set the correlationToken. @@ -134,7 +129,7 @@ class AlexaResponse: This should normally mirror the value from a request, and is set by AlexaDirective.response() usually. """ - self._response[API_EVENT][API_HEADER]['correlationToken'] = token + self._response[API_EVENT][API_HEADER]["correlationToken"] = token def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None): """Set the endpoint dictionary. @@ -142,17 +137,14 @@ class AlexaResponse: This is used to send proactive messages to Alexa. """ self._response[API_EVENT][API_ENDPOINT] = { - API_SCOPE: { - 'type': 'BearerToken', - 'token': bearer_token - } + API_SCOPE: {"type": "BearerToken", "token": bearer_token} } if endpoint_id is not None: - self._response[API_EVENT][API_ENDPOINT]['endpointId'] = endpoint_id + self._response[API_EVENT][API_ENDPOINT]["endpointId"] = endpoint_id if cookie is not None: - self._response[API_EVENT][API_ENDPOINT]['cookie'] = cookie + self._response[API_EVENT][API_ENDPOINT]["cookie"] = cookie def set_endpoint(self, endpoint): """Set the endpoint. @@ -164,14 +156,14 @@ class AlexaResponse: def _properties(self): context = self._response.setdefault(API_CONTEXT, {}) - return context.setdefault('properties', []) + return context.setdefault("properties", []) def add_context_property(self, prop): """Add a property to the response context. The Alexa response includes a list of properties which provides feedback on how states have changed. For example if a user asks, - "Alexa, set theromstat to 20 degrees", the API expects a response with + "Alexa, set thermostat to 20 degrees", the API expects a response with the new value of the property, and Alexa will respond to the user "Thermostat set to 20 degrees". @@ -189,10 +181,10 @@ class AlexaResponse: Handlers should be using .add_context_property(). """ properties = self._properties() - already_set = {(p['namespace'], p['name']) for p in properties} + already_set = {(p["namespace"], p["name"]) for p in properties} for prop in endpoint.serialize_properties(): - if (prop['namespace'], prop['name']) not in already_set: + if (prop["namespace"], prop["name"]) not in already_set: self.add_context_property(prop) def serialize(self): diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 688828b20bd..2c34542e25c 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -4,32 +4,23 @@ import logging import homeassistant.core as ha from .const import API_DIRECTIVE, API_HEADER -from .errors import ( - AlexaError, - AlexaBridgeUnreachableError, -) +from .errors import AlexaError, AlexaBridgeUnreachableError from .handlers import HANDLERS from .messages import AlexaDirective _LOGGER = logging.getLogger(__name__) -EVENT_ALEXA_SMART_HOME = 'alexa_smart_home' +EVENT_ALEXA_SMART_HOME = "alexa_smart_home" -async def async_handle_message( - hass, - config, - request, - context=None, - enabled=True, -): +async def async_handle_message(hass, config, request, context=None, enabled=True): """Handle incoming API messages. If enabled is False, the response to all messagess will be a BRIDGE_UNREACHABLE error. This can be used if the API has been disabled in configuration. """ - assert request[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3' + assert request[API_DIRECTIVE][API_HEADER]["payloadVersion"] == "3" if context is None: context = ha.Context() @@ -39,7 +30,8 @@ async def async_handle_message( try: if not enabled: raise AlexaBridgeUnreachableError( - 'Alexa API not enabled in Home Assistant configuration') + "Alexa API not enabled in Home Assistant configuration" + ) if directive.has_endpoint: directive.load_entity(hass, config) @@ -51,30 +43,26 @@ async def async_handle_message( response.merge_context_properties(directive.endpoint) else: _LOGGER.warning( - "Unsupported API request %s/%s", - directive.namespace, - directive.name, + "Unsupported API request %s/%s", directive.namespace, directive.name ) response = directive.error() except AlexaError as err: response = directive.error( - error_type=err.error_type, - error_message=err.error_message) + error_type=err.error_type, error_message=err.error_message + ) - request_info = { - 'namespace': directive.namespace, - 'name': directive.name, - } + request_info = {"namespace": directive.namespace, "name": directive.name} if directive.has_endpoint: - request_info['entity_id'] = directive.entity_id + request_info["entity_id"] = directive.entity_id - hass.bus.async_fire(EVENT_ALEXA_SMART_HOME, { - 'request': request_info, - 'response': { - 'namespace': response.namespace, - 'name': response.name, - } - }, context=context) + hass.bus.async_fire( + EVENT_ALEXA_SMART_HOME, + { + "request": request_info, + "response": {"namespace": response.namespace, "name": response.name}, + }, + context=context, + ) return response.serialize() diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 4636ee10bb7..7fdd4e3000a 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -11,13 +11,13 @@ from .const import ( CONF_CLIENT_SECRET, CONF_ENDPOINT, CONF_ENTITY_CONFIG, - CONF_FILTER + CONF_FILTER, ) from .state_report import async_enable_proactive_mode from .smart_home import async_handle_message _LOGGER = logging.getLogger(__name__) -SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home' +SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" class AlexaConfig(AbstractConfig): @@ -29,8 +29,7 @@ class AlexaConfig(AbstractConfig): self._config = config if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET): - self._auth = Auth(hass, config[CONF_CLIENT_ID], - config[CONF_CLIENT_SECRET]) + self._auth = Auth(hass, config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET]) else: self._auth = None @@ -87,7 +86,7 @@ class SmartHomeView(HomeAssistantView): """Expose Smart Home v3 payload interface via HTTP POST.""" url = SMART_HOME_HTTP_ENDPOINT - name = 'api:alexa:smart_home' + name = "api:alexa:smart_home" def __init__(self, smart_home_config): """Initialize.""" @@ -100,15 +99,14 @@ class SmartHomeView(HomeAssistantView): Lambda, which will need to forward the requests to here and pass back the response. """ - hass = request.app['hass'] - user = request['hass_user'] + hass = request.app["hass"] + user = request["hass_user"] message = await request.json() _LOGGER.debug("Received Alexa Smart Home request: %s", message) response = await async_handle_message( - hass, self.smart_home_config, message, - context=core.Context(user_id=user.id) + hass, self.smart_home_config, message, context=core.Context(user_id=user.id) ) _LOGGER.debug("Sending Alexa Smart Home response: %s", response) - return b'' if response is None else self.json(response) + return b"" if response is None else self.json(response) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 022b38be59d..7e842889977 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -24,8 +24,7 @@ async def async_enable_proactive_mode(hass, smart_home_config): # Validate we can get access token. await smart_home_config.async_get_access_token() - async def async_entity_state_listener(changed_entity, old_state, - new_state): + async def async_entity_state_listener(changed_entity, old_state, new_state): if not new_state: return @@ -33,18 +32,18 @@ async def async_enable_proactive_mode(hass, smart_home_config): return if not smart_home_config.should_expose(changed_entity): - _LOGGER.debug("Not exposing %s because filtered by config", - changed_entity) + _LOGGER.debug("Not exposing %s because filtered by config", changed_entity) return - alexa_changed_entity = \ - ENTITY_ADAPTERS[new_state.domain](hass, smart_home_config, - new_state) + alexa_changed_entity = ENTITY_ADAPTERS[new_state.domain]( + hass, smart_home_config, new_state + ) for interface in alexa_changed_entity.interfaces(): if interface.properties_proactively_reported(): - await async_send_changereport_message(hass, smart_home_config, - alexa_changed_entity) + await async_send_changereport_message( + hass, smart_home_config, alexa_changed_entity + ) return return hass.helpers.event.async_track_state_change( @@ -59,9 +58,7 @@ async def async_send_changereport_message(hass, config, alexa_entity): """ token = await config.async_get_access_token() - headers = { - "Authorization": "Bearer {}".format(token) - } + headers = {"Authorization": "Bearer {}".format(token)} endpoint = alexa_entity.alexa_id() @@ -71,14 +68,10 @@ async def async_send_changereport_message(hass, config, alexa_entity): properties = list(alexa_entity.serialize_properties()) payload = { - API_CHANGE: { - 'cause': {'type': Cause.APP_INTERACTION}, - 'properties': properties - } + API_CHANGE: {"cause": {"type": Cause.APP_INTERACTION}, "properties": properties} } - message = AlexaResponse(name='ChangeReport', namespace='Alexa', - payload=payload) + message = AlexaResponse(name="ChangeReport", namespace="Alexa", payload=payload) message.set_endpoint_full(token, endpoint) message_serialized = message.serialize() @@ -86,10 +79,12 @@ async def async_send_changereport_message(hass, config, alexa_entity): try: with async_timeout.timeout(DEFAULT_TIMEOUT): - response = await session.post(config.endpoint, - headers=headers, - json=message_serialized, - allow_redirects=True) + response = await session.post( + config.endpoint, + headers=headers, + json=message_serialized, + allow_redirects=True, + ) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout sending report to Alexa.") @@ -102,9 +97,11 @@ async def async_send_changereport_message(hass, config, alexa_entity): if response.status != 202: response_json = json.loads(response_text) - _LOGGER.error("Error when sending ChangeReport to Alexa: %s: %s", - response_json["payload"]["code"], - response_json["payload"]["description"]) + _LOGGER.error( + "Error when sending ChangeReport to Alexa: %s: %s", + response_json["payload"]["code"], + response_json["payload"]["description"], + ) async def async_send_add_or_update_message(hass, config, entity_ids): @@ -114,35 +111,27 @@ async def async_send_add_or_update_message(hass, config, entity_ids): """ token = await config.async_get_access_token() - headers = { - "Authorization": "Bearer {}".format(token) - } + headers = {"Authorization": "Bearer {}".format(token)} endpoints = [] for entity_id in entity_ids: - domain = entity_id.split('.', 1)[0] - alexa_entity = ENTITY_ADAPTERS[domain]( - hass, config, hass.states.get(entity_id) - ) + domain = entity_id.split(".", 1)[0] + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) endpoints.append(alexa_entity.serialize_discovery()) - payload = { - 'endpoints': endpoints, - 'scope': { - 'type': 'BearerToken', - 'token': token, - } - } + payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} message = AlexaResponse( - name='AddOrUpdateReport', namespace='Alexa.Discovery', payload=payload) + name="AddOrUpdateReport", namespace="Alexa.Discovery", payload=payload + ) message_serialized = message.serialize() session = hass.helpers.aiohttp_client.async_get_clientsession() - return await session.post(config.endpoint, headers=headers, - json=message_serialized, allow_redirects=True) + return await session.post( + config.endpoint, headers=headers, json=message_serialized, allow_redirects=True + ) async def async_send_delete_message(hass, config, entity_ids): @@ -152,34 +141,24 @@ async def async_send_delete_message(hass, config, entity_ids): """ token = await config.async_get_access_token() - headers = { - "Authorization": "Bearer {}".format(token) - } + headers = {"Authorization": "Bearer {}".format(token)} endpoints = [] for entity_id in entity_ids: - domain = entity_id.split('.', 1)[0] - alexa_entity = ENTITY_ADAPTERS[domain]( - hass, config, hass.states.get(entity_id) - ) - endpoints.append({ - 'endpointId': alexa_entity.alexa_id() - }) + domain = entity_id.split(".", 1)[0] + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) + endpoints.append({"endpointId": alexa_entity.alexa_id()}) - payload = { - 'endpoints': endpoints, - 'scope': { - 'type': 'BearerToken', - 'token': token, - } - } + payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} - message = AlexaResponse(name='DeleteReport', namespace='Alexa.Discovery', - payload=payload) + message = AlexaResponse( + name="DeleteReport", namespace="Alexa.Discovery", payload=payload + ) message_serialized = message.serialize() session = hass.helpers.aiohttp_client.async_get_clientsession() - return await session.post(config.endpoint, headers=headers, - json=message_serialized, allow_redirects=True) + return await session.post( + config.endpoint, headers=headers, json=message_serialized, allow_redirects=True + ) diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 9ea6797a56e..6d790e0719b 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -5,56 +5,59 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_CLOSE = 'close' -ATTR_HIGH = 'high' -ATTR_LOW = 'low' +ATTR_CLOSE = "close" +ATTR_HIGH = "high" +ATTR_LOW = "low" ATTRIBUTION = "Stock market information provided by Alpha Vantage" -CONF_FOREIGN_EXCHANGE = 'foreign_exchange' -CONF_FROM = 'from' -CONF_SYMBOL = 'symbol' -CONF_SYMBOLS = 'symbols' -CONF_TO = 'to' +CONF_FOREIGN_EXCHANGE = "foreign_exchange" +CONF_FROM = "from" +CONF_SYMBOL = "symbol" +CONF_SYMBOLS = "symbols" +CONF_TO = "to" ICONS = { - 'BTC': 'mdi:currency-btc', - 'EUR': 'mdi:currency-eur', - 'GBP': 'mdi:currency-gbp', - 'INR': 'mdi:currency-inr', - 'RUB': 'mdi:currency-rub', - 'TRY': 'mdi:currency-try', - 'USD': 'mdi:currency-usd', + "BTC": "mdi:currency-btc", + "EUR": "mdi:currency-eur", + "GBP": "mdi:currency-gbp", + "INR": "mdi:currency-inr", + "RUB": "mdi:currency-rub", + "TRY": "mdi:currency-try", + "USD": "mdi:currency-usd", } SCAN_INTERVAL = timedelta(minutes=5) -SYMBOL_SCHEMA = vol.Schema({ - vol.Required(CONF_SYMBOL): cv.string, - vol.Optional(CONF_CURRENCY): cv.string, - vol.Optional(CONF_NAME): cv.string, -}) +SYMBOL_SCHEMA = vol.Schema( + { + vol.Required(CONF_SYMBOL): cv.string, + vol.Optional(CONF_CURRENCY): cv.string, + vol.Optional(CONF_NAME): cv.string, + } +) -CURRENCY_SCHEMA = vol.Schema({ - vol.Required(CONF_FROM): cv.string, - vol.Required(CONF_TO): cv.string, - vol.Optional(CONF_NAME): cv.string, -}) +CURRENCY_SCHEMA = vol.Schema( + { + vol.Required(CONF_FROM): cv.string, + vol.Required(CONF_TO): cv.string, + vol.Optional(CONF_NAME): cv.string, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_FOREIGN_EXCHANGE): - vol.All(cv.ensure_list, [CURRENCY_SCHEMA]), - vol.Optional(CONF_SYMBOLS): - vol.All(cv.ensure_list, [SYMBOL_SCHEMA]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_FOREIGN_EXCHANGE): vol.All(cv.ensure_list, [CURRENCY_SCHEMA]), + vol.Optional(CONF_SYMBOLS): vol.All(cv.ensure_list, [SYMBOL_SCHEMA]), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -67,9 +70,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): conversions = config.get(CONF_FOREIGN_EXCHANGE, []) if not symbols and not conversions: - msg = 'Warning: No symbols or currencies configured.' - hass.components.persistent_notification.create( - msg, 'Sensor alpha_vantage') + msg = "Warning: No symbols or currencies configured." + hass.components.persistent_notification.create(msg, "Sensor alpha_vantage") _LOGGER.warning(msg) return @@ -78,12 +80,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] for symbol in symbols: try: - _LOGGER.debug("Configuring timeseries for symbols: %s", - symbol[CONF_SYMBOL]) + _LOGGER.debug("Configuring timeseries for symbols: %s", symbol[CONF_SYMBOL]) timeseries.get_intraday(symbol[CONF_SYMBOL]) except ValueError: - _LOGGER.error( - "API Key is not valid or symbol '%s' not known", symbol) + _LOGGER.error("API Key is not valid or symbol '%s' not known", symbol) dev.append(AlphaVantageSensor(timeseries, symbol)) forex = ForeignExchange(key=api_key) @@ -92,12 +92,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): to_cur = conversion.get(CONF_TO) try: _LOGGER.debug("Configuring forex %s - %s", from_cur, to_cur) - forex.get_currency_exchange_rate( - from_currency=from_cur, to_currency=to_cur) + forex.get_currency_exchange_rate(from_currency=from_cur, to_currency=to_cur) except ValueError as error: _LOGGER.error( "API Key is not valid or currencies '%s'/'%s' not known", - from_cur, to_cur) + from_cur, + to_cur, + ) _LOGGER.debug(str(error)) dev.append(AlphaVantageForeignExchange(forex, conversion)) @@ -115,7 +116,7 @@ class AlphaVantageSensor(Entity): self._timeseries = timeseries self.values = None self._unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol) - self._icon = ICONS.get(symbol.get(CONF_CURRENCY, 'USD')) + self._icon = ICONS.get(symbol.get(CONF_CURRENCY, "USD")) @property def name(self): @@ -130,7 +131,7 @@ class AlphaVantageSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self.values['1. open'] + return self.values["1. open"] @property def device_state_attributes(self): @@ -138,9 +139,9 @@ class AlphaVantageSensor(Entity): if self.values is not None: return { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_CLOSE: self.values['4. close'], - ATTR_HIGH: self.values['2. high'], - ATTR_LOW: self.values['3. low'], + ATTR_CLOSE: self.values["4. close"], + ATTR_HIGH: self.values["2. high"], + ATTR_LOW: self.values["3. low"], } @property @@ -167,9 +168,9 @@ class AlphaVantageForeignExchange(Entity): if CONF_NAME in config: self._name = config.get(CONF_NAME) else: - self._name = '{}/{}'.format(self._to_currency, self._from_currency) + self._name = "{}/{}".format(self._to_currency, self._from_currency) self._unit_of_measurement = self._to_currency - self._icon = ICONS.get(self._from_currency, 'USD') + self._icon = ICONS.get(self._from_currency, "USD") self.values = None @property @@ -185,7 +186,7 @@ class AlphaVantageForeignExchange(Entity): @property def state(self): """Return the state of the sensor.""" - return round(float(self.values['5. Exchange Rate']), 4) + return round(float(self.values["5. Exchange Rate"]), 4) @property def icon(self): @@ -204,9 +205,16 @@ class AlphaVantageForeignExchange(Entity): def update(self): """Get the latest data and updates the states.""" - _LOGGER.debug("Requesting new data for forex %s - %s", - self._from_currency, self._to_currency) + _LOGGER.debug( + "Requesting new data for forex %s - %s", + self._from_currency, + self._to_currency, + ) self.values, _ = self._foreign_exchange.get_currency_exchange_rate( - from_currency=self._from_currency, to_currency=self._to_currency) - _LOGGER.debug("Received new data for forex %s - %s", - self._from_currency, self._to_currency) + from_currency=self._from_currency, to_currency=self._to_currency + ) + _LOGGER.debug( + "Received new data for forex %s - %s", + self._from_currency, + self._to_currency, + ) diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 4511a587a60..c7098867ee8 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -8,108 +8,145 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_REGION = 'region_name' -CONF_ACCESS_KEY_ID = 'aws_access_key_id' -CONF_SECRET_ACCESS_KEY = 'aws_secret_access_key' -CONF_PROFILE_NAME = 'profile_name' -ATTR_CREDENTIALS = 'credentials' +CONF_REGION = "region_name" +CONF_ACCESS_KEY_ID = "aws_access_key_id" +CONF_SECRET_ACCESS_KEY = "aws_secret_access_key" +CONF_PROFILE_NAME = "profile_name" +ATTR_CREDENTIALS = "credentials" -DEFAULT_REGION = 'us-east-1' -SUPPORTED_REGIONS = ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2', - 'ca-central-1', 'eu-west-1', 'eu-central-1', 'eu-west-2', - 'eu-west-3', 'ap-southeast-1', 'ap-southeast-2', - 'ap-northeast-2', 'ap-northeast-1', 'ap-south-1', - 'sa-east-1'] - -CONF_VOICE = 'voice' -CONF_OUTPUT_FORMAT = 'output_format' -CONF_SAMPLE_RATE = 'sample_rate' -CONF_TEXT_TYPE = 'text_type' - -SUPPORTED_VOICES = [ - 'Zhiyu', # Chinese - 'Mads', 'Naja', # Danish - 'Ruben', 'Lotte', # Dutch - 'Russell', 'Nicole', # English Austrailian - 'Brian', 'Amy', 'Emma', # English - 'Aditi', 'Raveena', # English, Indian - 'Joey', 'Justin', 'Matthew', 'Ivy', 'Joanna', 'Kendra', 'Kimberly', - 'Salli', # English - 'Geraint', # English Welsh - 'Mathieu', 'Celine', 'Lea', # French - 'Chantal', # French Canadian - 'Hans', 'Marlene', 'Vicki', # German - 'Aditi', # Hindi - 'Karl', 'Dora', # Icelandic - 'Giorgio', 'Carla', 'Bianca', # Italian - 'Takumi', 'Mizuki', # Japanese - 'Seoyeon', # Korean - 'Liv', # Norwegian - 'Jacek', 'Jan', 'Ewa', 'Maja', # Polish - 'Ricardo', 'Vitoria', # Portuguese, Brazilian - 'Cristiano', 'Ines', # Portuguese, European - 'Carmen', # Romanian - 'Maxim', 'Tatyana', # Russian - 'Enrique', 'Conchita', 'Lucia', # Spanish European - 'Mia', # Spanish Mexican - 'Miguel', 'Penelope', # Spanish US - 'Astrid', # Swedish - 'Filiz', # Turkish - 'Gwyneth', # Welsh +DEFAULT_REGION = "us-east-1" +SUPPORTED_REGIONS = [ + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2", + "ca-central-1", + "eu-west-1", + "eu-central-1", + "eu-west-2", + "eu-west-3", + "ap-southeast-1", + "ap-southeast-2", + "ap-northeast-2", + "ap-northeast-1", + "ap-south-1", + "sa-east-1", ] -SUPPORTED_OUTPUT_FORMATS = ['mp3', 'ogg_vorbis', 'pcm'] +CONF_VOICE = "voice" +CONF_OUTPUT_FORMAT = "output_format" +CONF_SAMPLE_RATE = "sample_rate" +CONF_TEXT_TYPE = "text_type" -SUPPORTED_SAMPLE_RATES = ['8000', '16000', '22050'] +SUPPORTED_VOICES = [ + "Zhiyu", # Chinese + "Mads", + "Naja", # Danish + "Ruben", + "Lotte", # Dutch + "Russell", + "Nicole", # English Australian + "Brian", + "Amy", + "Emma", # English + "Aditi", + "Raveena", # English, Indian + "Joey", + "Justin", + "Matthew", + "Ivy", + "Joanna", + "Kendra", + "Kimberly", + "Salli", # English + "Geraint", # English Welsh + "Mathieu", + "Celine", + "Lea", # French + "Chantal", # French Canadian + "Hans", + "Marlene", + "Vicki", # German + "Aditi", # Hindi + "Karl", + "Dora", # Icelandic + "Giorgio", + "Carla", + "Bianca", # Italian + "Takumi", + "Mizuki", # Japanese + "Seoyeon", # Korean + "Liv", # Norwegian + "Jacek", + "Jan", + "Ewa", + "Maja", # Polish + "Ricardo", + "Vitoria", # Portuguese, Brazilian + "Cristiano", + "Ines", # Portuguese, European + "Carmen", # Romanian + "Maxim", + "Tatyana", # Russian + "Enrique", + "Conchita", + "Lucia", # Spanish European + "Mia", # Spanish Mexican + "Miguel", + "Penelope", # Spanish US + "Astrid", # Swedish + "Filiz", # Turkish + "Gwyneth", # Welsh +] + +SUPPORTED_OUTPUT_FORMATS = ["mp3", "ogg_vorbis", "pcm"] + +SUPPORTED_SAMPLE_RATES = ["8000", "16000", "22050"] SUPPORTED_SAMPLE_RATES_MAP = { - 'mp3': ['8000', '16000', '22050'], - 'ogg_vorbis': ['8000', '16000', '22050'], - 'pcm': ['8000', '16000'], + "mp3": ["8000", "16000", "22050"], + "ogg_vorbis": ["8000", "16000", "22050"], + "pcm": ["8000", "16000"], } -SUPPORTED_TEXT_TYPES = ['text', 'ssml'] +SUPPORTED_TEXT_TYPES = ["text", "ssml"] -CONTENT_TYPE_EXTENSIONS = { - 'audio/mpeg': 'mp3', - 'audio/ogg': 'ogg', - 'audio/pcm': 'pcm', -} +CONTENT_TYPE_EXTENSIONS = {"audio/mpeg": "mp3", "audio/ogg": "ogg", "audio/pcm": "pcm"} -DEFAULT_VOICE = 'Joanna' -DEFAULT_OUTPUT_FORMAT = 'mp3' -DEFAULT_TEXT_TYPE = 'text' +DEFAULT_VOICE = "Joanna" +DEFAULT_OUTPUT_FORMAT = "mp3" +DEFAULT_TEXT_TYPE = "text" -DEFAULT_SAMPLE_RATES = { - 'mp3': '22050', - 'ogg_vorbis': '22050', - 'pcm': '16000', -} +DEFAULT_SAMPLE_RATES = {"mp3": "22050", "ogg_vorbis": "22050", "pcm": "16000"} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_REGION, default=DEFAULT_REGION): - vol.In(SUPPORTED_REGIONS), - vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, - vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, - vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, - vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES), - vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT): - vol.In(SUPPORTED_OUTPUT_FORMATS), - vol.Optional(CONF_SAMPLE_RATE): - vol.All(cv.string, vol.In(SUPPORTED_SAMPLE_RATES)), - vol.Optional(CONF_TEXT_TYPE, default=DEFAULT_TEXT_TYPE): - vol.In(SUPPORTED_TEXT_TYPES), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(SUPPORTED_REGIONS), + vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, + vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, + vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES), + vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT): vol.In( + SUPPORTED_OUTPUT_FORMATS + ), + vol.Optional(CONF_SAMPLE_RATE): vol.All( + cv.string, vol.In(SUPPORTED_SAMPLE_RATES) + ), + vol.Optional(CONF_TEXT_TYPE, default=DEFAULT_TEXT_TYPE): vol.In( + SUPPORTED_TEXT_TYPES + ), + } +) def get_engine(hass, config): """Set up Amazon Polly speech component.""" output_format = config.get(CONF_OUTPUT_FORMAT) - sample_rate = config.get( - CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format]) + sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format]) if sample_rate not in SUPPORTED_SAMPLE_RATES_MAP.get(output_format): - _LOGGER.error("%s is not a valid sample rate for %s", - sample_rate, output_format) + _LOGGER.error( + "%s is not a valid sample rate for %s", sample_rate, output_format + ) return None config[CONF_SAMPLE_RATE] = sample_rate @@ -131,7 +168,7 @@ def get_engine(hass, config): del config[CONF_ACCESS_KEY_ID] del config[CONF_SECRET_ACCESS_KEY] - polly_client = boto3.client('polly', **aws_config) + polly_client = boto3.client("polly", **aws_config) supported_languages = [] @@ -139,27 +176,25 @@ def get_engine(hass, config): all_voices_req = polly_client.describe_voices() - for voice in all_voices_req.get('Voices'): - all_voices[voice.get('Id')] = voice - if voice.get('LanguageCode') not in supported_languages: - supported_languages.append(voice.get('LanguageCode')) + for voice in all_voices_req.get("Voices"): + all_voices[voice.get("Id")] = voice + if voice.get("LanguageCode") not in supported_languages: + supported_languages.append(voice.get("LanguageCode")) - return AmazonPollyProvider( - polly_client, config, supported_languages, all_voices) + return AmazonPollyProvider(polly_client, config, supported_languages, all_voices) class AmazonPollyProvider(Provider): """Amazon Polly speech api provider.""" - def __init__(self, polly_client, config, supported_languages, - all_voices): + def __init__(self, polly_client, config, supported_languages, all_voices): """Initialize Amazon Polly provider for TTS.""" self.client = polly_client self.config = config self.supported_langs = supported_languages self.all_voices = all_voices self.default_voice = self.config.get(CONF_VOICE) - self.name = 'Amazon Polly' + self.name = "Amazon Polly" @property def supported_languages(self): @@ -169,7 +204,7 @@ class AmazonPollyProvider(Provider): @property def default_language(self): """Return the default language.""" - return self.all_voices.get(self.default_voice).get('LanguageCode') + return self.all_voices.get(self.default_voice).get("LanguageCode") @property def default_options(self): @@ -185,9 +220,8 @@ class AmazonPollyProvider(Provider): """Request TTS file from Polly.""" voice_id = options.get(CONF_VOICE, self.default_voice) voice_in_dict = self.all_voices.get(voice_id) - if language != voice_in_dict.get('LanguageCode'): - _LOGGER.error("%s does not support the %s language", - voice_id, language) + if language != voice_in_dict.get("LanguageCode"): + _LOGGER.error("%s does not support the %s language", voice_id, language) return None, None resp = self.client.synthesize_speech( @@ -195,8 +229,10 @@ class AmazonPollyProvider(Provider): SampleRate=self.config[CONF_SAMPLE_RATE], Text=message, TextType=self.config[CONF_TEXT_TYPE], - VoiceId=voice_id + VoiceId=voice_id, ) - return (CONTENT_TYPE_EXTENSIONS[resp.get('ContentType')], - resp.get('AudioStream').read()) + return ( + CONTENT_TYPE_EXTENSIONS[resp.get("ContentType")], + resp.get("AudioStream").read(), + ) diff --git a/homeassistant/components/ambiclimate/.translations/bg.json b/homeassistant/components/ambiclimate/.translations/bg.json new file mode 100644 index 00000000000..4795267cd5e --- /dev/null +++ b/homeassistant/components/ambiclimate/.translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "access_token": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f.", + "already_setup": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u043d\u0430 Ambiclimate \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d.", + "no_config": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Ambiclimate, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0433\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u0442\u0435. [\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438\u0442\u0435](https://www.home-assistant.io/components/ambiclimate/)." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Ambiclimate." + }, + "error": { + "follow_link": "\u041c\u043e\u043b\u044f, \u043f\u043e\u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0438 \u0441\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u0439\u0442\u0435, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435", + "no_token": "\u041b\u0438\u043f\u0441\u0432\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Ambiclimate" + }, + "step": { + "auth": { + "description": "\u041c\u043e\u043b\u044f, \u043f\u043e\u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0442\u043e\u0437\u0438 [link]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0435\u0442\u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u0430 \u0434\u043e \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0441\u0438 \u0432 Ambiclimate, \u0441\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u0441\u0435 \u0432\u044a\u0440\u043d\u0435\u0442\u0435 \u0438 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435 \u043f\u043e-\u0434\u043e\u043b\u0443. \n (\u0423\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435, \u0447\u0435 \u043f\u043e\u0441\u043e\u0447\u0435\u043d\u0438\u044f\u0442 url \u0437\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u0430 \u043f\u043e\u0432\u0438\u043a\u0432\u0430\u043d\u0435 \u0435 {cb_url})", + "title": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Ambiclimate" + } + }, + "title": "Ambiclimate" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/da.json b/homeassistant/components/ambiclimate/.translations/da.json new file mode 100644 index 00000000000..b57a0e15797 --- /dev/null +++ b/homeassistant/components/ambiclimate/.translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "access_token": "Ukendt fejl ved generering af et adgangstoken.", + "already_setup": "Ambiclimate kontoen er konfigureret.", + "no_config": "Du skal konfigurere Ambiclimate f\u00f8r du kan godkende med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/ambiclimate/)." + }, + "create_entry": { + "default": "Godkendt med Ambiclimate" + }, + "error": { + "follow_link": "F\u00f8lg linket og godkend f\u00f8r du trykker p\u00e5 send", + "no_token": "Ikke godkendt med Ambiclimate" + }, + "step": { + "auth": { + "description": "F\u00f8lg dette [link]({authorization_url}) og Tillad adgang til din Ambiclimate-konto, vend s\u00e5 tilbage og tryk p\u00e5 Indsend nedenfor.\n(Kontroll\u00e9r den angivne callback url er {cb_url})", + "title": "Godkend Ambiclimate" + } + }, + "title": "Ambiclimate" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/__init__.py b/homeassistant/components/ambiclimate/__init__.py index 07494ce6cf7..962c8c8a82d 100644 --- a/homeassistant/components/ambiclimate/__init__.py +++ b/homeassistant/components/ambiclimate/__init__.py @@ -12,11 +12,12 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { - DOMAIN: - vol.Schema({ + DOMAIN: vol.Schema( + { vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, - }) + } + ) }, extra=vol.ALLOW_EXTRA, ) @@ -30,15 +31,16 @@ async def async_setup(hass, config): conf = config[DOMAIN] config_flow.register_flow_implementation( - hass, conf[CONF_CLIENT_ID], - conf[CONF_CLIENT_SECRET]) + hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET] + ) return True async def async_setup_entry(hass, entry): """Set up Ambiclimate from a config entry.""" - hass.async_create_task(hass.config_entries.async_forward_entry_setup( - entry, 'climate')) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "climate") + ) return True diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 5bd000f6485..bb3e5ab2b25 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -7,35 +7,41 @@ import voluptuous as vol from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_OFF, HVAC_MODE_HEAT) + SUPPORT_TARGET_TEMPERATURE, + HVAC_MODE_OFF, + HVAC_MODE_HEAT, +) from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import (ATTR_VALUE, CONF_CLIENT_ID, CONF_CLIENT_SECRET, - DOMAIN, SERVICE_COMFORT_FEEDBACK, SERVICE_COMFORT_MODE, - SERVICE_TEMPERATURE_MODE, STORAGE_KEY, STORAGE_VERSION) +from .const import ( + ATTR_VALUE, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + DOMAIN, + SERVICE_COMFORT_FEEDBACK, + SERVICE_COMFORT_MODE, + SERVICE_TEMPERATURE_MODE, + STORAGE_KEY, + STORAGE_VERSION, +) _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE -SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema({ - vol.Required(ATTR_NAME): cv.string, - vol.Required(ATTR_VALUE): cv.string, -}) +SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema( + {vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_VALUE): cv.string} +) -SET_COMFORT_MODE_SCHEMA = vol.Schema({ - vol.Required(ATTR_NAME): cv.string, -}) +SET_COMFORT_MODE_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string}) -SET_TEMPERATURE_MODE_SCHEMA = vol.Schema({ - vol.Required(ATTR_NAME): cv.string, - vol.Required(ATTR_VALUE): cv.string, -}) +SET_TEMPERATURE_MODE_SCHEMA = vol.Schema( + {vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_VALUE): cv.string} +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Ambicliamte device.""" @@ -46,10 +52,12 @@ async def async_setup_entry(hass, entry, async_add_entities): store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) token_info = await store.async_load() - oauth = ambiclimate.AmbiclimateOAuth(config[CONF_CLIENT_ID], - config[CONF_CLIENT_SECRET], - config['callback_url'], - websession) + oauth = ambiclimate.AmbiclimateOAuth( + config[CONF_CLIENT_ID], + config[CONF_CLIENT_SECRET], + config["callback_url"], + websession, + ) try: token_info = await oauth.refresh_access_token(token_info) @@ -62,9 +70,9 @@ async def async_setup_entry(hass, entry, async_add_entities): await store.async_save(token_info) - data_connection = ambiclimate.AmbiclimateConnection(oauth, - token_info=token_info, - websession=websession) + data_connection = ambiclimate.AmbiclimateConnection( + oauth, token_info=token_info, websession=websession + ) if not await data_connection.find_devices(): _LOGGER.error("No devices found") @@ -88,10 +96,12 @@ async def async_setup_entry(hass, entry, async_add_entities): if device: await device.set_comfort_feedback(service.data[ATTR_VALUE]) - hass.services.async_register(DOMAIN, - SERVICE_COMFORT_FEEDBACK, - send_comfort_feedback, - schema=SEND_COMFORT_FEEDBACK_SCHEMA) + hass.services.async_register( + DOMAIN, + SERVICE_COMFORT_FEEDBACK, + send_comfort_feedback, + schema=SEND_COMFORT_FEEDBACK_SCHEMA, + ) async def set_comfort_mode(service): """Set comfort mode.""" @@ -100,10 +110,9 @@ async def async_setup_entry(hass, entry, async_add_entities): if device: await device.set_comfort_mode() - hass.services.async_register(DOMAIN, - SERVICE_COMFORT_MODE, - set_comfort_mode, - schema=SET_COMFORT_MODE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_COMFORT_MODE, set_comfort_mode, schema=SET_COMFORT_MODE_SCHEMA + ) async def set_temperature_mode(service): """Set temperature mode.""" @@ -112,10 +121,12 @@ async def async_setup_entry(hass, entry, async_add_entities): if device: await device.set_temperature_mode(service.data[ATTR_VALUE]) - hass.services.async_register(DOMAIN, - SERVICE_TEMPERATURE_MODE, - set_temperature_mode, - schema=SET_TEMPERATURE_MODE_SCHEMA) + hass.services.async_register( + DOMAIN, + SERVICE_TEMPERATURE_MODE, + set_temperature_mode, + schema=SET_TEMPERATURE_MODE_SCHEMA, + ) class AmbiclimateEntity(ClimateDevice): @@ -141,11 +152,9 @@ class AmbiclimateEntity(ClimateDevice): def device_info(self): """Return the device info.""" return { - 'identifiers': { - (DOMAIN, self.unique_id) - }, - 'name': self.name, - 'manufacturer': 'Ambiclimate', + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Ambiclimate", } @property @@ -156,7 +165,7 @@ class AmbiclimateEntity(ClimateDevice): @property def target_temperature(self): """Return the target temperature.""" - return self._data.get('target_temperature') + return self._data.get("target_temperature") @property def target_temperature_step(self): @@ -166,12 +175,12 @@ class AmbiclimateEntity(ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - return self._data.get('temperature') + return self._data.get("temperature") @property def current_humidity(self): """Return the current humidity.""" - return self._data.get('humidity') + return self._data.get("humidity") @property def min_temp(self): @@ -196,7 +205,7 @@ class AmbiclimateEntity(ClimateDevice): @property def hvac_mode(self): """Return current operation.""" - if self._data.get('power', '').lower() == 'on': + if self._data.get("power", "").lower() == "on": return HVAC_MODE_HEAT return HVAC_MODE_OFF diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 9bbdfceb7b0..db6d42d1d5c 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -7,10 +7,17 @@ from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import (AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, CONF_CLIENT_ID, - CONF_CLIENT_SECRET, DOMAIN, STORAGE_VERSION, STORAGE_KEY) +from .const import ( + AUTH_CALLBACK_NAME, + AUTH_CALLBACK_PATH, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + DOMAIN, + STORAGE_VERSION, + STORAGE_KEY, +) -DATA_AMBICLIMATE_IMPL = 'ambiclimate_flow_implementation' +DATA_AMBICLIMATE_IMPL = "ambiclimate_flow_implementation" _LOGGER = logging.getLogger(__name__) @@ -30,7 +37,7 @@ def register_flow_implementation(hass, client_id, client_secret): } -@config_entries.HANDLERS.register('ambiclimate') +@config_entries.HANDLERS.register("ambiclimate") class AmbiclimateFlowHandler(config_entries.ConfigFlow): """Handle a config flow.""" @@ -45,54 +52,52 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle external yaml configuration.""" if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason='already_setup') + return self.async_abort(reason="already_setup") config = self.hass.data.get(DATA_AMBICLIMATE_IMPL, {}) if not config: _LOGGER.debug("No config") - return self.async_abort(reason='no_config') + return self.async_abort(reason="no_config") return await self.async_step_auth() async def async_step_auth(self, user_input=None): """Handle a flow start.""" if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason='already_setup') + return self.async_abort(reason="already_setup") errors = {} if user_input is not None: - errors['base'] = 'follow_link' + errors["base"] = "follow_link" if not self._registered_view: self._generate_view() return self.async_show_form( - step_id='auth', - description_placeholders={'authorization_url': - await self._get_authorize_url(), - 'cb_url': self._cb_url()}, + step_id="auth", + description_placeholders={ + "authorization_url": await self._get_authorize_url(), + "cb_url": self._cb_url(), + }, errors=errors, ) async def async_step_code(self, code=None): """Received code for authentication.""" if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason='already_setup') + return self.async_abort(reason="already_setup") token_info = await self._get_token_info(code) if token_info is None: - return self.async_abort(reason='access_token') + return self.async_abort(reason="access_token") config = self.hass.data[DATA_AMBICLIMATE_IMPL].copy() - config['callback_url'] = self._cb_url() + config["callback_url"] = self._cb_url() - return self.async_create_entry( - title="Ambiclimate", - data=config, - ) + return self.async_create_entry(title="Ambiclimate", data=config) async def _get_token_info(self, code): oauth = self._generate_oauth() @@ -116,15 +121,16 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow): clientsession = async_get_clientsession(self.hass) callback_url = self._cb_url() - oauth = ambiclimate.AmbiclimateOAuth(config.get(CONF_CLIENT_ID), - config.get(CONF_CLIENT_SECRET), - callback_url, - clientsession) + oauth = ambiclimate.AmbiclimateOAuth( + config.get(CONF_CLIENT_ID), + config.get(CONF_CLIENT_SECRET), + callback_url, + clientsession, + ) return oauth def _cb_url(self): - return '{}{}'.format(self.hass.config.api.base_url, - AUTH_CALLBACK_PATH) + return "{}{}".format(self.hass.config.api.base_url, AUTH_CALLBACK_PATH) async def _get_authorize_url(self): oauth = self._generate_oauth() @@ -140,14 +146,13 @@ class AmbiclimateAuthCallbackView(HomeAssistantView): async def get(self, request): """Receive authorization token.""" - code = request.query.get('code') + code = request.query.get("code") if code is None: return "No code" - hass = request.app['hass'] + hass = request.app["hass"] hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, - context={'source': 'code'}, - data=code, - )) + DOMAIN, context={"source": "code"}, data=code + ) + ) return "OK!" diff --git a/homeassistant/components/ambiclimate/const.py b/homeassistant/components/ambiclimate/const.py index b1b9f4c2767..833fef303f5 100644 --- a/homeassistant/components/ambiclimate/const.py +++ b/homeassistant/components/ambiclimate/const.py @@ -1,14 +1,14 @@ """Constants used by the Ambiclimate component.""" -ATTR_VALUE = 'value' -CONF_CLIENT_ID = 'client_id' -CONF_CLIENT_SECRET = 'client_secret' -DOMAIN = 'ambiclimate' -SERVICE_COMFORT_FEEDBACK = 'send_comfort_feedback' -SERVICE_COMFORT_MODE = 'set_comfort_mode' -SERVICE_TEMPERATURE_MODE = 'set_temperature_mode' -STORAGE_KEY = 'ambiclimate_auth' +ATTR_VALUE = "value" +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" +DOMAIN = "ambiclimate" +SERVICE_COMFORT_FEEDBACK = "send_comfort_feedback" +SERVICE_COMFORT_MODE = "set_comfort_mode" +SERVICE_TEMPERATURE_MODE = "set_temperature_mode" +STORAGE_KEY = "ambiclimate_auth" STORAGE_VERSION = 1 -AUTH_CALLBACK_NAME = 'api:ambiclimate' -AUTH_CALLBACK_PATH = '/api/ambiclimate' +AUTH_CALLBACK_NAME = "api:ambiclimate" +AUTH_CALLBACK_PATH = "/api/ambiclimate" diff --git a/homeassistant/components/ambient_station/.translations/pt-BR.json b/homeassistant/components/ambient_station/.translations/pt-BR.json new file mode 100644 index 00000000000..61f5cea5e26 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Chave de aplicativo e / ou chave de API j\u00e1 registrada", + "invalid_key": "Chave de API e / ou chave de aplicativo inv\u00e1lidas", + "no_devices": "Nenhum dispositivo encontrado na conta" + }, + "step": { + "user": { + "data": { + "api_key": "Chave API", + "app_key": "Chave de aplicativo" + }, + "title": "Preencha suas informa\u00e7\u00f5es" + } + }, + "title": "Ambiente PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 40487040474..82c29f79983 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -1,224 +1,241 @@ """Support for Ambient Weather Station Service.""" import logging +from aioambient import Client +from aioambient.errors import WebsocketError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( - ATTR_NAME, ATTR_LOCATION, CONF_API_KEY, CONF_MONITORED_CONDITIONS, - EVENT_HOMEASSISTANT_STOP) + ATTR_NAME, + ATTR_LOCATION, + CONF_API_KEY, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, async_dispatcher_send) + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later from .config_flow import configured_instances from .const import ( - ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, - TYPE_BINARY_SENSOR, TYPE_SENSOR) + ATTR_LAST_DATA, + CONF_APP_KEY, + DATA_CLIENT, + DOMAIN, + TOPIC_UPDATE, + TYPE_BINARY_SENSOR, + TYPE_SENSOR, +) _LOGGER = logging.getLogger(__name__) -DATA_CONFIG = 'config' +DATA_CONFIG = "config" DEFAULT_SOCKET_MIN_RETRY = 15 DEFAULT_WATCHDOG_SECONDS = 5 * 60 -TYPE_24HOURRAININ = '24hourrainin' -TYPE_BAROMABSIN = 'baromabsin' -TYPE_BAROMRELIN = 'baromrelin' -TYPE_BATT1 = 'batt1' -TYPE_BATT10 = 'batt10' -TYPE_BATT2 = 'batt2' -TYPE_BATT3 = 'batt3' -TYPE_BATT4 = 'batt4' -TYPE_BATT5 = 'batt5' -TYPE_BATT6 = 'batt6' -TYPE_BATT7 = 'batt7' -TYPE_BATT8 = 'batt8' -TYPE_BATT9 = 'batt9' -TYPE_BATTOUT = 'battout' -TYPE_CO2 = 'co2' -TYPE_DAILYRAININ = 'dailyrainin' -TYPE_DEWPOINT = 'dewPoint' -TYPE_EVENTRAININ = 'eventrainin' -TYPE_FEELSLIKE = 'feelsLike' -TYPE_HOURLYRAININ = 'hourlyrainin' -TYPE_HUMIDITY = 'humidity' -TYPE_HUMIDITY1 = 'humidity1' -TYPE_HUMIDITY10 = 'humidity10' -TYPE_HUMIDITY2 = 'humidity2' -TYPE_HUMIDITY3 = 'humidity3' -TYPE_HUMIDITY4 = 'humidity4' -TYPE_HUMIDITY5 = 'humidity5' -TYPE_HUMIDITY6 = 'humidity6' -TYPE_HUMIDITY7 = 'humidity7' -TYPE_HUMIDITY8 = 'humidity8' -TYPE_HUMIDITY9 = 'humidity9' -TYPE_HUMIDITYIN = 'humidityin' -TYPE_LASTRAIN = 'lastRain' -TYPE_MAXDAILYGUST = 'maxdailygust' -TYPE_MONTHLYRAININ = 'monthlyrainin' -TYPE_RELAY1 = 'relay1' -TYPE_RELAY10 = 'relay10' -TYPE_RELAY2 = 'relay2' -TYPE_RELAY3 = 'relay3' -TYPE_RELAY4 = 'relay4' -TYPE_RELAY5 = 'relay5' -TYPE_RELAY6 = 'relay6' -TYPE_RELAY7 = 'relay7' -TYPE_RELAY8 = 'relay8' -TYPE_RELAY9 = 'relay9' -TYPE_SOILHUM1 = 'soilhum1' -TYPE_SOILHUM10 = 'soilhum10' -TYPE_SOILHUM2 = 'soilhum2' -TYPE_SOILHUM3 = 'soilhum3' -TYPE_SOILHUM4 = 'soilhum4' -TYPE_SOILHUM5 = 'soilhum5' -TYPE_SOILHUM6 = 'soilhum6' -TYPE_SOILHUM7 = 'soilhum7' -TYPE_SOILHUM8 = 'soilhum8' -TYPE_SOILHUM9 = 'soilhum9' -TYPE_SOILTEMP1F = 'soiltemp1f' -TYPE_SOILTEMP10F = 'soiltemp10f' -TYPE_SOILTEMP2F = 'soiltemp2f' -TYPE_SOILTEMP3F = 'soiltemp3f' -TYPE_SOILTEMP4F = 'soiltemp4f' -TYPE_SOILTEMP5F = 'soiltemp5f' -TYPE_SOILTEMP6F = 'soiltemp6f' -TYPE_SOILTEMP7F = 'soiltemp7f' -TYPE_SOILTEMP8F = 'soiltemp8f' -TYPE_SOILTEMP9F = 'soiltemp9f' -TYPE_SOLARRADIATION = 'solarradiation' -TYPE_TEMP10F = 'temp10f' -TYPE_TEMP1F = 'temp1f' -TYPE_TEMP2F = 'temp2f' -TYPE_TEMP3F = 'temp3f' -TYPE_TEMP4F = 'temp4f' -TYPE_TEMP5F = 'temp5f' -TYPE_TEMP6F = 'temp6f' -TYPE_TEMP7F = 'temp7f' -TYPE_TEMP8F = 'temp8f' -TYPE_TEMP9F = 'temp9f' -TYPE_TEMPF = 'tempf' -TYPE_TEMPINF = 'tempinf' -TYPE_TOTALRAININ = 'totalrainin' -TYPE_UV = 'uv' -TYPE_WEEKLYRAININ = 'weeklyrainin' -TYPE_WINDDIR = 'winddir' -TYPE_WINDDIR_AVG10M = 'winddir_avg10m' -TYPE_WINDDIR_AVG2M = 'winddir_avg2m' -TYPE_WINDGUSTDIR = 'windgustdir' -TYPE_WINDGUSTMPH = 'windgustmph' -TYPE_WINDSPDMPH_AVG10M = 'windspdmph_avg10m' -TYPE_WINDSPDMPH_AVG2M = 'windspdmph_avg2m' -TYPE_WINDSPEEDMPH = 'windspeedmph' -TYPE_YEARLYRAININ = 'yearlyrainin' +TYPE_24HOURRAININ = "24hourrainin" +TYPE_BAROMABSIN = "baromabsin" +TYPE_BAROMRELIN = "baromrelin" +TYPE_BATT1 = "batt1" +TYPE_BATT10 = "batt10" +TYPE_BATT2 = "batt2" +TYPE_BATT3 = "batt3" +TYPE_BATT4 = "batt4" +TYPE_BATT5 = "batt5" +TYPE_BATT6 = "batt6" +TYPE_BATT7 = "batt7" +TYPE_BATT8 = "batt8" +TYPE_BATT9 = "batt9" +TYPE_BATTOUT = "battout" +TYPE_CO2 = "co2" +TYPE_DAILYRAININ = "dailyrainin" +TYPE_DEWPOINT = "dewPoint" +TYPE_EVENTRAININ = "eventrainin" +TYPE_FEELSLIKE = "feelsLike" +TYPE_HOURLYRAININ = "hourlyrainin" +TYPE_HUMIDITY = "humidity" +TYPE_HUMIDITY1 = "humidity1" +TYPE_HUMIDITY10 = "humidity10" +TYPE_HUMIDITY2 = "humidity2" +TYPE_HUMIDITY3 = "humidity3" +TYPE_HUMIDITY4 = "humidity4" +TYPE_HUMIDITY5 = "humidity5" +TYPE_HUMIDITY6 = "humidity6" +TYPE_HUMIDITY7 = "humidity7" +TYPE_HUMIDITY8 = "humidity8" +TYPE_HUMIDITY9 = "humidity9" +TYPE_HUMIDITYIN = "humidityin" +TYPE_LASTRAIN = "lastRain" +TYPE_MAXDAILYGUST = "maxdailygust" +TYPE_MONTHLYRAININ = "monthlyrainin" +TYPE_RELAY1 = "relay1" +TYPE_RELAY10 = "relay10" +TYPE_RELAY2 = "relay2" +TYPE_RELAY3 = "relay3" +TYPE_RELAY4 = "relay4" +TYPE_RELAY5 = "relay5" +TYPE_RELAY6 = "relay6" +TYPE_RELAY7 = "relay7" +TYPE_RELAY8 = "relay8" +TYPE_RELAY9 = "relay9" +TYPE_SOILHUM1 = "soilhum1" +TYPE_SOILHUM10 = "soilhum10" +TYPE_SOILHUM2 = "soilhum2" +TYPE_SOILHUM3 = "soilhum3" +TYPE_SOILHUM4 = "soilhum4" +TYPE_SOILHUM5 = "soilhum5" +TYPE_SOILHUM6 = "soilhum6" +TYPE_SOILHUM7 = "soilhum7" +TYPE_SOILHUM8 = "soilhum8" +TYPE_SOILHUM9 = "soilhum9" +TYPE_SOILTEMP1F = "soiltemp1f" +TYPE_SOILTEMP10F = "soiltemp10f" +TYPE_SOILTEMP2F = "soiltemp2f" +TYPE_SOILTEMP3F = "soiltemp3f" +TYPE_SOILTEMP4F = "soiltemp4f" +TYPE_SOILTEMP5F = "soiltemp5f" +TYPE_SOILTEMP6F = "soiltemp6f" +TYPE_SOILTEMP7F = "soiltemp7f" +TYPE_SOILTEMP8F = "soiltemp8f" +TYPE_SOILTEMP9F = "soiltemp9f" +TYPE_SOLARRADIATION = "solarradiation" +TYPE_SOLARRADIATION_LX = "solarradiation_lx" +TYPE_TEMP10F = "temp10f" +TYPE_TEMP1F = "temp1f" +TYPE_TEMP2F = "temp2f" +TYPE_TEMP3F = "temp3f" +TYPE_TEMP4F = "temp4f" +TYPE_TEMP5F = "temp5f" +TYPE_TEMP6F = "temp6f" +TYPE_TEMP7F = "temp7f" +TYPE_TEMP8F = "temp8f" +TYPE_TEMP9F = "temp9f" +TYPE_TEMPF = "tempf" +TYPE_TEMPINF = "tempinf" +TYPE_TOTALRAININ = "totalrainin" +TYPE_UV = "uv" +TYPE_WEEKLYRAININ = "weeklyrainin" +TYPE_WINDDIR = "winddir" +TYPE_WINDDIR_AVG10M = "winddir_avg10m" +TYPE_WINDDIR_AVG2M = "winddir_avg2m" +TYPE_WINDGUSTDIR = "windgustdir" +TYPE_WINDGUSTMPH = "windgustmph" +TYPE_WINDSPDMPH_AVG10M = "windspdmph_avg10m" +TYPE_WINDSPDMPH_AVG2M = "windspdmph_avg2m" +TYPE_WINDSPEEDMPH = "windspeedmph" +TYPE_YEARLYRAININ = "yearlyrainin" SENSOR_TYPES = { - TYPE_24HOURRAININ: ('24 Hr Rain', 'in', TYPE_SENSOR, None), - TYPE_BAROMABSIN: ('Abs Pressure', 'inHg', TYPE_SENSOR, 'pressure'), - TYPE_BAROMRELIN: ('Rel Pressure', 'inHg', TYPE_SENSOR, 'pressure'), - TYPE_BATT10: ('Battery 10', None, TYPE_BINARY_SENSOR, 'battery'), - TYPE_BATT1: ('Battery 1', None, TYPE_BINARY_SENSOR, 'battery'), - TYPE_BATT2: ('Battery 2', None, TYPE_BINARY_SENSOR, 'battery'), - TYPE_BATT3: ('Battery 3', None, TYPE_BINARY_SENSOR, 'battery'), - TYPE_BATT4: ('Battery 4', None, TYPE_BINARY_SENSOR, 'battery'), - TYPE_BATT5: ('Battery 5', None, TYPE_BINARY_SENSOR, 'battery'), - TYPE_BATT6: ('Battery 6', None, TYPE_BINARY_SENSOR, 'battery'), - TYPE_BATT7: ('Battery 7', None, TYPE_BINARY_SENSOR, 'battery'), - TYPE_BATT8: ('Battery 8', None, TYPE_BINARY_SENSOR, 'battery'), - TYPE_BATT9: ('Battery 9', None, TYPE_BINARY_SENSOR, 'battery'), - TYPE_BATTOUT: ('Battery', None, TYPE_BINARY_SENSOR, 'battery'), - TYPE_CO2: ('co2', 'ppm', TYPE_SENSOR, None), - TYPE_DAILYRAININ: ('Daily Rain', 'in', TYPE_SENSOR, None), - TYPE_DEWPOINT: ('Dew Point', '°F', TYPE_SENSOR, 'temperature'), - TYPE_EVENTRAININ: ('Event Rain', 'in', TYPE_SENSOR, None), - TYPE_FEELSLIKE: ('Feels Like', '°F', TYPE_SENSOR, 'temperature'), - TYPE_HOURLYRAININ: ('Hourly Rain Rate', 'in/hr', TYPE_SENSOR, None), - TYPE_HUMIDITY10: ('Humidity 10', '%', TYPE_SENSOR, 'humidity'), - TYPE_HUMIDITY1: ('Humidity 1', '%', TYPE_SENSOR, 'humidity'), - TYPE_HUMIDITY2: ('Humidity 2', '%', TYPE_SENSOR, 'humidity'), - TYPE_HUMIDITY3: ('Humidity 3', '%', TYPE_SENSOR, 'humidity'), - TYPE_HUMIDITY4: ('Humidity 4', '%', TYPE_SENSOR, 'humidity'), - TYPE_HUMIDITY5: ('Humidity 5', '%', TYPE_SENSOR, 'humidity'), - TYPE_HUMIDITY6: ('Humidity 6', '%', TYPE_SENSOR, 'humidity'), - TYPE_HUMIDITY7: ('Humidity 7', '%', TYPE_SENSOR, 'humidity'), - TYPE_HUMIDITY8: ('Humidity 8', '%', TYPE_SENSOR, 'humidity'), - TYPE_HUMIDITY9: ('Humidity 9', '%', TYPE_SENSOR, 'humidity'), - TYPE_HUMIDITY: ('Humidity', '%', TYPE_SENSOR, 'humidity'), - TYPE_HUMIDITYIN: ('Humidity In', '%', TYPE_SENSOR, 'humidity'), - TYPE_LASTRAIN: ('Last Rain', None, TYPE_SENSOR, 'timestamp'), - TYPE_MAXDAILYGUST: ('Max Gust', 'mph', TYPE_SENSOR, None), - TYPE_MONTHLYRAININ: ('Monthly Rain', 'in', TYPE_SENSOR, None), - TYPE_RELAY10: ('Relay 10', None, TYPE_BINARY_SENSOR, 'connectivity'), - TYPE_RELAY1: ('Relay 1', None, TYPE_BINARY_SENSOR, 'connectivity'), - TYPE_RELAY2: ('Relay 2', None, TYPE_BINARY_SENSOR, 'connectivity'), - TYPE_RELAY3: ('Relay 3', None, TYPE_BINARY_SENSOR, 'connectivity'), - TYPE_RELAY4: ('Relay 4', None, TYPE_BINARY_SENSOR, 'connectivity'), - TYPE_RELAY5: ('Relay 5', None, TYPE_BINARY_SENSOR, 'connectivity'), - TYPE_RELAY6: ('Relay 6', None, TYPE_BINARY_SENSOR, 'connectivity'), - TYPE_RELAY7: ('Relay 7', None, TYPE_BINARY_SENSOR, 'connectivity'), - TYPE_RELAY8: ('Relay 8', None, TYPE_BINARY_SENSOR, 'connectivity'), - TYPE_RELAY9: ('Relay 9', None, TYPE_BINARY_SENSOR, 'connectivity'), - TYPE_SOILHUM10: ('Soil Humidity 10', '%', TYPE_SENSOR, 'humidity'), - TYPE_SOILHUM1: ('Soil Humidity 1', '%', TYPE_SENSOR, 'humidity'), - TYPE_SOILHUM2: ('Soil Humidity 2', '%', TYPE_SENSOR, 'humidity'), - TYPE_SOILHUM3: ('Soil Humidity 3', '%', TYPE_SENSOR, 'humidity'), - TYPE_SOILHUM4: ('Soil Humidity 4', '%', TYPE_SENSOR, 'humidity'), - TYPE_SOILHUM5: ('Soil Humidity 5', '%', TYPE_SENSOR, 'humidity'), - TYPE_SOILHUM6: ('Soil Humidity 6', '%', TYPE_SENSOR, 'humidity'), - TYPE_SOILHUM7: ('Soil Humidity 7', '%', TYPE_SENSOR, 'humidity'), - TYPE_SOILHUM8: ('Soil Humidity 8', '%', TYPE_SENSOR, 'humidity'), - TYPE_SOILHUM9: ('Soil Humidity 9', '%', TYPE_SENSOR, 'humidity'), - TYPE_SOILTEMP10F: ('Soil Temp 10', '°F', TYPE_SENSOR, 'temperature'), - TYPE_SOILTEMP1F: ('Soil Temp 1', '°F', TYPE_SENSOR, 'temperature'), - TYPE_SOILTEMP2F: ('Soil Temp 2', '°F', TYPE_SENSOR, 'temperature'), - TYPE_SOILTEMP3F: ('Soil Temp 3', '°F', TYPE_SENSOR, 'temperature'), - TYPE_SOILTEMP4F: ('Soil Temp 4', '°F', TYPE_SENSOR, 'temperature'), - TYPE_SOILTEMP5F: ('Soil Temp 5', '°F', TYPE_SENSOR, 'temperature'), - TYPE_SOILTEMP6F: ('Soil Temp 6', '°F', TYPE_SENSOR, 'temperature'), - TYPE_SOILTEMP7F: ('Soil Temp 7', '°F', TYPE_SENSOR, 'temperature'), - TYPE_SOILTEMP8F: ('Soil Temp 8', '°F', TYPE_SENSOR, 'temperature'), - TYPE_SOILTEMP9F: ('Soil Temp 9', '°F', TYPE_SENSOR, 'temperature'), - TYPE_SOLARRADIATION: ('Solar Rad', 'lx', TYPE_SENSOR, 'illuminance'), - TYPE_TEMP10F: ('Temp 10', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TEMP1F: ('Temp 1', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TEMP2F: ('Temp 2', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TEMP3F: ('Temp 3', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TEMP4F: ('Temp 4', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TEMP5F: ('Temp 5', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TEMP6F: ('Temp 6', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TEMP7F: ('Temp 7', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TEMP8F: ('Temp 8', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TEMP9F: ('Temp 9', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TEMPF: ('Temp', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TEMPINF: ('Inside Temp', '°F', TYPE_SENSOR, 'temperature'), - TYPE_TOTALRAININ: ('Lifetime Rain', 'in', TYPE_SENSOR, None), - TYPE_UV: ('uv', 'Index', TYPE_SENSOR, None), - TYPE_WEEKLYRAININ: ('Weekly Rain', 'in', TYPE_SENSOR, None), - TYPE_WINDDIR: ('Wind Dir', '°', TYPE_SENSOR, None), - TYPE_WINDDIR_AVG10M: ('Wind Dir Avg 10m', '°', TYPE_SENSOR, None), - TYPE_WINDDIR_AVG2M: ('Wind Dir Avg 2m', 'mph', TYPE_SENSOR, None), - TYPE_WINDGUSTDIR: ('Gust Dir', '°', TYPE_SENSOR, None), - TYPE_WINDGUSTMPH: ('Wind Gust', 'mph', TYPE_SENSOR, None), - TYPE_WINDSPDMPH_AVG10M: ('Wind Avg 10m', 'mph', TYPE_SENSOR, None), - TYPE_WINDSPDMPH_AVG2M: ('Wind Avg 2m', 'mph', TYPE_SENSOR, None), - TYPE_WINDSPEEDMPH: ('Wind Speed', 'mph', TYPE_SENSOR, None), - TYPE_YEARLYRAININ: ('Yearly Rain', 'in', TYPE_SENSOR, None), + TYPE_24HOURRAININ: ("24 Hr Rain", "in", TYPE_SENSOR, None), + TYPE_BAROMABSIN: ("Abs Pressure", "inHg", TYPE_SENSOR, "pressure"), + TYPE_BAROMRELIN: ("Rel Pressure", "inHg", TYPE_SENSOR, "pressure"), + TYPE_BATT10: ("Battery 10", None, TYPE_BINARY_SENSOR, "battery"), + TYPE_BATT1: ("Battery 1", None, TYPE_BINARY_SENSOR, "battery"), + TYPE_BATT2: ("Battery 2", None, TYPE_BINARY_SENSOR, "battery"), + TYPE_BATT3: ("Battery 3", None, TYPE_BINARY_SENSOR, "battery"), + TYPE_BATT4: ("Battery 4", None, TYPE_BINARY_SENSOR, "battery"), + TYPE_BATT5: ("Battery 5", None, TYPE_BINARY_SENSOR, "battery"), + TYPE_BATT6: ("Battery 6", None, TYPE_BINARY_SENSOR, "battery"), + TYPE_BATT7: ("Battery 7", None, TYPE_BINARY_SENSOR, "battery"), + TYPE_BATT8: ("Battery 8", None, TYPE_BINARY_SENSOR, "battery"), + TYPE_BATT9: ("Battery 9", None, TYPE_BINARY_SENSOR, "battery"), + TYPE_BATTOUT: ("Battery", None, TYPE_BINARY_SENSOR, "battery"), + TYPE_CO2: ("co2", "ppm", TYPE_SENSOR, None), + TYPE_DAILYRAININ: ("Daily Rain", "in", TYPE_SENSOR, None), + TYPE_DEWPOINT: ("Dew Point", "°F", TYPE_SENSOR, "temperature"), + TYPE_EVENTRAININ: ("Event Rain", "in", TYPE_SENSOR, None), + TYPE_FEELSLIKE: ("Feels Like", "°F", TYPE_SENSOR, "temperature"), + TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", TYPE_SENSOR, None), + TYPE_HUMIDITY10: ("Humidity 10", "%", TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY1: ("Humidity 1", "%", TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY2: ("Humidity 2", "%", TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY3: ("Humidity 3", "%", TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY4: ("Humidity 4", "%", TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY5: ("Humidity 5", "%", TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY6: ("Humidity 6", "%", TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY7: ("Humidity 7", "%", TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY8: ("Humidity 8", "%", TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY9: ("Humidity 9", "%", TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY: ("Humidity", "%", TYPE_SENSOR, "humidity"), + TYPE_HUMIDITYIN: ("Humidity In", "%", TYPE_SENSOR, "humidity"), + TYPE_LASTRAIN: ("Last Rain", None, TYPE_SENSOR, "timestamp"), + TYPE_MAXDAILYGUST: ("Max Gust", "mph", TYPE_SENSOR, None), + TYPE_MONTHLYRAININ: ("Monthly Rain", "in", TYPE_SENSOR, None), + TYPE_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, "connectivity"), + TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, "connectivity"), + TYPE_RELAY2: ("Relay 2", None, TYPE_BINARY_SENSOR, "connectivity"), + TYPE_RELAY3: ("Relay 3", None, TYPE_BINARY_SENSOR, "connectivity"), + TYPE_RELAY4: ("Relay 4", None, TYPE_BINARY_SENSOR, "connectivity"), + TYPE_RELAY5: ("Relay 5", None, TYPE_BINARY_SENSOR, "connectivity"), + TYPE_RELAY6: ("Relay 6", None, TYPE_BINARY_SENSOR, "connectivity"), + TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, "connectivity"), + TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, "connectivity"), + TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, "connectivity"), + TYPE_SOILHUM10: ("Soil Humidity 10", "%", TYPE_SENSOR, "humidity"), + TYPE_SOILHUM1: ("Soil Humidity 1", "%", TYPE_SENSOR, "humidity"), + TYPE_SOILHUM2: ("Soil Humidity 2", "%", TYPE_SENSOR, "humidity"), + TYPE_SOILHUM3: ("Soil Humidity 3", "%", TYPE_SENSOR, "humidity"), + TYPE_SOILHUM4: ("Soil Humidity 4", "%", TYPE_SENSOR, "humidity"), + TYPE_SOILHUM5: ("Soil Humidity 5", "%", TYPE_SENSOR, "humidity"), + TYPE_SOILHUM6: ("Soil Humidity 6", "%", TYPE_SENSOR, "humidity"), + TYPE_SOILHUM7: ("Soil Humidity 7", "%", TYPE_SENSOR, "humidity"), + TYPE_SOILHUM8: ("Soil Humidity 8", "%", TYPE_SENSOR, "humidity"), + TYPE_SOILHUM9: ("Soil Humidity 9", "%", TYPE_SENSOR, "humidity"), + TYPE_SOILTEMP10F: ("Soil Temp 10", "°F", TYPE_SENSOR, "temperature"), + TYPE_SOILTEMP1F: ("Soil Temp 1", "°F", TYPE_SENSOR, "temperature"), + TYPE_SOILTEMP2F: ("Soil Temp 2", "°F", TYPE_SENSOR, "temperature"), + TYPE_SOILTEMP3F: ("Soil Temp 3", "°F", TYPE_SENSOR, "temperature"), + TYPE_SOILTEMP4F: ("Soil Temp 4", "°F", TYPE_SENSOR, "temperature"), + TYPE_SOILTEMP5F: ("Soil Temp 5", "°F", TYPE_SENSOR, "temperature"), + TYPE_SOILTEMP6F: ("Soil Temp 6", "°F", TYPE_SENSOR, "temperature"), + TYPE_SOILTEMP7F: ("Soil Temp 7", "°F", TYPE_SENSOR, "temperature"), + TYPE_SOILTEMP8F: ("Soil Temp 8", "°F", TYPE_SENSOR, "temperature"), + TYPE_SOILTEMP9F: ("Soil Temp 9", "°F", TYPE_SENSOR, "temperature"), + TYPE_SOLARRADIATION: ("Solar Rad", "W/m^2", TYPE_SENSOR, None), + TYPE_SOLARRADIATION_LX: ("Solar Rad (lx)", "lx", TYPE_SENSOR, "illuminance"), + TYPE_TEMP10F: ("Temp 10", "°F", TYPE_SENSOR, "temperature"), + TYPE_TEMP1F: ("Temp 1", "°F", TYPE_SENSOR, "temperature"), + TYPE_TEMP2F: ("Temp 2", "°F", TYPE_SENSOR, "temperature"), + TYPE_TEMP3F: ("Temp 3", "°F", TYPE_SENSOR, "temperature"), + TYPE_TEMP4F: ("Temp 4", "°F", TYPE_SENSOR, "temperature"), + TYPE_TEMP5F: ("Temp 5", "°F", TYPE_SENSOR, "temperature"), + TYPE_TEMP6F: ("Temp 6", "°F", TYPE_SENSOR, "temperature"), + TYPE_TEMP7F: ("Temp 7", "°F", TYPE_SENSOR, "temperature"), + TYPE_TEMP8F: ("Temp 8", "°F", TYPE_SENSOR, "temperature"), + TYPE_TEMP9F: ("Temp 9", "°F", TYPE_SENSOR, "temperature"), + TYPE_TEMPF: ("Temp", "°F", TYPE_SENSOR, "temperature"), + TYPE_TEMPINF: ("Inside Temp", "°F", TYPE_SENSOR, "temperature"), + TYPE_TOTALRAININ: ("Lifetime Rain", "in", TYPE_SENSOR, None), + TYPE_UV: ("uv", "Index", TYPE_SENSOR, None), + TYPE_WEEKLYRAININ: ("Weekly Rain", "in", TYPE_SENSOR, None), + TYPE_WINDDIR: ("Wind Dir", "°", TYPE_SENSOR, None), + TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", "°", TYPE_SENSOR, None), + TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", "mph", TYPE_SENSOR, None), + TYPE_WINDGUSTDIR: ("Gust Dir", "°", TYPE_SENSOR, None), + TYPE_WINDGUSTMPH: ("Wind Gust", "mph", TYPE_SENSOR, None), + TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", "mph", TYPE_SENSOR, None), + TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", "mph", TYPE_SENSOR, None), + TYPE_WINDSPEEDMPH: ("Wind Speed", "mph", TYPE_SENSOR, None), + TYPE_YEARLYRAININ: ("Yearly Rain", "in", TYPE_SENSOR, None), } -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: - vol.Schema({ - vol.Required(CONF_APP_KEY): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_APP_KEY): cv.string, + vol.Required(CONF_API_KEY): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -240,38 +257,37 @@ async def async_setup(hass, config): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={'source': SOURCE_IMPORT}, - data={ - CONF_API_KEY: conf[CONF_API_KEY], - CONF_APP_KEY: conf[CONF_APP_KEY] - })) + context={"source": SOURCE_IMPORT}, + data={CONF_API_KEY: conf[CONF_API_KEY], CONF_APP_KEY: conf[CONF_APP_KEY]}, + ) + ) return True async def async_setup_entry(hass, config_entry): """Set up the Ambient PWS as config entry.""" - from aioambient import Client - from aioambient.errors import WebsocketError - session = aiohttp_client.async_get_clientsession(hass) try: ambient = AmbientStation( - hass, config_entry, + hass, + config_entry, Client( config_entry.data[CONF_API_KEY], - config_entry.data[CONF_APP_KEY], session), - hass.data[DOMAIN].get(DATA_CONFIG, {}).get( - CONF_MONITORED_CONDITIONS, [])) + config_entry.data[CONF_APP_KEY], + session, + ), + ) hass.loop.create_task(ambient.ws_connect()) hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient except WebsocketError as err: - _LOGGER.error('Config entry failed: %s', err) + _LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, ambient.client.websocket.disconnect()) + EVENT_HOMEASSISTANT_STOP, ambient.client.websocket.disconnect() + ) return True @@ -281,9 +297,30 @@ async def async_unload_entry(hass, config_entry): ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) hass.async_create_task(ambient.ws_disconnect()) - for component in ('binary_sensor', 'sensor'): - await hass.config_entries.async_forward_entry_unload( - config_entry, component) + for component in ("binary_sensor", "sensor"): + await hass.config_entries.async_forward_entry_unload(config_entry, component) + + return True + + +async def async_migrate_entry(hass, config_entry): + """Migrate old entry.""" + version = config_entry.version + + _LOGGER.debug("Migrating from version %s", version) + + # 1 -> 2: Unique ID format changed, so delete and re-import: + if version == 1: + dev_reg = await hass.helpers.device_registry.async_get_registry() + dev_reg.async_clear_config_entry(config_entry) + + en_reg = await hass.helpers.entity_registry.async_get_registry() + en_reg.async_clear_config_entry(config_entry) + + version = config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry) + + _LOGGER.info("Migration to version %s successful", version) return True @@ -291,7 +328,7 @@ async def async_unload_entry(hass, config_entry): class AmbientStation: """Define a class to handle the Ambient websocket.""" - def __init__(self, hass, config_entry, client, monitored_conditions): + def __init__(self, hass, config_entry, client): """Initialize.""" self._config_entry = config_entry self._entry_setup_complete = False @@ -299,79 +336,79 @@ class AmbientStation: self._watchdog_listener = None self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY self.client = client - self.monitored_conditions = monitored_conditions + self.monitored_conditions = [] self.stations = {} async def _attempt_connect(self): """Attempt to connect to the socket (retrying later on fail).""" - from aioambient.errors import WebsocketError - try: await self.client.websocket.connect() except WebsocketError as err: _LOGGER.error("Error with the websocket connection: %s", err) - self._ws_reconnect_delay = min( - 2 * self._ws_reconnect_delay, 480) - async_call_later( - self._hass, self._ws_reconnect_delay, self.ws_connect) + self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480) + async_call_later(self._hass, self._ws_reconnect_delay, self.ws_connect) async def ws_connect(self): """Register handlers and connect to the websocket.""" + async def _ws_reconnect(event_time): """Forcibly disconnect from and reconnect to the websocket.""" - _LOGGER.debug('Watchdog expired; forcing socket reconnection') + _LOGGER.debug("Watchdog expired; forcing socket reconnection") await self.client.websocket.disconnect() await self._attempt_connect() def on_connect(): """Define a handler to fire when the websocket is connected.""" - _LOGGER.info('Connected to websocket') - _LOGGER.debug('Watchdog starting') + _LOGGER.info("Connected to websocket") + _LOGGER.debug("Watchdog starting") if self._watchdog_listener is not None: self._watchdog_listener() self._watchdog_listener = async_call_later( - self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect) + self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect + ) def on_data(data): """Define a handler to fire when the data is received.""" - mac_address = data['macAddress'] + mac_address = data["macAddress"] if data != self.stations[mac_address][ATTR_LAST_DATA]: - _LOGGER.debug('New data received: %s', data) + _LOGGER.debug("New data received: %s", data) self.stations[mac_address][ATTR_LAST_DATA] = data async_dispatcher_send(self._hass, TOPIC_UPDATE) - _LOGGER.debug('Resetting watchdog') + _LOGGER.debug("Resetting watchdog") self._watchdog_listener() self._watchdog_listener = async_call_later( - self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect) + self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect + ) def on_disconnect(): """Define a handler to fire when the websocket is disconnected.""" - _LOGGER.info('Disconnected from websocket') + _LOGGER.info("Disconnected from websocket") def on_subscribed(data): """Define a handler to fire when the subscription is set.""" - for station in data['devices']: - if station['macAddress'] in self.stations: + for station in data["devices"]: + if station["macAddress"] in self.stations: continue - _LOGGER.debug('New station subscription: %s', data) + _LOGGER.debug("New station subscription: %s", data) - # If the user hasn't specified monitored conditions, use only - # those that their station supports (and which are defined - # here): - if not self.monitored_conditions: - self.monitored_conditions = [ - k for k in station['lastData'].keys() - if k in SENSOR_TYPES - ] + self.monitored_conditions = [ + k for k in station["lastData"] if k in SENSOR_TYPES + ] - self.stations[station['macAddress']] = { - ATTR_LAST_DATA: station['lastData'], - ATTR_LOCATION: station.get('info', {}).get('location'), - ATTR_NAME: - station.get('info', {}).get( - 'name', station['macAddress']), + # If the user is monitoring brightness (in W/m^2), + # make sure we also add a calculated sensor for the + # same data measured in lx: + if TYPE_SOLARRADIATION in self.monitored_conditions: + self.monitored_conditions.append(TYPE_SOLARRADIATION_LX) + + self.stations[station["macAddress"]] = { + ATTR_LAST_DATA: station["lastData"], + ATTR_LOCATION: station.get("info", {}).get("location"), + ATTR_NAME: station.get("info", {}).get( + "name", station["macAddress"] + ), } # If the websocket disconnects and reconnects, the on_subscribed @@ -379,10 +416,12 @@ class AmbientStation: # attempt forward setup of the config entry (because it will have # already been done): if not self._entry_setup_complete: - for component in ('binary_sensor', 'sensor'): + for component in ("binary_sensor", "sensor"): self._hass.async_create_task( self._hass.config_entries.async_forward_entry_setup( - self._config_entry, component)) + self._config_entry, component + ) + ) self._entry_setup_complete = True self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY @@ -403,8 +442,8 @@ class AmbientWeatherEntity(Entity): """Define a base Ambient PWS entity.""" def __init__( - self, ambient, mac_address, station_name, sensor_type, - sensor_name, device_class): + self, ambient, mac_address, station_name, sensor_type, sensor_name, device_class + ): """Initialize the sensor.""" self._ambient = ambient self._device_class = device_class @@ -418,8 +457,23 @@ class AmbientWeatherEntity(Entity): @property def available(self): """Return True if entity is available.""" - return self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( - self._sensor_type) is not None + # Since the solarradiation_lx sensor is created only if the + # user shows a solarradiation sensor, ensure that the + # solarradiation_lx sensor shows as available if the solarradiation + # sensor is available: + if self._sensor_type == TYPE_SOLARRADIATION_LX: + return ( + self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + TYPE_SOLARRADIATION + ) + is not None + ) + return ( + self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + self._sensor_type + ) + is not None + ) @property def device_class(self): @@ -430,17 +484,15 @@ class AmbientWeatherEntity(Entity): def device_info(self): """Return device registry information for this entity.""" return { - 'identifiers': { - (DOMAIN, self._mac_address) - }, - 'name': self._station_name, - 'manufacturer': 'Ambient Weather', + "identifiers": {(DOMAIN, self._mac_address)}, + "name": self._station_name, + "manufacturer": "Ambient Weather", } @property def name(self): """Return the name of the sensor.""" - return '{0}_{1}'.format(self._station_name, self._sensor_name) + return "{0}_{1}".format(self._station_name, self._sensor_name) @property def should_poll(self): @@ -450,17 +502,19 @@ class AmbientWeatherEntity(Entity): @property def unique_id(self): """Return a unique, unchanging string that represents this sensor.""" - return '{0}_{1}'.format(self._mac_address, self._sensor_name) + return "{0}_{1}".format(self._mac_address, self._sensor_type) async def async_added_to_hass(self): """Register callbacks.""" + @callback def update(): """Update the state.""" self.async_schedule_update_ha_state(True) self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update) + self.hass, TOPIC_UPDATE, update + ) async def async_will_remove_from_hass(self): """Disconnect dispatcher listener when removed.""" diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 798605a1aa2..3f02eb9f1e8 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -5,16 +5,26 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import ATTR_NAME from . import ( - SENSOR_TYPES, TYPE_BATT1, TYPE_BATT2, TYPE_BATT3, TYPE_BATT4, TYPE_BATT5, - TYPE_BATT6, TYPE_BATT7, TYPE_BATT8, TYPE_BATT9, TYPE_BATT10, TYPE_BATTOUT, - AmbientWeatherEntity) + SENSOR_TYPES, + TYPE_BATT1, + TYPE_BATT2, + TYPE_BATT3, + TYPE_BATT4, + TYPE_BATT5, + TYPE_BATT6, + TYPE_BATT7, + TYPE_BATT8, + TYPE_BATT9, + TYPE_BATT10, + TYPE_BATTOUT, + AmbientWeatherEntity, +) from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_BINARY_SENSOR _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up Ambient PWS binary sensors based on the old way.""" pass @@ -30,8 +40,14 @@ async def async_setup_entry(hass, entry, async_add_entities): if kind == TYPE_BINARY_SENSOR: binary_sensor_list.append( AmbientWeatherBinarySensor( - ambient, mac_address, station[ATTR_NAME], condition, - name, device_class)) + ambient, + mac_address, + station[ATTR_NAME], + condition, + name, + device_class, + ) + ) async_add_entities(binary_sensor_list, True) @@ -42,15 +58,25 @@ class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorDevice): @property def is_on(self): """Return the status of the sensor.""" - if self._sensor_type in (TYPE_BATT1, TYPE_BATT10, TYPE_BATT2, - TYPE_BATT3, TYPE_BATT4, TYPE_BATT5, - TYPE_BATT6, TYPE_BATT7, TYPE_BATT8, - TYPE_BATT9, TYPE_BATTOUT): + if self._sensor_type in ( + TYPE_BATT1, + TYPE_BATT10, + TYPE_BATT2, + TYPE_BATT3, + TYPE_BATT4, + TYPE_BATT5, + TYPE_BATT6, + TYPE_BATT7, + TYPE_BATT8, + TYPE_BATT9, + TYPE_BATTOUT, + ): return self._state == 0 return self._state == 1 async def async_update(self): """Fetch new state data for the entity.""" - self._state = self._ambient.stations[ - self._mac_address][ATTR_LAST_DATA].get(self._sensor_type) + self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + self._sensor_type + ) diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py index f01bfd8f791..256e55ba402 100644 --- a/homeassistant/components/ambient_station/config_flow.py +++ b/homeassistant/components/ambient_station/config_flow.py @@ -13,28 +13,25 @@ from .const import CONF_APP_KEY, DOMAIN def configured_instances(hass): """Return a set of configured Ambient PWS instances.""" return set( - entry.data[CONF_APP_KEY] - for entry in hass.config_entries.async_entries(DOMAIN)) + entry.data[CONF_APP_KEY] for entry in hass.config_entries.async_entries(DOMAIN) + ) @config_entries.HANDLERS.register(DOMAIN) class AmbientStationFlowHandler(config_entries.ConfigFlow): """Handle an Ambient PWS config flow.""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH async def _show_form(self, errors=None): """Show the form to the user.""" - data_schema = vol.Schema({ - vol.Required(CONF_API_KEY): str, - vol.Required(CONF_APP_KEY): str, - }) + data_schema = vol.Schema( + {vol.Required(CONF_API_KEY): str, vol.Required(CONF_APP_KEY): str} + ) return self.async_show_form( - step_id='user', - data_schema=data_schema, - errors=errors if errors else {}, + step_id="user", data_schema=data_schema, errors=errors if errors else {} ) async def async_step_import(self, import_config): @@ -50,22 +47,22 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow): return await self._show_form() if user_input[CONF_APP_KEY] in configured_instances(self.hass): - return await self._show_form({CONF_APP_KEY: 'identifier_exists'}) + return await self._show_form({CONF_APP_KEY: "identifier_exists"}) session = aiohttp_client.async_get_clientsession(self.hass) - client = Client( - user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session) + client = Client(user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session) try: devices = await client.api.get_devices() except AmbientError: - return await self._show_form({'base': 'invalid_key'}) + return await self._show_form({"base": "invalid_key"}) if not devices: - return await self._show_form({'base': 'no_devices'}) + return await self._show_form({"base": "no_devices"}) # The Application Key (which identifies each config entry) is too long # to show nicely in the UI, so we take the first 12 characters (similar # to how GitHub does it): return self.async_create_entry( - title=user_input[CONF_APP_KEY][:12], data=user_input) + title=user_input[CONF_APP_KEY][:12], data=user_input + ) diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py index 27ec7afefaa..b2df34f2f28 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -1,13 +1,13 @@ """Define constants for the Ambient PWS component.""" -DOMAIN = 'ambient_station' +DOMAIN = "ambient_station" -ATTR_LAST_DATA = 'last_data' +ATTR_LAST_DATA = "last_data" -CONF_APP_KEY = 'app_key' +CONF_APP_KEY = "app_key" -DATA_CLIENT = 'data_client' +DATA_CLIENT = "data_client" -TOPIC_UPDATE = 'update' +TOPIC_UPDATE = "update" -TYPE_BINARY_SENSOR = 'binary_sensor' -TYPE_SENSOR = 'sensor' +TYPE_BINARY_SENSOR = "binary_sensor" +TYPE_SENSOR = "sensor" diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index dcab3d7e50e..56425221e0d 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -3,14 +3,18 @@ import logging from homeassistant.const import ATTR_NAME -from . import SENSOR_TYPES, TYPE_SOLARRADIATION, AmbientWeatherEntity +from . import ( + SENSOR_TYPES, + TYPE_SOLARRADIATION, + TYPE_SOLARRADIATION_LX, + AmbientWeatherEntity, +) from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_SENSOR _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up Ambient PWS sensors based on existing config.""" pass @@ -26,8 +30,15 @@ async def async_setup_entry(hass, entry, async_add_entities): if kind == TYPE_SENSOR: sensor_list.append( AmbientWeatherSensor( - ambient, mac_address, station[ATTR_NAME], condition, - name, device_class, unit)) + ambient, + mac_address, + station[ATTR_NAME], + condition, + name, + device_class, + unit, + ) + ) async_add_entities(sensor_list, True) @@ -36,16 +47,19 @@ class AmbientWeatherSensor(AmbientWeatherEntity): """Define an Ambient sensor.""" def __init__( - self, ambient, mac_address, station_name, sensor_type, sensor_name, - device_class, unit): + self, + ambient, + mac_address, + station_name, + sensor_type, + sensor_name, + device_class, + unit, + ): """Initialize the sensor.""" super().__init__( - ambient, - mac_address, - station_name, - sensor_type, - sensor_name, - device_class) + ambient, mac_address, station_name, sensor_type, sensor_name, device_class + ) self._unit = unit @@ -61,13 +75,15 @@ class AmbientWeatherSensor(AmbientWeatherEntity): async def async_update(self): """Fetch new state data for the sensor.""" - new_state = self._ambient.stations[ - self._mac_address][ATTR_LAST_DATA].get(self._sensor_type) - - if self._sensor_type == TYPE_SOLARRADIATION: - # Ambient's units for solar radiation (illuminance) are - # W/m^2; since those aren't commonly used in the HASS - # world, transform them to lx: - self._state = round(float(new_state)/0.0079) + if self._sensor_type == TYPE_SOLARRADIATION_LX: + # If the user requests the solarradiation_lx sensor, use the + # value of the solarradiation sensor and apply a very accurate + # approximation of converting sunlight W/m^2 to lx: + w_m2_brightness_val = self._ambient.stations[self._mac_address][ + ATTR_LAST_DATA + ].get(TYPE_SOLARRADIATION) + self._state = round(float(w_m2_brightness_val) / 0.0079) else: - self._state = new_state + self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + self._sensor_type + ) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 1c9303b2c52..f915872abf0 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -13,14 +13,24 @@ from homeassistant.components.camera import DOMAIN as CAMERA from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_AUTHENTICATION, CONF_BINARY_SENSORS, CONF_HOST, - CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, - CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION) + ATTR_ENTITY_ID, + CONF_AUTHENTICATION, + CONF_BINARY_SENSORS, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SENSORS, + CONF_SWITCHES, + CONF_USERNAME, + ENTITY_MATCH_ALL, + HTTP_BASIC_AUTHENTICATION, +) from homeassistant.exceptions import Unauthorized, UnknownUser from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, dispatcher_send) +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.service import async_extract_entity_ids @@ -33,31 +43,26 @@ from .switch import SWITCHES _LOGGER = logging.getLogger(__name__) -CONF_RESOLUTION = 'resolution' -CONF_STREAM_SOURCE = 'stream_source' -CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' -CONF_CONTROL_LIGHT = 'control_light' +CONF_RESOLUTION = "resolution" +CONF_STREAM_SOURCE = "stream_source" +CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" +CONF_CONTROL_LIGHT = "control_light" -DEFAULT_NAME = 'Amcrest Camera' +DEFAULT_NAME = "Amcrest Camera" DEFAULT_PORT = 80 -DEFAULT_RESOLUTION = 'high' -DEFAULT_ARGUMENTS = '-pred 1' +DEFAULT_RESOLUTION = "high" +DEFAULT_ARGUMENTS = "-pred 1" MAX_ERRORS = 5 RECHECK_INTERVAL = timedelta(minutes=1) -NOTIFICATION_ID = 'amcrest_notification' -NOTIFICATION_TITLE = 'Amcrest Camera Setup' +NOTIFICATION_ID = "amcrest_notification" +NOTIFICATION_TITLE = "Amcrest Camera Setup" -RESOLUTION_LIST = { - 'high': 0, - 'low': 1, -} +RESOLUTION_LIST = {"high": 0, "low": 1} SCAN_INTERVAL = timedelta(seconds=10) -AUTHENTICATION_LIST = { - 'basic': 'basic' -} +AUTHENTICATION_LIST = {"basic": "basic"} def _deprecated_sensor_values(sensors): @@ -66,8 +71,11 @@ def _deprecated_sensor_values(sensors): "The '%s' option value '%s' is deprecated, " "please remove it from your configuration and use " "the '%s' option with value '%s' instead", - CONF_SENSORS, SENSOR_MOTION_DETECTOR, CONF_BINARY_SENSORS, - BINARY_SENSOR_MOTION_DETECTED) + CONF_SENSORS, + SENSOR_MOTION_DETECTOR, + CONF_BINARY_SENSORS, + BINARY_SENSOR_MOTION_DETECTED, + ) return sensors @@ -77,7 +85,9 @@ def _deprecated_switches(config): "The '%s' option (with value %s) is deprecated, " "please remove it from your configuration and use " "services and attributes instead", - CONF_SWITCHES, config[CONF_SWITCHES]) + CONF_SWITCHES, + config[CONF_SWITCHES], + ) return config @@ -88,37 +98,41 @@ def _has_unique_names(devices): AMCREST_SCHEMA = vol.All( - vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): - vol.All(vol.In(AUTHENTICATION_LIST)), - vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): - vol.All(vol.In(RESOLUTION_LIST)), - vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]): - vol.All(vol.In(STREAM_SOURCE_LIST)), - vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): - cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, - vol.Optional(CONF_BINARY_SENSORS): - vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]), - vol.Optional(CONF_SENSORS): - vol.All(cv.ensure_list, [vol.In(SENSORS)], - _deprecated_sensor_values), - vol.Optional(CONF_SWITCHES): - vol.All(cv.ensure_list, [vol.In(SWITCHES)]), - vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean, - }), - _deprecated_switches + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional( + CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION + ): vol.All(vol.In(AUTHENTICATION_LIST)), + vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): vol.All( + vol.In(RESOLUTION_LIST) + ), + vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]): vol.All( + vol.In(STREAM_SOURCE_LIST) + ), + vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [vol.In(BINARY_SENSORS)] + ), + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensor_values + ), + vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [vol.In(SWITCHES)]), + vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean, + } + ), + _deprecated_switches, ) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)}, + extra=vol.ALLOW_EXTRA, +) # pylint: disable=too-many-ancestors @@ -132,8 +146,9 @@ class AmcrestChecker(Http): self._wrap_errors = 0 self._wrap_lock = threading.Lock() self._unsub_recheck = None - super().__init__(host, port, user, password, retries_connection=1, - timeout_protocol=3.05) + super().__init__( + host, port, user, password, retries_connection=1, timeout_protocol=3.05 + ) @property def available(self): @@ -148,17 +163,16 @@ class AmcrestChecker(Http): with self._wrap_lock: was_online = self.available self._wrap_errors += 1 - _LOGGER.debug('%s camera errs: %i', self._wrap_name, - self._wrap_errors) + _LOGGER.debug("%s camera errs: %i", self._wrap_name, self._wrap_errors) offline = not self.available if offline and was_online: - _LOGGER.error( - '%s camera offline: Too many errors', self._wrap_name) + _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) dispatcher_send( - self._hass, - service_signal(SERVICE_UPDATE, self._wrap_name)) + self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) + ) self._unsub_recheck = track_time_interval( - self._hass, self._wrap_test_online, RECHECK_INTERVAL) + self._hass, self._wrap_test_online, RECHECK_INTERVAL + ) raise with self._wrap_lock: was_offline = not self.available @@ -166,9 +180,8 @@ class AmcrestChecker(Http): if was_offline: self._unsub_recheck() self._unsub_recheck = None - _LOGGER.error('%s camera back online', self._wrap_name) - dispatcher_send( - self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)) + _LOGGER.error("%s camera back online", self._wrap_name) + dispatcher_send(self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)) return ret def _wrap_test_online(self, now): @@ -190,9 +203,8 @@ def setup(hass, config): try: api = AmcrestChecker( - hass, name, - device[CONF_HOST], device[CONF_PORT], - username, password) + hass, name, device[CONF_HOST], device[CONF_PORT], username, password + ) except LoginError as ex: _LOGGER.error("Login error for %s camera: %s", name, ex) @@ -214,41 +226,40 @@ def setup(hass, config): authentication = None hass.data[DATA_AMCREST][DEVICES][name] = AmcrestDevice( - api, authentication, ffmpeg_arguments, stream_source, - resolution, control_light) + api, + authentication, + ffmpeg_arguments, + stream_source, + resolution, + control_light, + ) - discovery.load_platform( - hass, CAMERA, DOMAIN, { - CONF_NAME: name, - }, config) + discovery.load_platform(hass, CAMERA, DOMAIN, {CONF_NAME: name}, config) if binary_sensors: discovery.load_platform( - hass, BINARY_SENSOR, DOMAIN, { - CONF_NAME: name, - CONF_BINARY_SENSORS: binary_sensors - }, config) + hass, + BINARY_SENSOR, + DOMAIN, + {CONF_NAME: name, CONF_BINARY_SENSORS: binary_sensors}, + config, + ) if sensors: discovery.load_platform( - hass, SENSOR, DOMAIN, { - CONF_NAME: name, - CONF_SENSORS: sensors, - }, config) + hass, SENSOR, DOMAIN, {CONF_NAME: name, CONF_SENSORS: sensors}, config + ) if switches: discovery.load_platform( - hass, SWITCH, DOMAIN, { - CONF_NAME: name, - CONF_SWITCHES: switches - }, config) + hass, SWITCH, DOMAIN, {CONF_NAME: name, CONF_SWITCHES: switches}, config + ) if not hass.data[DATA_AMCREST][DEVICES]: return False def have_permission(user, entity_id): - return not user or user.permissions.check_entity( - entity_id, POLICY_CONTROL) + return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL) async def async_extract_from_service(call): if call.context.user_id: @@ -261,7 +272,8 @@ def setup(hass, config): if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL: # Return all entity_ids user has permission to control. return [ - entity_id for entity_id in hass.data[DATA_AMCREST][CAMERAS] + entity_id + for entity_id in hass.data[DATA_AMCREST][CAMERAS] if have_permission(user, entity_id) ] @@ -272,9 +284,7 @@ def setup(hass, config): continue if not have_permission(user, entity_id): raise Unauthorized( - context=call.context, - entity_id=entity_id, - permission=POLICY_CONTROL + context=call.context, entity_id=entity_id, permission=POLICY_CONTROL ) entity_ids.append(entity_id) return entity_ids @@ -284,15 +294,10 @@ def setup(hass, config): for arg in CAMERA_SERVICES[call.service][2]: args.append(call.data[arg]) for entity_id in await async_extract_from_service(call): - async_dispatcher_send( - hass, - service_signal(call.service, entity_id), - *args - ) + async_dispatcher_send(hass, service_signal(call.service, entity_id), *args) for service, params in CAMERA_SERVICES.items(): - hass.services.async_register( - DOMAIN, service, async_service_handler, params[0]) + hass.services.async_register(DOMAIN, service, async_service_handler, params[0]) return True @@ -300,8 +305,15 @@ def setup(hass, config): class AmcrestDevice: """Representation of a base Amcrest discovery device.""" - def __init__(self, api, authentication, ffmpeg_arguments, - stream_source, resolution, control_light): + def __init__( + self, + api, + authentication, + ffmpeg_arguments, + stream_source, + resolution, + control_light, + ): """Initialize the entity.""" self.api = api self.authentication = authentication diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index 9489fc60d4d..f8b50d1114e 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -5,29 +5,35 @@ import logging from amcrest import AmcrestError from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOTION) + BinarySensorDevice, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_MOTION, +) from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( - BINARY_SENSOR_SCAN_INTERVAL_SECS, DATA_AMCREST, DEVICES, SERVICE_UPDATE) + BINARY_SENSOR_SCAN_INTERVAL_SECS, + DATA_AMCREST, + DEVICES, + SERVICE_UPDATE, +) from .helpers import log_update_error, service_signal _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS) -BINARY_SENSOR_MOTION_DETECTED = 'motion_detected' -BINARY_SENSOR_ONLINE = 'online' +BINARY_SENSOR_MOTION_DETECTED = "motion_detected" +BINARY_SENSOR_ONLINE = "online" # Binary sensor types are defined like: Name, device class BINARY_SENSORS = { - BINARY_SENSOR_MOTION_DETECTED: ('Motion Detected', DEVICE_CLASS_MOTION), - BINARY_SENSOR_ONLINE: ('Online', DEVICE_CLASS_CONNECTIVITY), + BINARY_SENSOR_MOTION_DETECTED: ("Motion Detected", DEVICE_CLASS_MOTION), + BINARY_SENSOR_ONLINE: ("Online", DEVICE_CLASS_CONNECTIVITY), } -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a binary sensor for an Amcrest IP Camera.""" if discovery_info is None: return @@ -35,9 +41,12 @@ async def async_setup_platform(hass, config, async_add_entities, name = discovery_info[CONF_NAME] device = hass.data[DATA_AMCREST][DEVICES][name] async_add_entities( - [AmcrestBinarySensor(name, device, sensor_type) - for sensor_type in discovery_info[CONF_BINARY_SENSORS]], - True) + [ + AmcrestBinarySensor(name, device, sensor_type) + for sensor_type in discovery_info[CONF_BINARY_SENSORS] + ], + True, + ) class AmcrestBinarySensor(BinarySensorDevice): @@ -45,7 +54,7 @@ class AmcrestBinarySensor(BinarySensorDevice): def __init__(self, name, device, sensor_type): """Initialize entity.""" - self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type][0]) + self._name = "{} {}".format(name, BINARY_SENSORS[sensor_type][0]) self._signal_name = name self._api = device.api self._sensor_type = sensor_type @@ -82,7 +91,7 @@ class AmcrestBinarySensor(BinarySensorDevice): """Update entity.""" if not self.available: return - _LOGGER.debug('Updating %s binary sensor', self._name) + _LOGGER.debug("Updating %s binary sensor", self._name) try: if self._sensor_type == BINARY_SENSOR_MOTION_DETECTED: @@ -91,8 +100,7 @@ class AmcrestBinarySensor(BinarySensorDevice): elif self._sensor_type == BINARY_SENSOR_ONLINE: self._state = self._api.available except AmcrestError as error: - log_update_error( - _LOGGER, 'update', self.name, 'binary sensor', error) + log_update_error(_LOGGER, "update", self.name, "binary sensor", error) async def async_on_demand_update(self): """Update state.""" @@ -101,8 +109,10 @@ class AmcrestBinarySensor(BinarySensorDevice): async def async_added_to_hass(self): """Subscribe to update signal.""" self._unsub_dispatcher = async_dispatcher_connect( - self.hass, service_signal(SERVICE_UPDATE, self._signal_name), - self.async_on_demand_update) + self.hass, + service_signal(SERVICE_UPDATE, self._signal_name), + self.async_on_demand_update, + ) async def async_will_remove_from_hass(self): """Disconnect from update signal.""" diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 685d92d5ae6..483bdb2c7cf 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -8,83 +8,85 @@ from amcrest import AmcrestError import voluptuous as vol from homeassistant.components.camera import ( - Camera, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, SUPPORT_STREAM) + Camera, + CAMERA_SERVICE_SCHEMA, + SUPPORT_ON_OFF, + SUPPORT_STREAM, +) from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.const import ( - CONF_NAME, STATE_ON, STATE_OFF) +from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF from homeassistant.helpers.aiohttp_client import ( - async_aiohttp_proxy_stream, async_aiohttp_proxy_web, - async_get_clientsession) + async_aiohttp_proxy_stream, + async_aiohttp_proxy_web, + async_get_clientsession, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( - CAMERA_WEB_SESSION_TIMEOUT, CAMERAS, DATA_AMCREST, DEVICES, SERVICE_UPDATE) + CAMERA_WEB_SESSION_TIMEOUT, + CAMERAS, + DATA_AMCREST, + DEVICES, + SERVICE_UPDATE, +) from .helpers import log_update_error, service_signal _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=15) -STREAM_SOURCE_LIST = [ - 'snapshot', - 'mjpeg', - 'rtsp', -] +STREAM_SOURCE_LIST = ["snapshot", "mjpeg", "rtsp"] -_SRV_EN_REC = 'enable_recording' -_SRV_DS_REC = 'disable_recording' -_SRV_EN_AUD = 'enable_audio' -_SRV_DS_AUD = 'disable_audio' -_SRV_EN_MOT_REC = 'enable_motion_recording' -_SRV_DS_MOT_REC = 'disable_motion_recording' -_SRV_GOTO = 'goto_preset' -_SRV_CBW = 'set_color_bw' -_SRV_TOUR_ON = 'start_tour' -_SRV_TOUR_OFF = 'stop_tour' +_SRV_EN_REC = "enable_recording" +_SRV_DS_REC = "disable_recording" +_SRV_EN_AUD = "enable_audio" +_SRV_DS_AUD = "disable_audio" +_SRV_EN_MOT_REC = "enable_motion_recording" +_SRV_DS_MOT_REC = "disable_motion_recording" +_SRV_GOTO = "goto_preset" +_SRV_CBW = "set_color_bw" +_SRV_TOUR_ON = "start_tour" +_SRV_TOUR_OFF = "stop_tour" -_ATTR_PRESET = 'preset' -_ATTR_COLOR_BW = 'color_bw' +_ATTR_PRESET = "preset" +_ATTR_COLOR_BW = "color_bw" -_CBW_COLOR = 'color' -_CBW_AUTO = 'auto' -_CBW_BW = 'bw' +_CBW_COLOR = "color" +_CBW_AUTO = "auto" +_CBW_BW = "bw" _CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW] -_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({ - vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1)), -}) -_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({ - vol.Required(_ATTR_COLOR_BW): vol.In(_CBW), -}) +_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( + {vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))} +) +_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( + {vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)} +) CAMERA_SERVICES = { - _SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, 'async_enable_recording', ()), - _SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, 'async_disable_recording', ()), - _SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, 'async_enable_audio', ()), - _SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, 'async_disable_audio', ()), - _SRV_EN_MOT_REC: ( - CAMERA_SERVICE_SCHEMA, 'async_enable_motion_recording', ()), - _SRV_DS_MOT_REC: ( - CAMERA_SERVICE_SCHEMA, 'async_disable_motion_recording', ()), - _SRV_GOTO: (_SRV_GOTO_SCHEMA, 'async_goto_preset', (_ATTR_PRESET,)), - _SRV_CBW: (_SRV_CBW_SCHEMA, 'async_set_color_bw', (_ATTR_COLOR_BW,)), - _SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, 'async_start_tour', ()), - _SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, 'async_stop_tour', ()), + _SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, "async_enable_recording", ()), + _SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, "async_disable_recording", ()), + _SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, "async_enable_audio", ()), + _SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, "async_disable_audio", ()), + _SRV_EN_MOT_REC: (CAMERA_SERVICE_SCHEMA, "async_enable_motion_recording", ()), + _SRV_DS_MOT_REC: (CAMERA_SERVICE_SCHEMA, "async_disable_motion_recording", ()), + _SRV_GOTO: (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)), + _SRV_CBW: (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)), + _SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, "async_start_tour", ()), + _SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, "async_stop_tour", ()), } _BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an Amcrest IP Camera.""" if discovery_info is None: return name = discovery_info[CONF_NAME] device = hass.data[DATA_AMCREST][DEVICES][name] - async_add_entities([ - AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True) + async_add_entities([AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True) class AmcrestCam(Camera): @@ -118,56 +120,59 @@ class AmcrestCam(Camera): available = self.available if not available or not self.is_on: _LOGGER.warning( - 'Attempt to take snaphot when %s camera is %s', self.name, - 'offline' if not available else 'off') + "Attempt to take snaphot when %s camera is %s", + self.name, + "offline" if not available else "off", + ) return None async with self._snapshot_lock: try: # Send the request to snap a picture and return raw jpg data - response = await self.hass.async_add_executor_job( - self._api.snapshot) + response = await self.hass.async_add_executor_job(self._api.snapshot) return response.data except (AmcrestError, HTTPError) as error: - log_update_error( - _LOGGER, 'get image from', self.name, 'camera', error) + log_update_error(_LOGGER, "get image from", self.name, "camera", error) return None async def handle_async_mjpeg_stream(self, request): """Return an MJPEG stream.""" # The snapshot implementation is handled by the parent class - if self._stream_source == 'snapshot': + if self._stream_source == "snapshot": return await super().handle_async_mjpeg_stream(request) if not self.available: _LOGGER.warning( - 'Attempt to stream %s when %s camera is offline', - self._stream_source, self.name) + "Attempt to stream %s when %s camera is offline", + self._stream_source, + self.name, + ) return None - if self._stream_source == 'mjpeg': + if self._stream_source == "mjpeg": # stream an MJPEG image stream directly from the camera websession = async_get_clientsession(self.hass) streaming_url = self._api.mjpeg_url(typeno=self._resolution) stream_coro = websession.get( - streaming_url, auth=self._token, - timeout=CAMERA_WEB_SESSION_TIMEOUT) + streaming_url, auth=self._token, timeout=CAMERA_WEB_SESSION_TIMEOUT + ) - return await async_aiohttp_proxy_web( - self.hass, request, stream_coro) + return await async_aiohttp_proxy_web(self.hass, request, stream_coro) # streaming via ffmpeg from haffmpeg.camera import CameraMjpeg streaming_url = self._rtsp_url stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - await stream.open_camera( - streaming_url, extra_cmd=self._ffmpeg_arguments) + await stream.open_camera(streaming_url, extra_cmd=self._ffmpeg_arguments) try: stream_reader = await stream.get_reader() return await async_aiohttp_proxy_stream( - self.hass, request, stream_reader, - self._ffmpeg.ffmpeg_stream_content_type) + self.hass, + request, + stream_reader, + self._ffmpeg.ffmpeg_stream_content_type, + ) finally: await stream.close() @@ -191,10 +196,11 @@ class AmcrestCam(Camera): """Return the Amcrest-specific camera state attributes.""" attr = {} if self._audio_enabled is not None: - attr['audio'] = _BOOL_TO_STATE.get(self._audio_enabled) + attr["audio"] = _BOOL_TO_STATE.get(self._audio_enabled) if self._motion_recording_enabled is not None: - attr['motion_recording'] = _BOOL_TO_STATE.get( - self._motion_recording_enabled) + attr["motion_recording"] = _BOOL_TO_STATE.get( + self._motion_recording_enabled + ) if self._color_bw is not None: attr[_ATTR_COLOR_BW] = self._color_bw return attr @@ -249,13 +255,20 @@ class AmcrestCam(Camera): async def async_added_to_hass(self): """Subscribe to signals and add camera to list.""" for service, params in CAMERA_SERVICES.items(): - self._unsub_dispatcher.append(async_dispatcher_connect( + self._unsub_dispatcher.append( + async_dispatcher_connect( + self.hass, + service_signal(service, self.entity_id), + getattr(self, params[1]), + ) + ) + self._unsub_dispatcher.append( + async_dispatcher_connect( self.hass, - service_signal(service, self.entity_id), - getattr(self, params[1]))) - self._unsub_dispatcher.append(async_dispatcher_connect( - self.hass, service_signal(SERVICE_UPDATE, self._name), - self.async_on_demand_update)) + service_signal(SERVICE_UPDATE, self._name), + self.async_on_demand_update, + ) + ) self.hass.data[DATA_AMCREST][CAMERAS].append(self.entity_id) async def async_will_remove_from_hass(self): @@ -270,32 +283,29 @@ class AmcrestCam(Camera): if not self.available: self._update_succeeded = False return - _LOGGER.debug('Updating %s camera', self.name) + _LOGGER.debug("Updating %s camera", self.name) try: if self._brand is None: resp = self._api.vendor_information.strip() - if resp.startswith('vendor='): - self._brand = resp.split('=')[-1] + if resp.startswith("vendor="): + self._brand = resp.split("=")[-1] else: - self._brand = 'unknown' + self._brand = "unknown" if self._model is None: resp = self._api.device_type.strip() - if resp.startswith('type='): - self._model = resp.split('=')[-1] + if resp.startswith("type="): + self._model = resp.split("=")[-1] else: - self._model = 'unknown' + self._model = "unknown" self.is_streaming = self._api.video_enabled - self._is_recording = self._api.record_mode == 'Manual' - self._motion_detection_enabled = ( - self._api.is_motion_detector_on()) + self._is_recording = self._api.record_mode == "Manual" + self._motion_detection_enabled = self._api.is_motion_detector_on() self._audio_enabled = self._api.audio_enabled - self._motion_recording_enabled = ( - self._api.is_record_on_motion_detection()) + self._motion_recording_enabled = self._api.is_record_on_motion_detection() self._color_bw = _CBW[self._api.day_night_color] self._rtsp_url = self._api.rtsp_url(typeno=self._resolution) except AmcrestError as error: - log_update_error( - _LOGGER, 'get', self.name, 'camera attributes', error) + log_update_error(_LOGGER, "get", self.name, "camera attributes", error) self._update_succeeded = False else: self._update_succeeded = True @@ -338,13 +348,11 @@ class AmcrestCam(Camera): async def async_enable_motion_recording(self): """Call the job and enable motion recording.""" - await self.hass.async_add_executor_job(self._enable_motion_recording, - True) + await self.hass.async_add_executor_job(self._enable_motion_recording, True) async def async_disable_motion_recording(self): """Call the job and disable motion recording.""" - await self.hass.async_add_executor_job(self._enable_motion_recording, - False) + await self.hass.async_add_executor_job(self._enable_motion_recording, False) async def async_goto_preset(self, preset): """Call the job and move camera to preset position.""" @@ -375,8 +383,12 @@ class AmcrestCam(Camera): self._api.video_enabled = enable except AmcrestError as error: log_update_error( - _LOGGER, 'enable' if enable else 'disable', self.name, - 'camera video stream', error) + _LOGGER, + "enable" if enable else "disable", + self.name, + "camera video stream", + error, + ) else: self.is_streaming = enable self.schedule_update_ha_state() @@ -390,14 +402,17 @@ class AmcrestCam(Camera): # video stream off if recording is being turned on. if not self.is_streaming and enable: self._enable_video_stream(True) - rec_mode = {'Automatic': 0, 'Manual': 1} + rec_mode = {"Automatic": 0, "Manual": 1} try: - self._api.record_mode = rec_mode[ - 'Manual' if enable else 'Automatic'] + self._api.record_mode = rec_mode["Manual" if enable else "Automatic"] except AmcrestError as error: log_update_error( - _LOGGER, 'enable' if enable else 'disable', self.name, - 'camera recording', error) + _LOGGER, + "enable" if enable else "disable", + self.name, + "camera recording", + error, + ) else: self._is_recording = enable self.schedule_update_ha_state() @@ -408,8 +423,12 @@ class AmcrestCam(Camera): self._api.motion_detection = str(enable).lower() except AmcrestError as error: log_update_error( - _LOGGER, 'enable' if enable else 'disable', self.name, - 'camera motion detection', error) + _LOGGER, + "enable" if enable else "disable", + self.name, + "camera motion detection", + error, + ) else: self._motion_detection_enabled = enable self.schedule_update_ha_state() @@ -420,8 +439,12 @@ class AmcrestCam(Camera): self._api.audio_enabled = enable except AmcrestError as error: log_update_error( - _LOGGER, 'enable' if enable else 'disable', self.name, - 'camera audio stream', error) + _LOGGER, + "enable" if enable else "disable", + self.name, + "camera audio stream", + error, + ) else: self._audio_enabled = enable self.schedule_update_ha_state() @@ -432,12 +455,18 @@ class AmcrestCam(Camera): """Enable or disable indicator light.""" try: self._api.command( - 'configManager.cgi?action=setConfig&LightGlobal[0].Enable={}' - .format(str(enable).lower())) + "configManager.cgi?action=setConfig&LightGlobal[0].Enable={}".format( + str(enable).lower() + ) + ) except AmcrestError as error: log_update_error( - _LOGGER, 'enable' if enable else 'disable', self.name, - 'indicator light', error) + _LOGGER, + "enable" if enable else "disable", + self.name, + "indicator light", + error, + ) def _enable_motion_recording(self, enable): """Enable or disable motion recording.""" @@ -445,8 +474,12 @@ class AmcrestCam(Camera): self._api.motion_recording = str(enable).lower() except AmcrestError as error: log_update_error( - _LOGGER, 'enable' if enable else 'disable', self.name, - 'camera motion recording', error) + _LOGGER, + "enable" if enable else "disable", + self.name, + "camera motion recording", + error, + ) else: self._motion_recording_enabled = enable self.schedule_update_ha_state() @@ -454,12 +487,11 @@ class AmcrestCam(Camera): def _goto_preset(self, preset): """Move camera position and zoom to preset.""" try: - self._api.go_to_preset( - action='start', preset_point_number=preset) + self._api.go_to_preset(action="start", preset_point_number=preset) except AmcrestError as error: log_update_error( - _LOGGER, 'move', self.name, - 'camera to preset {}'.format(preset), error) + _LOGGER, "move", self.name, "camera to preset {}".format(preset), error + ) def _set_color_bw(self, cbw): """Set camera color mode.""" @@ -467,8 +499,8 @@ class AmcrestCam(Camera): self._api.day_night_color = _CBW.index(cbw) except AmcrestError as error: log_update_error( - _LOGGER, 'set', self.name, - 'camera color mode to {}'.format(cbw), error) + _LOGGER, "set", self.name, "camera color mode to {}".format(cbw), error + ) else: self._color_bw = cbw self.schedule_update_ha_state() @@ -479,5 +511,5 @@ class AmcrestCam(Camera): self._api.tour(start=start) except AmcrestError as error: log_update_error( - _LOGGER, 'start' if start else 'stop', self.name, - 'camera tour', error) + _LOGGER, "start" if start else "stop", self.name, "camera tour", error + ) diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py index fe07659b48a..98d613634b5 100644 --- a/homeassistant/components/amcrest/const.py +++ b/homeassistant/components/amcrest/const.py @@ -1,11 +1,11 @@ """Constants for amcrest component.""" -DOMAIN = 'amcrest' +DOMAIN = "amcrest" DATA_AMCREST = DOMAIN -CAMERAS = 'cameras' -DEVICES = 'devices' +CAMERAS = "cameras" +DEVICES = "devices" BINARY_SENSOR_SCAN_INTERVAL_SECS = 5 CAMERA_WEB_SESSION_TIMEOUT = 10 SENSOR_SCAN_INTERVAL_SECS = 10 -SERVICE_UPDATE = 'update' +SERVICE_UPDATE = "update" diff --git a/homeassistant/components/amcrest/helpers.py b/homeassistant/components/amcrest/helpers.py index 69d7f5ef288..d24d6e0e707 100644 --- a/homeassistant/components/amcrest/helpers.py +++ b/homeassistant/components/amcrest/helpers.py @@ -4,14 +4,18 @@ from .const import DOMAIN def service_signal(service, ident=None): """Encode service and identifier into signal.""" - signal = '{}_{}'.format(DOMAIN, service) + signal = "{}_{}".format(DOMAIN, service) if ident: - signal += '_{}'.format(ident.replace('.', '_')) + signal += "_{}".format(ident.replace(".", "_")) return signal def log_update_error(logger, action, name, entity_type, error): """Log an update error.""" logger.error( - 'Could not %s %s %s due to error: %s', - action, name, entity_type, error.__class__.__name__) + "Could not %s %s %s due to error: %s", + action, + name, + entity_type, + error.__class__.__name__, + ) diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index 1788b9c62b0..b53f05273fa 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -8,27 +8,25 @@ from homeassistant.const import CONF_NAME, CONF_SENSORS from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import ( - DATA_AMCREST, DEVICES, SENSOR_SCAN_INTERVAL_SECS, SERVICE_UPDATE) +from .const import DATA_AMCREST, DEVICES, SENSOR_SCAN_INTERVAL_SECS, SERVICE_UPDATE from .helpers import log_update_error, service_signal _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS) -SENSOR_MOTION_DETECTOR = 'motion_detector' -SENSOR_PTZ_PRESET = 'ptz_preset' -SENSOR_SDCARD = 'sdcard' +SENSOR_MOTION_DETECTOR = "motion_detector" +SENSOR_PTZ_PRESET = "ptz_preset" +SENSOR_SDCARD = "sdcard" # Sensor types are defined like: Name, units, icon SENSORS = { - SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'], - SENSOR_PTZ_PRESET: ['PTZ Preset', None, 'mdi:camera-iris'], - SENSOR_SDCARD: ['SD Used', '%', 'mdi:sd'], + SENSOR_MOTION_DETECTOR: ["Motion Detected", None, "mdi:run"], + SENSOR_PTZ_PRESET: ["PTZ Preset", None, "mdi:camera-iris"], + SENSOR_SDCARD: ["SD Used", "%", "mdi:sd"], } -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a sensor for an Amcrest IP Camera.""" if discovery_info is None: return @@ -36,9 +34,12 @@ async def async_setup_platform( name = discovery_info[CONF_NAME] device = hass.data[DATA_AMCREST][DEVICES][name] async_add_entities( - [AmcrestSensor(name, device, sensor_type) - for sensor_type in discovery_info[CONF_SENSORS]], - True) + [ + AmcrestSensor(name, device, sensor_type) + for sensor_type in discovery_info[CONF_SENSORS] + ], + True, + ) class AmcrestSensor(Entity): @@ -46,7 +47,7 @@ class AmcrestSensor(Entity): def __init__(self, name, device, sensor_type): """Initialize a sensor for Amcrest camera.""" - self._name = '{} {}'.format(name, SENSORS[sensor_type][0]) + self._name = "{} {}".format(name, SENSORS[sensor_type][0]) self._signal_name = name self._api = device.api self._sensor_type = sensor_type @@ -95,7 +96,7 @@ class AmcrestSensor(Entity): try: if self._sensor_type == SENSOR_MOTION_DETECTOR: self._state = self._api.is_motion_detected - self._attrs['Record Mode'] = self._api.record_mode + self._attrs["Record Mode"] = self._api.record_mode elif self._sensor_type == SENSOR_PTZ_PRESET: self._state = self._api.ptz_presets_count @@ -103,20 +104,19 @@ class AmcrestSensor(Entity): elif self._sensor_type == SENSOR_SDCARD: storage = self._api.storage_all try: - self._attrs['Total'] = '{:.2f} {}'.format( - *storage['total']) + self._attrs["Total"] = "{:.2f} {}".format(*storage["total"]) except ValueError: - self._attrs['Total'] = '{} {}'.format(*storage['total']) + self._attrs["Total"] = "{} {}".format(*storage["total"]) try: - self._attrs['Used'] = '{:.2f} {}'.format(*storage['used']) + self._attrs["Used"] = "{:.2f} {}".format(*storage["used"]) except ValueError: - self._attrs['Used'] = '{} {}'.format(*storage['used']) + self._attrs["Used"] = "{} {}".format(*storage["used"]) try: - self._state = '{:.2f}'.format(storage['used_percent']) + self._state = "{:.2f}".format(storage["used_percent"]) except ValueError: - self._state = storage['used_percent'] + self._state = storage["used_percent"] except AmcrestError as error: - log_update_error(_LOGGER, 'update', self.name, 'sensor', error) + log_update_error(_LOGGER, "update", self.name, "sensor", error) async def async_on_demand_update(self): """Update state.""" @@ -125,8 +125,10 @@ class AmcrestSensor(Entity): async def async_added_to_hass(self): """Subscribe to update signal.""" self._unsub_dispatcher = async_dispatcher_connect( - self.hass, service_signal(SERVICE_UPDATE, self._signal_name), - self.async_on_demand_update) + self.hass, + service_signal(SERVICE_UPDATE, self._signal_name), + self.async_on_demand_update, + ) async def async_will_remove_from_hass(self): """Disconnect from update signal.""" diff --git a/homeassistant/components/amcrest/switch.py b/homeassistant/components/amcrest/switch.py index ec286b4f404..0c3390c16f9 100644 --- a/homeassistant/components/amcrest/switch.py +++ b/homeassistant/components/amcrest/switch.py @@ -12,17 +12,16 @@ from .helpers import log_update_error, service_signal _LOGGER = logging.getLogger(__name__) -MOTION_DETECTION = 'motion_detection' -MOTION_RECORDING = 'motion_recording' +MOTION_DETECTION = "motion_detection" +MOTION_RECORDING = "motion_recording" # Switch types are defined like: Name, icon SWITCHES = { - MOTION_DETECTION: ['Motion Detection', 'mdi:run-fast'], - MOTION_RECORDING: ['Motion Recording', 'mdi:record-rec'] + MOTION_DETECTION: ["Motion Detection", "mdi:run-fast"], + MOTION_RECORDING: ["Motion Recording", "mdi:record-rec"], } -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the IP Amcrest camera switch platform.""" if discovery_info is None: return @@ -30,9 +29,12 @@ async def async_setup_platform( name = discovery_info[CONF_NAME] device = hass.data[DATA_AMCREST][DEVICES][name] async_add_entities( - [AmcrestSwitch(name, device, setting) - for setting in discovery_info[CONF_SWITCHES]], - True) + [ + AmcrestSwitch(name, device, setting) + for setting in discovery_info[CONF_SWITCHES] + ], + True, + ) class AmcrestSwitch(ToggleEntity): @@ -40,7 +42,7 @@ class AmcrestSwitch(ToggleEntity): def __init__(self, name, device, setting): """Initialize the Amcrest switch.""" - self._name = '{} {}'.format(name, SWITCHES[setting][0]) + self._name = "{} {}".format(name, SWITCHES[setting][0]) self._signal_name = name self._api = device.api self._setting = setting @@ -64,11 +66,11 @@ class AmcrestSwitch(ToggleEntity): return try: if self._setting == MOTION_DETECTION: - self._api.motion_detection = 'true' + self._api.motion_detection = "true" elif self._setting == MOTION_RECORDING: - self._api.motion_recording = 'true' + self._api.motion_recording = "true" except AmcrestError as error: - log_update_error(_LOGGER, 'turn on', self.name, 'switch', error) + log_update_error(_LOGGER, "turn on", self.name, "switch", error) def turn_off(self, **kwargs): """Turn setting off.""" @@ -76,11 +78,11 @@ class AmcrestSwitch(ToggleEntity): return try: if self._setting == MOTION_DETECTION: - self._api.motion_detection = 'false' + self._api.motion_detection = "false" elif self._setting == MOTION_RECORDING: - self._api.motion_recording = 'false' + self._api.motion_recording = "false" except AmcrestError as error: - log_update_error(_LOGGER, 'turn off', self.name, 'switch', error) + log_update_error(_LOGGER, "turn off", self.name, "switch", error) @property def available(self): @@ -100,7 +102,7 @@ class AmcrestSwitch(ToggleEntity): detection = self._api.is_record_on_motion_detection() self._state = detection except AmcrestError as error: - log_update_error(_LOGGER, 'update', self.name, 'switch', error) + log_update_error(_LOGGER, "update", self.name, "switch", error) @property def icon(self): @@ -114,8 +116,10 @@ class AmcrestSwitch(ToggleEntity): async def async_added_to_hass(self): """Subscribe to update signal.""" self._unsub_dispatcher = async_dispatcher_connect( - self.hass, service_signal(SERVICE_UPDATE, self._signal_name), - self.async_on_demand_update) + self.hass, + service_signal(SERVICE_UPDATE, self._signal_name), + self.async_on_demand_update, + ) async def async_will_remove_from_hass(self): """Disconnect from update signal.""" diff --git a/homeassistant/components/ampio/air_quality.py b/homeassistant/components/ampio/air_quality.py index 339f490bae5..f55f20fc150 100644 --- a/homeassistant/components/ampio/air_quality.py +++ b/homeassistant/components/ampio/air_quality.py @@ -4,8 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components.air_quality import ( - PLATFORM_SCHEMA, AirQualityEntity) +from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -13,18 +12,16 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = 'Data provided by Ampio' -CONF_STATION_ID = 'station_id' +ATTRIBUTION = "Data provided by Ampio" +CONF_STATION_ID = "station_id" SCAN_INTERVAL = timedelta(minutes=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_STATION_ID): cv.string, - vol.Optional(CONF_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME): cv.string} +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Ampio Smog air quality platform.""" from asmog import AmpioSmog diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index c9357c4cce0..33362bd37cc 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -7,140 +7,182 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, - CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL, - CONF_PLATFORM) + CONF_NAME, + CONF_HOST, + CONF_PORT, + CONF_USERNAME, + CONF_PASSWORD, + CONF_SENSORS, + CONF_SWITCHES, + CONF_TIMEOUT, + CONF_SCAN_INTERVAL, + CONF_PLATFORM, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect) + async_dispatcher_send, + async_dispatcher_connect, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -from homeassistant.components.mjpeg.camera import ( - CONF_MJPEG_URL, CONF_STILL_IMAGE_URL) +from homeassistant.components.mjpeg.camera import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL _LOGGER = logging.getLogger(__name__) -ATTR_AUD_CONNS = 'Audio Connections' -ATTR_HOST = 'host' -ATTR_VID_CONNS = 'Video Connections' +ATTR_AUD_CONNS = "Audio Connections" +ATTR_HOST = "host" +ATTR_VID_CONNS = "Video Connections" -CONF_MOTION_SENSOR = 'motion_sensor' +CONF_MOTION_SENSOR = "motion_sensor" -DATA_IP_WEBCAM = 'android_ip_webcam' -DEFAULT_NAME = 'IP Webcam' +DATA_IP_WEBCAM = "android_ip_webcam" +DEFAULT_NAME = "IP Webcam" DEFAULT_PORT = 8080 DEFAULT_TIMEOUT = 10 -DOMAIN = 'android_ip_webcam' +DOMAIN = "android_ip_webcam" SCAN_INTERVAL = timedelta(seconds=10) -SIGNAL_UPDATE_DATA = 'android_ip_webcam_update' +SIGNAL_UPDATE_DATA = "android_ip_webcam_update" KEY_MAP = { - 'audio_connections': 'Audio Connections', - 'adet_limit': 'Audio Trigger Limit', - 'antibanding': 'Anti-banding', - 'audio_only': 'Audio Only', - 'battery_level': 'Battery Level', - 'battery_temp': 'Battery Temperature', - 'battery_voltage': 'Battery Voltage', - 'coloreffect': 'Color Effect', - 'exposure': 'Exposure Level', - 'exposure_lock': 'Exposure Lock', - 'ffc': 'Front-facing Camera', - 'flashmode': 'Flash Mode', - 'focus': 'Focus', - 'focus_homing': 'Focus Homing', - 'focus_region': 'Focus Region', - 'focusmode': 'Focus Mode', - 'gps_active': 'GPS Active', - 'idle': 'Idle', - 'ip_address': 'IPv4 Address', - 'ipv6_address': 'IPv6 Address', - 'ivideon_streaming': 'Ivideon Streaming', - 'light': 'Light Level', - 'mirror_flip': 'Mirror Flip', - 'motion': 'Motion', - 'motion_active': 'Motion Active', - 'motion_detect': 'Motion Detection', - 'motion_event': 'Motion Event', - 'motion_limit': 'Motion Limit', - 'night_vision': 'Night Vision', - 'night_vision_average': 'Night Vision Average', - 'night_vision_gain': 'Night Vision Gain', - 'orientation': 'Orientation', - 'overlay': 'Overlay', - 'photo_size': 'Photo Size', - 'pressure': 'Pressure', - 'proximity': 'Proximity', - 'quality': 'Quality', - 'scenemode': 'Scene Mode', - 'sound': 'Sound', - 'sound_event': 'Sound Event', - 'sound_timeout': 'Sound Timeout', - 'torch': 'Torch', - 'video_connections': 'Video Connections', - 'video_chunk_len': 'Video Chunk Length', - 'video_recording': 'Video Recording', - 'video_size': 'Video Size', - 'whitebalance': 'White Balance', - 'whitebalance_lock': 'White Balance Lock', - 'zoom': 'Zoom' + "audio_connections": "Audio Connections", + "adet_limit": "Audio Trigger Limit", + "antibanding": "Anti-banding", + "audio_only": "Audio Only", + "battery_level": "Battery Level", + "battery_temp": "Battery Temperature", + "battery_voltage": "Battery Voltage", + "coloreffect": "Color Effect", + "exposure": "Exposure Level", + "exposure_lock": "Exposure Lock", + "ffc": "Front-facing Camera", + "flashmode": "Flash Mode", + "focus": "Focus", + "focus_homing": "Focus Homing", + "focus_region": "Focus Region", + "focusmode": "Focus Mode", + "gps_active": "GPS Active", + "idle": "Idle", + "ip_address": "IPv4 Address", + "ipv6_address": "IPv6 Address", + "ivideon_streaming": "Ivideon Streaming", + "light": "Light Level", + "mirror_flip": "Mirror Flip", + "motion": "Motion", + "motion_active": "Motion Active", + "motion_detect": "Motion Detection", + "motion_event": "Motion Event", + "motion_limit": "Motion Limit", + "night_vision": "Night Vision", + "night_vision_average": "Night Vision Average", + "night_vision_gain": "Night Vision Gain", + "orientation": "Orientation", + "overlay": "Overlay", + "photo_size": "Photo Size", + "pressure": "Pressure", + "proximity": "Proximity", + "quality": "Quality", + "scenemode": "Scene Mode", + "sound": "Sound", + "sound_event": "Sound Event", + "sound_timeout": "Sound Timeout", + "torch": "Torch", + "video_connections": "Video Connections", + "video_chunk_len": "Video Chunk Length", + "video_recording": "Video Recording", + "video_size": "Video Size", + "whitebalance": "White Balance", + "whitebalance_lock": "White Balance Lock", + "zoom": "Zoom", } ICON_MAP = { - 'audio_connections': 'mdi:speaker', - 'battery_level': 'mdi:battery', - 'battery_temp': 'mdi:thermometer', - 'battery_voltage': 'mdi:battery-charging-100', - 'exposure_lock': 'mdi:camera', - 'ffc': 'mdi:camera-front-variant', - 'focus': 'mdi:image-filter-center-focus', - 'gps_active': 'mdi:crosshairs-gps', - 'light': 'mdi:flashlight', - 'motion': 'mdi:run', - 'night_vision': 'mdi:weather-night', - 'overlay': 'mdi:monitor', - 'pressure': 'mdi:gauge', - 'proximity': 'mdi:map-marker-radius', - 'quality': 'mdi:quality-high', - 'sound': 'mdi:speaker', - 'sound_event': 'mdi:speaker', - 'sound_timeout': 'mdi:speaker', - 'torch': 'mdi:white-balance-sunny', - 'video_chunk_len': 'mdi:video', - 'video_connections': 'mdi:eye', - 'video_recording': 'mdi:record-rec', - 'whitebalance_lock': 'mdi:white-balance-auto' + "audio_connections": "mdi:speaker", + "battery_level": "mdi:battery", + "battery_temp": "mdi:thermometer", + "battery_voltage": "mdi:battery-charging-100", + "exposure_lock": "mdi:camera", + "ffc": "mdi:camera-front-variant", + "focus": "mdi:image-filter-center-focus", + "gps_active": "mdi:crosshairs-gps", + "light": "mdi:flashlight", + "motion": "mdi:run", + "night_vision": "mdi:weather-night", + "overlay": "mdi:monitor", + "pressure": "mdi:gauge", + "proximity": "mdi:map-marker-radius", + "quality": "mdi:quality-high", + "sound": "mdi:speaker", + "sound_event": "mdi:speaker", + "sound_timeout": "mdi:speaker", + "torch": "mdi:white-balance-sunny", + "video_chunk_len": "mdi:video", + "video_connections": "mdi:eye", + "video_recording": "mdi:record-rec", + "whitebalance_lock": "mdi:white-balance-auto", } -SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', - 'motion_detect', 'night_vision', 'overlay', - 'torch', 'whitebalance_lock', 'video_recording'] +SWITCHES = [ + "exposure_lock", + "ffc", + "focus", + "gps_active", + "motion_detect", + "night_vision", + "overlay", + "torch", + "whitebalance_lock", + "video_recording", +] -SENSORS = ['audio_connections', 'battery_level', 'battery_temp', - 'battery_voltage', 'light', 'motion', 'pressure', 'proximity', - 'sound', 'video_connections'] +SENSORS = [ + "audio_connections", + "battery_level", + "battery_temp", + "battery_voltage", + "light", + "motion", + "pressure", + "proximity", + "sound", + "video_connections", +] -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, - vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, - vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, - vol.Optional(CONF_SWITCHES): - vol.All(cv.ensure_list, [vol.In(SWITCHES)]), - vol.Optional(CONF_SENSORS): - vol.All(cv.ensure_list, [vol.In(SENSORS)]), - vol.Optional(CONF_MOTION_SENSOR): cv.boolean, - })]) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional( + CONF_TIMEOUT, default=DEFAULT_TIMEOUT + ): cv.positive_int, + vol.Optional( + CONF_SCAN_INTERVAL, default=SCAN_INTERVAL + ): cv.time_period, + vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, + vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [vol.In(SWITCHES)] + ), + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSORS)] + ), + vol.Optional(CONF_MOTION_SENSOR): cv.boolean, + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -163,30 +205,33 @@ async def async_setup(hass, config): # Init ip webcam cam = PyDroidIPCam( - hass.loop, websession, host, cam_config[CONF_PORT], - username=username, password=password, - timeout=cam_config[CONF_TIMEOUT] + hass.loop, + websession, + host, + cam_config[CONF_PORT], + username=username, + password=password, + timeout=cam_config[CONF_TIMEOUT], ) if switches is None: - switches = [setting for setting in cam.enabled_settings - if setting in SWITCHES] + switches = [ + setting for setting in cam.enabled_settings if setting in SWITCHES + ] if sensors is None: - sensors = [sensor for sensor in cam.enabled_sensors - if sensor in SENSORS] - sensors.extend(['audio_connections', 'video_connections']) + sensors = [sensor for sensor in cam.enabled_sensors if sensor in SENSORS] + sensors.extend(["audio_connections", "video_connections"]) if motion is None: - motion = 'motion_active' in cam.enabled_sensors + motion = "motion_active" in cam.enabled_sensors async def async_update_data(now): """Update data from IP camera in SCAN_INTERVAL.""" await cam.update() async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) - async_track_point_in_utc_time( - hass, async_update_data, utcnow() + interval) + async_track_point_in_utc_time(hass, async_update_data, utcnow() + interval) await async_update_data(None) @@ -194,42 +239,50 @@ async def async_setup(hass, config): webcams[host] = cam mjpeg_camera = { - CONF_PLATFORM: 'mjpeg', + CONF_PLATFORM: "mjpeg", CONF_MJPEG_URL: cam.mjpeg_url, CONF_STILL_IMAGE_URL: cam.image_url, CONF_NAME: name, } if username and password: - mjpeg_camera.update({ - CONF_USERNAME: username, - CONF_PASSWORD: password - }) + mjpeg_camera.update({CONF_USERNAME: username, CONF_PASSWORD: password}) - hass.async_create_task(discovery.async_load_platform( - hass, 'camera', 'mjpeg', mjpeg_camera, config)) + hass.async_create_task( + discovery.async_load_platform(hass, "camera", "mjpeg", mjpeg_camera, config) + ) if sensors: - hass.async_create_task(discovery.async_load_platform( - hass, 'sensor', DOMAIN, { - CONF_NAME: name, - CONF_HOST: host, - CONF_SENSORS: sensors, - }, config)) + hass.async_create_task( + discovery.async_load_platform( + hass, + "sensor", + DOMAIN, + {CONF_NAME: name, CONF_HOST: host, CONF_SENSORS: sensors}, + config, + ) + ) if switches: - hass.async_create_task(discovery.async_load_platform( - hass, 'switch', DOMAIN, { - CONF_NAME: name, - CONF_HOST: host, - CONF_SWITCHES: switches, - }, config)) + hass.async_create_task( + discovery.async_load_platform( + hass, + "switch", + DOMAIN, + {CONF_NAME: name, CONF_HOST: host, CONF_SWITCHES: switches}, + config, + ) + ) if motion: - hass.async_create_task(discovery.async_load_platform( - hass, 'binary_sensor', DOMAIN, { - CONF_HOST: host, - CONF_NAME: name, - }, config)) + hass.async_create_task( + discovery.async_load_platform( + hass, + "binary_sensor", + DOMAIN, + {CONF_HOST: host, CONF_NAME: name}, + config, + ) + ) tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] if tasks: @@ -248,6 +301,7 @@ class AndroidIPCamEntity(Entity): async def async_added_to_hass(self): """Register update dispatcher.""" + @callback def async_ipcam_update(host): """Update callback.""" @@ -255,8 +309,7 @@ class AndroidIPCamEntity(Entity): return self.async_schedule_update_ha_state(True) - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) + async_dispatcher_connect(self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) @property def should_poll(self): @@ -275,9 +328,7 @@ class AndroidIPCamEntity(Entity): if self._ipcam.status_data is None: return state_attr - state_attr[ATTR_VID_CONNS] = \ - self._ipcam.status_data.get('video_connections') - state_attr[ATTR_AUD_CONNS] = \ - self._ipcam.status_data.get('audio_connections') + state_attr[ATTR_VID_CONNS] = self._ipcam.status_data.get("video_connections") + state_attr[ATTR_AUD_CONNS] = self._ipcam.status_data.get("audio_connections") return state_attr diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index dbe50d81862..d7bf009701d 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -4,8 +4,7 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from . import CONF_HOST, CONF_NAME, DATA_IP_WEBCAM, KEY_MAP, AndroidIPCamEntity -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the IP Webcam binary sensors.""" if discovery_info is None: return @@ -14,8 +13,7 @@ async def async_setup_platform( name = discovery_info[CONF_NAME] ipcam = hass.data[DATA_IP_WEBCAM][host] - async_add_entities( - [IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True) + async_add_entities([IPWebcamBinarySensor(name, host, ipcam, "motion_active")], True) class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): @@ -27,7 +25,7 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): self._sensor = sensor self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._name = '{} {}'.format(name, self._mapped_name) + self._name = "{} {}".format(name, self._mapped_name) self._state = None self._unit = None @@ -49,4 +47,4 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return 'motion' + return "motion" diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index 9748b6ba548..20f4acebca6 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -2,12 +2,17 @@ from homeassistant.helpers.icon import icon_for_battery_level from . import ( - CONF_HOST, CONF_NAME, CONF_SENSORS, DATA_IP_WEBCAM, ICON_MAP, KEY_MAP, - AndroidIPCamEntity) + CONF_HOST, + CONF_NAME, + CONF_SENSORS, + DATA_IP_WEBCAM, + ICON_MAP, + KEY_MAP, + AndroidIPCamEntity, +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the IP Webcam Sensor.""" if discovery_info is None: return @@ -34,7 +39,7 @@ class IPWebcamSensor(AndroidIPCamEntity): self._sensor = sensor self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._name = '{} {}'.format(name, self._mapped_name) + self._name = "{} {}".format(name, self._mapped_name) self._state = None self._unit = None @@ -55,17 +60,17 @@ class IPWebcamSensor(AndroidIPCamEntity): async def async_update(self): """Retrieve latest state.""" - if self._sensor in ('audio_connections', 'video_connections'): + if self._sensor in ("audio_connections", "video_connections"): if not self._ipcam.status_data: return self._state = self._ipcam.status_data.get(self._sensor) - self._unit = 'Connections' + self._unit = "Connections" else: self._state, self._unit = self._ipcam.export_sensor(self._sensor) @property def icon(self): """Return the icon for the sensor.""" - if self._sensor == 'battery_level' and self._state is not None: + if self._sensor == "battery_level" and self._state is not None: return icon_for_battery_level(int(self._state)) - return ICON_MAP.get(self._sensor, 'mdi:eye') + return ICON_MAP.get(self._sensor, "mdi:eye") diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index e894913f5a4..5b2f5dad5e1 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -2,12 +2,17 @@ from homeassistant.components.switch import SwitchDevice from . import ( - CONF_HOST, CONF_NAME, CONF_SWITCHES, DATA_IP_WEBCAM, ICON_MAP, KEY_MAP, - AndroidIPCamEntity) + CONF_HOST, + CONF_NAME, + CONF_SWITCHES, + DATA_IP_WEBCAM, + ICON_MAP, + KEY_MAP, + AndroidIPCamEntity, +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the IP Webcam switch platform.""" if discovery_info is None: return @@ -34,7 +39,7 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): self._setting = setting self._mapped_name = KEY_MAP.get(self._setting, self._setting) - self._name = '{} {}'.format(name, self._mapped_name) + self._name = "{} {}".format(name, self._mapped_name) self._state = False @property @@ -53,11 +58,11 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn device on.""" - if self._setting == 'torch': + if self._setting == "torch": await self._ipcam.torch(activate=True) - elif self._setting == 'focus': + elif self._setting == "focus": await self._ipcam.focus(activate=True) - elif self._setting == 'video_recording': + elif self._setting == "video_recording": await self._ipcam.record(record=True) else: await self._ipcam.change_setting(self._setting, True) @@ -66,11 +71,11 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): async def async_turn_off(self, **kwargs): """Turn device off.""" - if self._setting == 'torch': + if self._setting == "torch": await self._ipcam.torch(activate=False) - elif self._setting == 'focus': + elif self._setting == "focus": await self._ipcam.focus(activate=False) - elif self._setting == 'video_recording': + elif self._setting == "video_recording": await self._ipcam.record(record=False) else: await self._ipcam.change_setting(self._setting, False) @@ -80,4 +85,4 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): @property def icon(self): """Return the icon for the switch.""" - return ICON_MAP.get(self._setting, 'mdi:flash') + return ICON_MAP.get(self._setting, "mdi:flash") diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index efdd32ecbc5..381f0bb7cf1 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -3,81 +3,113 @@ import functools import logging import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP) + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_STEP, +) from homeassistant.const import ( - ATTR_COMMAND, ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, - CONF_PORT, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, - STATE_STANDBY) + ATTR_COMMAND, + ATTR_ENTITY_ID, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, + CONF_PORT, + STATE_IDLE, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, + STATE_STANDBY, +) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -ANDROIDTV_DOMAIN = 'androidtv' +ANDROIDTV_DOMAIN = "androidtv" _LOGGER = logging.getLogger(__name__) -SUPPORT_ANDROIDTV = SUPPORT_PAUSE | SUPPORT_PLAY | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_STOP | SUPPORT_VOLUME_MUTE | \ - SUPPORT_VOLUME_STEP +SUPPORT_ANDROIDTV = ( + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_STOP + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_STEP +) -SUPPORT_FIRETV = SUPPORT_PAUSE | SUPPORT_PLAY | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP +SUPPORT_FIRETV = ( + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_SELECT_SOURCE + | SUPPORT_STOP +) -CONF_ADBKEY = 'adbkey' -CONF_ADB_SERVER_IP = 'adb_server_ip' -CONF_ADB_SERVER_PORT = 'adb_server_port' -CONF_APPS = 'apps' -CONF_GET_SOURCES = 'get_sources' -CONF_TURN_ON_COMMAND = 'turn_on_command' -CONF_TURN_OFF_COMMAND = 'turn_off_command' +CONF_ADBKEY = "adbkey" +CONF_ADB_SERVER_IP = "adb_server_ip" +CONF_ADB_SERVER_PORT = "adb_server_port" +CONF_APPS = "apps" +CONF_GET_SOURCES = "get_sources" +CONF_TURN_ON_COMMAND = "turn_on_command" +CONF_TURN_OFF_COMMAND = "turn_off_command" -DEFAULT_NAME = 'Android TV' +DEFAULT_NAME = "Android TV" DEFAULT_PORT = 5555 DEFAULT_ADB_SERVER_PORT = 5037 DEFAULT_GET_SOURCES = True -DEFAULT_DEVICE_CLASS = 'auto' +DEFAULT_DEVICE_CLASS = "auto" -DEVICE_ANDROIDTV = 'androidtv' -DEVICE_FIRETV = 'firetv' +DEVICE_ANDROIDTV = "androidtv" +DEVICE_FIRETV = "firetv" DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV] -SERVICE_ADB_COMMAND = 'adb_command' +SERVICE_ADB_COMMAND = "adb_command" -SERVICE_ADB_COMMAND_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_COMMAND): cv.string, -}) +SERVICE_ADB_COMMAND_SCHEMA = vol.Schema( + {vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_COMMAND): cv.string} +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): - vol.In(DEVICE_CLASSES), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_ADBKEY): cv.isfile, - vol.Optional(CONF_ADB_SERVER_IP): cv.string, - vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): - cv.port, - vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean, - vol.Optional(CONF_APPS, default=dict()): - vol.Schema({cv.string: cv.string}), - vol.Optional(CONF_TURN_ON_COMMAND): cv.string, - vol.Optional(CONF_TURN_OFF_COMMAND): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.In( + DEVICE_CLASSES + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_ADBKEY): cv.isfile, + vol.Optional(CONF_ADB_SERVER_IP): cv.string, + vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port, + vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean, + vol.Optional(CONF_APPS, default=dict()): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_TURN_ON_COMMAND): cv.string, + vol.Optional(CONF_TURN_OFF_COMMAND): cv.string, + } +) # Translate from `AndroidTV` / `FireTV` reported state to HA state. -ANDROIDTV_STATES = {'off': STATE_OFF, - 'idle': STATE_IDLE, - 'standby': STATE_STANDBY, - 'playing': STATE_PLAYING, - 'paused': STATE_PAUSED} +ANDROIDTV_STATES = { + "off": STATE_OFF, + "idle": STATE_IDLE, + "standby": STATE_STANDBY, + "playing": STATE_PLAYING, + "paused": STATE_PAUSED, +} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -86,14 +118,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.data.setdefault(ANDROIDTV_DOMAIN, {}) - host = '{0}:{1}'.format(config[CONF_HOST], config[CONF_PORT]) + host = "{0}:{1}".format(config[CONF_HOST], config[CONF_PORT]) if CONF_ADB_SERVER_IP not in config: # Use "python-adb" (Python ADB implementation) adb_log = "using Python ADB implementation " if CONF_ADBKEY in config: - aftv = setup(host, config[CONF_ADBKEY], - device_class=config[CONF_DEVICE_CLASS]) + aftv = setup( + host, config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS] + ) adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY]) else: @@ -101,44 +134,52 @@ def setup_platform(hass, config, add_entities, discovery_info=None): adb_log += "without adbkey authentication" else: # Use "pure-python-adb" (communicate with ADB server) - aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP], - adb_server_port=config[CONF_ADB_SERVER_PORT], - device_class=config[CONF_DEVICE_CLASS]) + aftv = setup( + host, + adb_server_ip=config[CONF_ADB_SERVER_IP], + adb_server_port=config[CONF_ADB_SERVER_PORT], + device_class=config[CONF_DEVICE_CLASS], + ) adb_log = "using ADB server at {0}:{1}".format( - config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT]) + config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT] + ) if not aftv.available: # Determine the name that will be used for the device in the log if CONF_NAME in config: device_name = config[CONF_NAME] elif config[CONF_DEVICE_CLASS] == DEVICE_ANDROIDTV: - device_name = 'Android TV device' + device_name = "Android TV device" elif config[CONF_DEVICE_CLASS] == DEVICE_FIRETV: - device_name = 'Fire TV device' + device_name = "Fire TV device" else: - device_name = 'Android TV / Fire TV device' + device_name = "Android TV / Fire TV device" - _LOGGER.warning("Could not connect to %s at %s %s", - device_name, host, adb_log) + _LOGGER.warning("Could not connect to %s at %s %s", device_name, host, adb_log) raise PlatformNotReady if host in hass.data[ANDROIDTV_DOMAIN]: _LOGGER.warning("Platform already setup on %s, skipping", host) else: if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV: - device = AndroidTVDevice(aftv, config[CONF_NAME], - config[CONF_APPS], - config.get(CONF_TURN_ON_COMMAND), - config.get(CONF_TURN_OFF_COMMAND)) - device_name = config[CONF_NAME] if CONF_NAME in config \ - else 'Android TV' + device = AndroidTVDevice( + aftv, + config[CONF_NAME], + config[CONF_APPS], + config.get(CONF_TURN_ON_COMMAND), + config.get(CONF_TURN_OFF_COMMAND), + ) + device_name = config[CONF_NAME] if CONF_NAME in config else "Android TV" else: - device = FireTVDevice(aftv, config[CONF_NAME], config[CONF_APPS], - config[CONF_GET_SOURCES], - config.get(CONF_TURN_ON_COMMAND), - config.get(CONF_TURN_OFF_COMMAND)) - device_name = config[CONF_NAME] if CONF_NAME in config \ - else 'Fire TV' + device = FireTVDevice( + aftv, + config[CONF_NAME], + config[CONF_APPS], + config[CONF_GET_SOURCES], + config.get(CONF_TURN_ON_COMMAND), + config.get(CONF_TURN_OFF_COMMAND), + ) + device_name = config[CONF_NAME] if CONF_NAME in config else "Fire TV" add_entities([device]) _LOGGER.debug("Setup %s at %s%s", device_name, host, adb_log) @@ -151,26 +192,38 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Dispatch service calls to target entities.""" cmd = service.data.get(ATTR_COMMAND) entity_id = service.data.get(ATTR_ENTITY_ID) - target_devices = [dev for dev in hass.data[ANDROIDTV_DOMAIN].values() - if dev.entity_id in entity_id] + target_devices = [ + dev + for dev in hass.data[ANDROIDTV_DOMAIN].values() + if dev.entity_id in entity_id + ] for target_device in target_devices: output = target_device.adb_command(cmd) # log the output, if there is any if output: - _LOGGER.info("Output of command '%s' from '%s': %s", - cmd, target_device.entity_id, output) + _LOGGER.info( + "Output of command '%s' from '%s': %s", + cmd, + target_device.entity_id, + output, + ) - hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND, - service_adb_command, - schema=SERVICE_ADB_COMMAND_SCHEMA) + hass.services.register( + ANDROIDTV_DOMAIN, + SERVICE_ADB_COMMAND, + service_adb_command, + schema=SERVICE_ADB_COMMAND_SCHEMA, + ) def adb_decorator(override_available=False): """Send an ADB command if the device is available and catch exceptions.""" + def _adb_decorator(func): """Wait if previous ADB commands haven't finished.""" + @functools.wraps(func) def _adb_exception_catcher(self, *args, **kwargs): # If the device is unavailable, don't do anything @@ -182,7 +235,9 @@ def adb_decorator(override_available=False): except self.exceptions as err: _LOGGER.error( "Failed to execute an ADB command. ADB connection re-" - "establishing attempt in the next update. Error: %s", err) + "establishing attempt in the next update. Error: %s", + err, + ) self._available = False # pylint: disable=protected-access return None @@ -194,8 +249,7 @@ def adb_decorator(override_available=False): class ADBDevice(MediaPlayerDevice): """Representation of an Android TV or Fire TV device.""" - def __init__(self, aftv, name, apps, turn_on_command, - turn_off_command): + def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): """Initialize the Android TV / Fire TV device.""" from androidtv.constants import APPS, KEYS @@ -211,15 +265,23 @@ class ADBDevice(MediaPlayerDevice): # ADB exceptions to catch if not self.aftv.adb_server_ip: # Using "python-adb" (Python ADB implementation) - from adb.adb_protocol import (InvalidChecksumError, - InvalidCommandError, - InvalidResponseError) + from adb.adb_protocol import ( + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + ) from adb.usb_exceptions import TcpTimeoutException - self.exceptions = (AttributeError, BrokenPipeError, TypeError, - ValueError, InvalidChecksumError, - InvalidCommandError, InvalidResponseError, - TcpTimeoutException) + self.exceptions = ( + AttributeError, + BrokenPipeError, + TypeError, + ValueError, + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, + ) else: # Using "pure-python-adb" (communicate with ADB server) self.exceptions = (ConnectionResetError, RuntimeError) @@ -248,7 +310,7 @@ class ADBDevice(MediaPlayerDevice): @property def device_state_attributes(self): """Provide the last ADB command's response as an attribute.""" - return {'adb_response': self._adb_response} + return {"adb_response": self._adb_response} @property def name(self): @@ -311,12 +373,12 @@ class ADBDevice(MediaPlayerDevice): """Send an ADB command to an Android TV / Fire TV device.""" key = self._keys.get(cmd) if key: - self.aftv.adb_shell('input keyevent {}'.format(key)) + self.aftv.adb_shell("input keyevent {}".format(key)) self._adb_response = None self.schedule_update_ha_state() return - if cmd == 'GET_PROPERTIES': + if cmd == "GET_PROPERTIES": self._adb_response = str(self.aftv.get_properties_dict()) self.schedule_update_ha_state() return self._adb_response @@ -334,16 +396,14 @@ class ADBDevice(MediaPlayerDevice): class AndroidTVDevice(ADBDevice): """Representation of an Android TV device.""" - def __init__(self, aftv, name, apps, turn_on_command, - turn_off_command): + def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): """Initialize the Android TV device.""" - super().__init__(aftv, name, apps, turn_on_command, - turn_off_command) + super().__init__(aftv, name, apps, turn_on_command, turn_off_command) self._device = None self._device_properties = self.aftv.device_properties self._is_volume_muted = None - self._unique_id = self._device_properties.get('serialno') + self._unique_id = self._device_properties.get("serialno") self._volume_level = None @adb_decorator(override_available=True) @@ -362,8 +422,9 @@ class AndroidTVDevice(ADBDevice): return # Get the updated state and attributes. - state, self._current_app, self._device, self._is_volume_muted, \ - self._volume_level = self.aftv.update() + state, self._current_app, self._device, self._is_volume_muted, self._volume_level = ( + self.aftv.update() + ) self._state = ANDROIDTV_STATES[state] @@ -416,11 +477,11 @@ class AndroidTVDevice(ADBDevice): class FireTVDevice(ADBDevice): """Representation of a Fire TV device.""" - def __init__(self, aftv, name, apps, get_sources, - turn_on_command, turn_off_command): + def __init__( + self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + ): """Initialize the Fire TV device.""" - super().__init__(aftv, name, apps, turn_on_command, - turn_off_command) + super().__init__(aftv, name, apps, turn_on_command, turn_off_command) self._get_sources = get_sources self._running_apps = None @@ -441,8 +502,9 @@ class FireTVDevice(ADBDevice): return # Get the `state`, `current_app`, and `running_apps`. - state, self._current_app, self._running_apps = \ - self.aftv.update(self._get_sources) + state, self._current_app, self._running_apps = self.aftv.update( + self._get_sources + ) self._state = ANDROIDTV_STATES[state] @@ -474,7 +536,7 @@ class FireTVDevice(ADBDevice): opening it. """ if isinstance(source, str): - if not source.startswith('!'): + if not source.startswith("!"): self.aftv.launch_app(source) else: self.aftv.stop_app(source[1:].lstrip()) diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py index 7552e35fe4b..6184465ef16 100644 --- a/homeassistant/components/anel_pwrctrl/switch.py +++ b/homeassistant/components/anel_pwrctrl/switch.py @@ -6,24 +6,26 @@ from datetime import timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_USERNAME) +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -CONF_PORT_RECV = 'port_recv' -CONF_PORT_SEND = 'port_send' +CONF_PORT_RECV = "port_recv" +CONF_PORT_SEND = "port_send" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PORT_RECV): cv.port, - vol.Required(CONF_PORT_SEND): cv.port, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_HOST): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PORT_RECV): cv.port, + vol.Required(CONF_PORT_SEND): cv.port, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -38,8 +40,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: master = DeviceMaster( - username=username, password=password, read_port=port_send, - write_port=port_recv) + username=username, + password=password, + read_port=port_send, + write_port=port_recv, + ) master.query(ip_addr=host) except socket.error as ex: _LOGGER.error("Unable to discover PwrCtrl device: %s", str(ex)) @@ -49,8 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for device in master.devices.values(): parent_device = PwrCtrlDevice(device) devices.extend( - PwrCtrlSwitch(switch, parent_device) - for switch in device.switches.values() + PwrCtrlSwitch(switch, parent_device) for switch in device.switches.values() ) add_entities(devices) @@ -72,9 +76,8 @@ class PwrCtrlSwitch(SwitchDevice): @property def unique_id(self): """Return the unique ID of the device.""" - return '{device}-{switch_idx}'.format( - device=self._port.device.host, - switch_idx=self._port.get_index() + return "{device}-{switch_idx}".format( + device=self._port.device.host, switch_idx=self._port.get_index() ) @property diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 13c0ef338bc..a033470e5c9 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -3,34 +3,48 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_OFF, - STATE_ON) + CONF_HOST, + CONF_NAME, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, + STATE_OFF, + STATE_ON, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DOMAIN = 'anthemav' +DOMAIN = "anthemav" DEFAULT_PORT = 14999 -SUPPORT_ANTHEMAV = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE +SUPPORT_ANTHEMAV = ( + SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up our socket to the AVR.""" import anthemav @@ -47,8 +61,8 @@ async def async_setup_platform(hass, config, async_add_entities, hass.async_create_task(device.async_update_ha_state()) avr = await anthemav.Connection.create( - host=host, port=port, - update_callback=async_anthemav_update_callback) + host=host, port=port, update_callback=async_anthemav_update_callback + ) device = AnthemAVR(avr, name) @@ -84,12 +98,12 @@ class AnthemAVR(MediaPlayerDevice): @property def name(self): """Return name of device.""" - return self._name or self._lookup('model') + return self._name or self._lookup("model") @property def state(self): """Return state of power on/off.""" - pwrstate = self._lookup('power') + pwrstate = self._lookup("power") if pwrstate is True: return STATE_ON @@ -100,64 +114,64 @@ class AnthemAVR(MediaPlayerDevice): @property def is_volume_muted(self): """Return boolean reflecting mute state on device.""" - return self._lookup('mute', False) + return self._lookup("mute", False) @property def volume_level(self): """Return volume level from 0 to 1.""" - return self._lookup('volume_as_percentage', 0.0) + return self._lookup("volume_as_percentage", 0.0) @property def media_title(self): """Return current input name (closest we have to media title).""" - return self._lookup('input_name', 'No Source') + return self._lookup("input_name", "No Source") @property def app_name(self): """Return details about current video and audio stream.""" - return self._lookup('video_input_resolution_text', '') + ' ' \ - + self._lookup('audio_input_name', '') + return ( + self._lookup("video_input_resolution_text", "") + + " " + + self._lookup("audio_input_name", "") + ) @property def source(self): """Return currently selected input.""" - return self._lookup('input_name', "Unknown") + return self._lookup("input_name", "Unknown") @property def source_list(self): """Return all active, configured inputs.""" - return self._lookup('input_list', ["Unknown"]) + return self._lookup("input_list", ["Unknown"]) async def async_select_source(self, source): """Change AVR to the designated source (by name).""" - self._update_avr('input_name', source) + self._update_avr("input_name", source) async def async_turn_off(self): """Turn AVR power off.""" - self._update_avr('power', False) + self._update_avr("power", False) async def async_turn_on(self): """Turn AVR power on.""" - self._update_avr('power', True) + self._update_avr("power", True) async def async_set_volume_level(self, volume): """Set AVR volume (0 to 1).""" - self._update_avr('volume_as_percentage', volume) + self._update_avr("volume_as_percentage", volume) async def async_mute_volume(self, mute): """Engage AVR mute.""" - self._update_avr('mute', mute) + self._update_avr("mute", mute) def _update_avr(self, propname, value): """Update a property in the AVR.""" - _LOGGER.info( - "Sending command to AVR: set %s to %s", propname, str(value)) + _LOGGER.info("Sending command to AVR: set %s to %s", propname, str(value)) setattr(self.avr.protocol, propname, value) @property def dump_avrdata(self): """Return state of avr object for debugging forensics.""" attrs = vars(self) - return( - 'dump_avrdata: ' - + ', '.join('%s: %s' % item for item in attrs.items())) + return "dump_avrdata: " + ", ".join("%s: %s" % item for item in attrs.items()) diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index e8617eaf317..caf96c61fb8 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -7,26 +7,36 @@ from aiokafka import AIOKafkaProducer import voluptuous as vol from homeassistant.const import ( - CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, - STATE_UNAVAILABLE, STATE_UNKNOWN) + CONF_IP_ADDRESS, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, + EVENT_STATE_CHANGED, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA _LOGGER = logging.getLogger(__name__) -DOMAIN = 'apache_kafka' +DOMAIN = "apache_kafka" -CONF_FILTER = 'filter' -CONF_TOPIC = 'topic' +CONF_FILTER = "filter" +CONF_TOPIC = "topic" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_TOPIC): cv.string, - vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_TOPIC): cv.string, + vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -38,7 +48,8 @@ async def async_setup(hass, config): conf[CONF_IP_ADDRESS], conf[CONF_PORT], conf[CONF_TOPIC], - conf[CONF_FILTER]) + conf[CONF_FILTER], + ) hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, kafka.shutdown()) @@ -63,13 +74,7 @@ class DateTimeJSONEncoder(json.JSONEncoder): class KafkaManager: """Define a manager to buffer events to Kafka.""" - def __init__( - self, - hass, - ip_address, - port, - topic, - entities_filter): + def __init__(self, hass, ip_address, port, topic, entities_filter): """Initialize.""" self._encoder = DateTimeJSONEncoder() self._entities_filter = entities_filter @@ -83,16 +88,17 @@ class KafkaManager: def _encode_event(self, event): """Translate events into a binary JSON payload.""" - state = event.data.get('new_state') - if (state is None - or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE) - or not self._entities_filter(state.entity_id)): + state = event.data.get("new_state") + if ( + state is None + or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) + or not self._entities_filter(state.entity_id) + ): return - return json.dumps( - obj=state.as_dict(), - default=self._encoder.encode - ).encode('utf-8') + return json.dumps(obj=state.as_dict(), default=self._encoder.encode).encode( + "utf-8" + ) async def start(self): """Start the Kafka manager.""" diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index d4649db0203..512bd01b72a 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -4,31 +4,36 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.const import (CONF_HOST, CONF_PORT) +from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -CONF_TYPE = 'type' +CONF_TYPE = "type" DATA = None -DEFAULT_HOST = 'localhost' +DEFAULT_HOST = "localhost" DEFAULT_PORT = 3551 -DOMAIN = 'apcupsd' +DOMAIN = "apcupsd" -KEY_STATUS = 'STATUS' +KEY_STATUS = "STATUS" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -VALUE_ONLINE = 'ONLINE' +VALUE_ONLINE = "ONLINE" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -60,6 +65,7 @@ class APCUPSdData: def __init__(self, host, port): """Initialize the data object.""" from apcaccess import status + self._host = host self._port = port self._status = None diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 367b3c2b9b5..62f0c90a447 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -1,16 +1,15 @@ """Support for tracking the online status of a UPS.""" import voluptuous as vol -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.components import apcupsd -DEFAULT_NAME = 'UPS Online Status' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +DEFAULT_NAME = "UPS Online Status" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} +) def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index ae1ad10223d..837e6e45c6c 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -6,101 +6,102 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv from homeassistant.components import apcupsd -from homeassistant.const import (TEMP_CELSIUS, CONF_RESOURCES, POWER_WATT) +from homeassistant.const import TEMP_CELSIUS, CONF_RESOURCES, POWER_WATT from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -SENSOR_PREFIX = 'UPS ' +SENSOR_PREFIX = "UPS " SENSOR_TYPES = { - 'alarmdel': ['Alarm Delay', '', 'mdi:alarm'], - 'ambtemp': ['Ambient Temperature', '', 'mdi:thermometer'], - 'apc': ['Status Data', '', 'mdi:information-outline'], - 'apcmodel': ['Model', '', 'mdi:information-outline'], - 'badbatts': ['Bad Batteries', '', 'mdi:information-outline'], - 'battdate': ['Battery Replaced', '', 'mdi:calendar-clock'], - 'battstat': ['Battery Status', '', 'mdi:information-outline'], - 'battv': ['Battery Voltage', 'V', 'mdi:flash'], - 'bcharge': ['Battery', '%', 'mdi:battery'], - 'cable': ['Cable Type', '', 'mdi:ethernet-cable'], - 'cumonbatt': ['Total Time on Battery', '', 'mdi:timer'], - 'date': ['Status Date', '', 'mdi:calendar-clock'], - 'dipsw': ['Dip Switch Settings', '', 'mdi:information-outline'], - 'dlowbatt': ['Low Battery Signal', '', 'mdi:clock-alert'], - 'driver': ['Driver', '', 'mdi:information-outline'], - 'dshutd': ['Shutdown Delay', '', 'mdi:timer'], - 'dwake': ['Wake Delay', '', 'mdi:timer'], - 'endapc': ['Date and Time', '', 'mdi:calendar-clock'], - 'extbatts': ['External Batteries', '', 'mdi:information-outline'], - 'firmware': ['Firmware Version', '', 'mdi:information-outline'], - 'hitrans': ['Transfer High', 'V', 'mdi:flash'], - 'hostname': ['Hostname', '', 'mdi:information-outline'], - 'humidity': ['Ambient Humidity', '%', 'mdi:water-percent'], - 'itemp': ['Internal Temperature', TEMP_CELSIUS, 'mdi:thermometer'], - 'lastxfer': ['Last Transfer', '', 'mdi:transfer'], - 'linefail': ['Input Voltage Status', '', 'mdi:information-outline'], - 'linefreq': ['Line Frequency', 'Hz', 'mdi:information-outline'], - 'linev': ['Input Voltage', 'V', 'mdi:flash'], - 'loadpct': ['Load', '%', 'mdi:gauge'], - 'loadapnt': ['Load Apparent Power', '%', 'mdi:gauge'], - 'lotrans': ['Transfer Low', 'V', 'mdi:flash'], - 'mandate': ['Manufacture Date', '', 'mdi:calendar'], - 'masterupd': ['Master Update', '', 'mdi:information-outline'], - 'maxlinev': ['Input Voltage High', 'V', 'mdi:flash'], - 'maxtime': ['Battery Timeout', '', 'mdi:timer-off'], - 'mbattchg': ['Battery Shutdown', '%', 'mdi:battery-alert'], - 'minlinev': ['Input Voltage Low', 'V', 'mdi:flash'], - 'mintimel': ['Shutdown Time', '', 'mdi:timer'], - 'model': ['Model', '', 'mdi:information-outline'], - 'nombattv': ['Battery Nominal Voltage', 'V', 'mdi:flash'], - 'nominv': ['Nominal Input Voltage', 'V', 'mdi:flash'], - 'nomoutv': ['Nominal Output Voltage', 'V', 'mdi:flash'], - 'nompower': ['Nominal Output Power', POWER_WATT, 'mdi:flash'], - 'nomapnt': ['Nominal Apparent Power', 'VA', 'mdi:flash'], - 'numxfers': ['Transfer Count', '', 'mdi:counter'], - 'outcurnt': ['Output Current', 'A', 'mdi:flash'], - 'outputv': ['Output Voltage', 'V', 'mdi:flash'], - 'reg1': ['Register 1 Fault', '', 'mdi:information-outline'], - 'reg2': ['Register 2 Fault', '', 'mdi:information-outline'], - 'reg3': ['Register 3 Fault', '', 'mdi:information-outline'], - 'retpct': ['Restore Requirement', '%', 'mdi:battery-alert'], - 'selftest': ['Last Self Test', '', 'mdi:calendar-clock'], - 'sense': ['Sensitivity', '', 'mdi:information-outline'], - 'serialno': ['Serial Number', '', 'mdi:information-outline'], - 'starttime': ['Startup Time', '', 'mdi:calendar-clock'], - 'statflag': ['Status Flag', '', 'mdi:information-outline'], - 'status': ['Status', '', 'mdi:information-outline'], - 'stesti': ['Self Test Interval', '', 'mdi:information-outline'], - 'timeleft': ['Time Left', '', 'mdi:clock-alert'], - 'tonbatt': ['Time on Battery', '', 'mdi:timer'], - 'upsmode': ['Mode', '', 'mdi:information-outline'], - 'upsname': ['Name', '', 'mdi:information-outline'], - 'version': ['Daemon Info', '', 'mdi:information-outline'], - 'xoffbat': ['Transfer from Battery', '', 'mdi:transfer'], - 'xoffbatt': ['Transfer from Battery', '', 'mdi:transfer'], - 'xonbatt': ['Transfer to Battery', '', 'mdi:transfer'], + "alarmdel": ["Alarm Delay", "", "mdi:alarm"], + "ambtemp": ["Ambient Temperature", "", "mdi:thermometer"], + "apc": ["Status Data", "", "mdi:information-outline"], + "apcmodel": ["Model", "", "mdi:information-outline"], + "badbatts": ["Bad Batteries", "", "mdi:information-outline"], + "battdate": ["Battery Replaced", "", "mdi:calendar-clock"], + "battstat": ["Battery Status", "", "mdi:information-outline"], + "battv": ["Battery Voltage", "V", "mdi:flash"], + "bcharge": ["Battery", "%", "mdi:battery"], + "cable": ["Cable Type", "", "mdi:ethernet-cable"], + "cumonbatt": ["Total Time on Battery", "", "mdi:timer"], + "date": ["Status Date", "", "mdi:calendar-clock"], + "dipsw": ["Dip Switch Settings", "", "mdi:information-outline"], + "dlowbatt": ["Low Battery Signal", "", "mdi:clock-alert"], + "driver": ["Driver", "", "mdi:information-outline"], + "dshutd": ["Shutdown Delay", "", "mdi:timer"], + "dwake": ["Wake Delay", "", "mdi:timer"], + "endapc": ["Date and Time", "", "mdi:calendar-clock"], + "extbatts": ["External Batteries", "", "mdi:information-outline"], + "firmware": ["Firmware Version", "", "mdi:information-outline"], + "hitrans": ["Transfer High", "V", "mdi:flash"], + "hostname": ["Hostname", "", "mdi:information-outline"], + "humidity": ["Ambient Humidity", "%", "mdi:water-percent"], + "itemp": ["Internal Temperature", TEMP_CELSIUS, "mdi:thermometer"], + "lastxfer": ["Last Transfer", "", "mdi:transfer"], + "linefail": ["Input Voltage Status", "", "mdi:information-outline"], + "linefreq": ["Line Frequency", "Hz", "mdi:information-outline"], + "linev": ["Input Voltage", "V", "mdi:flash"], + "loadpct": ["Load", "%", "mdi:gauge"], + "loadapnt": ["Load Apparent Power", "%", "mdi:gauge"], + "lotrans": ["Transfer Low", "V", "mdi:flash"], + "mandate": ["Manufacture Date", "", "mdi:calendar"], + "masterupd": ["Master Update", "", "mdi:information-outline"], + "maxlinev": ["Input Voltage High", "V", "mdi:flash"], + "maxtime": ["Battery Timeout", "", "mdi:timer-off"], + "mbattchg": ["Battery Shutdown", "%", "mdi:battery-alert"], + "minlinev": ["Input Voltage Low", "V", "mdi:flash"], + "mintimel": ["Shutdown Time", "", "mdi:timer"], + "model": ["Model", "", "mdi:information-outline"], + "nombattv": ["Battery Nominal Voltage", "V", "mdi:flash"], + "nominv": ["Nominal Input Voltage", "V", "mdi:flash"], + "nomoutv": ["Nominal Output Voltage", "V", "mdi:flash"], + "nompower": ["Nominal Output Power", POWER_WATT, "mdi:flash"], + "nomapnt": ["Nominal Apparent Power", "VA", "mdi:flash"], + "numxfers": ["Transfer Count", "", "mdi:counter"], + "outcurnt": ["Output Current", "A", "mdi:flash"], + "outputv": ["Output Voltage", "V", "mdi:flash"], + "reg1": ["Register 1 Fault", "", "mdi:information-outline"], + "reg2": ["Register 2 Fault", "", "mdi:information-outline"], + "reg3": ["Register 3 Fault", "", "mdi:information-outline"], + "retpct": ["Restore Requirement", "%", "mdi:battery-alert"], + "selftest": ["Last Self Test", "", "mdi:calendar-clock"], + "sense": ["Sensitivity", "", "mdi:information-outline"], + "serialno": ["Serial Number", "", "mdi:information-outline"], + "starttime": ["Startup Time", "", "mdi:calendar-clock"], + "statflag": ["Status Flag", "", "mdi:information-outline"], + "status": ["Status", "", "mdi:information-outline"], + "stesti": ["Self Test Interval", "", "mdi:information-outline"], + "timeleft": ["Time Left", "", "mdi:clock-alert"], + "tonbatt": ["Time on Battery", "", "mdi:timer"], + "upsmode": ["Mode", "", "mdi:information-outline"], + "upsname": ["Name", "", "mdi:information-outline"], + "version": ["Daemon Info", "", "mdi:information-outline"], + "xoffbat": ["Transfer from Battery", "", "mdi:transfer"], + "xoffbatt": ["Transfer from Battery", "", "mdi:transfer"], + "xonbatt": ["Transfer to Battery", "", "mdi:transfer"], } -SPECIFIC_UNITS = { - 'ITEMP': TEMP_CELSIUS -} +SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS} INFERRED_UNITS = { - ' Minutes': 'min', - ' Seconds': 'sec', - ' Percent': '%', - ' Volts': 'V', - ' Ampere': 'A', - ' Volt-Ampere': 'VA', - ' Watts': POWER_WATT, - ' Hz': 'Hz', - ' C': TEMP_CELSIUS, - ' Percent Load Capacity': '%', + " Minutes": "min", + " Seconds": "sec", + " Percent": "%", + " Volts": "V", + " Ampere": "A", + " Volt-Ampere": "VA", + " Watts": POWER_WATT, + " Hz": "Hz", + " C": TEMP_CELSIUS, + " Percent Load Capacity": "%", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_RESOURCES, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_RESOURCES, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ) + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -112,12 +113,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if sensor_type not in SENSOR_TYPES: SENSOR_TYPES[sensor_type] = [ - sensor_type.title(), '', 'mdi:information-outline'] + sensor_type.title(), + "", + "mdi:information-outline", + ] if sensor_type.upper() not in apcupsd.DATA.status: _LOGGER.warning( "Sensor type: %s does not appear in the APCUPSd status output", - sensor_type) + sensor_type, + ) entities.append(APCUPSdSensor(apcupsd.DATA, sensor_type)) @@ -131,9 +136,10 @@ def infer_unit(value): pair. Else return the original value and None as the unit. """ from apcaccess.status import ALL_UNITS + for unit in ALL_UNITS: if value.endswith(unit): - return value[:-len(unit)], INFERRED_UNITS.get(unit, unit.strip()) + return value[: -len(unit)], INFERRED_UNITS.get(unit, unit.strip()) return value, None @@ -178,4 +184,5 @@ class APCUPSdSensor(Entity): self._inferred_unit = None else: self._state, self._inferred_unit = infer_unit( - self._data.status[self.type.upper()]) + self._data.status[self.type.upper()] + ) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index feea4f21c9c..ee991535104 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -11,15 +11,28 @@ import voluptuous as vol from homeassistant.bootstrap import DATA_LOGGING from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST, - HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS, - URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS, - URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, - URL_API_TEMPLATE, __version__) + EVENT_HOMEASSISTANT_STOP, + EVENT_TIME_CHANGED, + HTTP_BAD_REQUEST, + HTTP_CREATED, + HTTP_NOT_FOUND, + MATCH_ALL, + URL_API, + URL_API_COMPONENTS, + URL_API_CONFIG, + URL_API_DISCOVERY_INFO, + URL_API_ERROR_LOG, + URL_API_EVENTS, + URL_API_SERVICES, + URL_API_STATES, + URL_API_STATES_ENTITY, + URL_API_STREAM, + URL_API_TEMPLATE, + __version__, +) import homeassistant.core as ha from homeassistant.auth.permissions.const import POLICY_READ -from homeassistant.exceptions import ( - TemplateError, Unauthorized, ServiceNotFound) +from homeassistant.exceptions import TemplateError, Unauthorized, ServiceNotFound from homeassistant.helpers import template from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.state import AsyncTrackStates @@ -27,13 +40,13 @@ from homeassistant.helpers.json import JSONEncoder _LOGGER = logging.getLogger(__name__) -ATTR_BASE_URL = 'base_url' -ATTR_LOCATION_NAME = 'location_name' -ATTR_REQUIRES_API_PASSWORD = 'requires_api_password' -ATTR_VERSION = 'version' +ATTR_BASE_URL = "base_url" +ATTR_LOCATION_NAME = "location_name" +ATTR_REQUIRES_API_PASSWORD = "requires_api_password" +ATTR_VERSION = "version" -DOMAIN = 'api' -STREAM_PING_PAYLOAD = 'ping' +DOMAIN = "api" +STREAM_PING_PAYLOAD = "ping" STREAM_PING_INTERVAL = 50 # seconds @@ -62,7 +75,7 @@ class APIStatusView(HomeAssistantView): """View to handle Status requests.""" url = URL_API - name = 'api:status' + name = "api:status" @ha.callback def get(self, request): @@ -74,19 +87,19 @@ class APIEventStream(HomeAssistantView): """View to handle EventStream requests.""" url = URL_API_STREAM - name = 'api:stream' + name = "api:stream" async def get(self, request): """Provide a streaming interface for the event bus.""" - if not request['hass_user'].is_admin: + if not request["hass_user"].is_admin: raise Unauthorized() - hass = request.app['hass'] + hass = request.app["hass"] stop_obj = object() to_write = asyncio.Queue() - restrict = request.query.get('restrict') + restrict = request.query.get("restrict") if restrict: - restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] + restrict = restrict.split(",") + [EVENT_HOMEASSISTANT_STOP] async def forward_events(event): """Forward events to the open request.""" @@ -106,7 +119,7 @@ class APIEventStream(HomeAssistantView): await to_write.put(data) response = web.StreamResponse() - response.content_type = 'text/event-stream' + response.content_type = "text/event-stream" await response.prepare(request) unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) @@ -126,9 +139,8 @@ class APIEventStream(HomeAssistantView): break msg = "data: {}\n\n".format(payload) - _LOGGER.debug( - "STREAM %s WRITING %s", id(stop_obj), msg.strip()) - await response.write(msg.encode('UTF-8')) + _LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip()) + await response.write(msg.encode("UTF-8")) except asyncio.TimeoutError: await to_write.put(STREAM_PING_PAYLOAD) @@ -146,12 +158,12 @@ class APIConfigView(HomeAssistantView): """View to handle Configuration requests.""" url = URL_API_CONFIG - name = 'api:config' + name = "api:config" @ha.callback def get(self, request): """Get current configuration.""" - return self.json(request.app['hass'].config.as_dict()) + return self.json(request.app["hass"].config.as_dict()) class APIDiscoveryView(HomeAssistantView): @@ -159,19 +171,21 @@ class APIDiscoveryView(HomeAssistantView): requires_auth = False url = URL_API_DISCOVERY_INFO - name = 'api:discovery' + name = "api:discovery" @ha.callback def get(self, request): """Get discovery information.""" - hass = request.app['hass'] - return self.json({ - ATTR_BASE_URL: hass.config.api.base_url, - ATTR_LOCATION_NAME: hass.config.location_name, - # always needs authentication - ATTR_REQUIRES_API_PASSWORD: True, - ATTR_VERSION: __version__, - }) + hass = request.app["hass"] + return self.json( + { + ATTR_BASE_URL: hass.config.api.base_url, + ATTR_LOCATION_NAME: hass.config.location_name, + # always needs authentication + ATTR_REQUIRES_API_PASSWORD: True, + ATTR_VERSION: __version__, + } + ) class APIStatesView(HomeAssistantView): @@ -183,11 +197,12 @@ class APIStatesView(HomeAssistantView): @ha.callback def get(self, request): """Get current states.""" - user = request['hass_user'] + user = request["hass_user"] entity_perm = user.permissions.check_entity states = [ - state for state in request.app['hass'].states.async_all() - if entity_perm(state.entity_id, 'read') + state + for state in request.app["hass"].states.async_all() + if entity_perm(state.entity_id, "read") ] return self.json(states) @@ -195,60 +210,60 @@ class APIStatesView(HomeAssistantView): class APIEntityStateView(HomeAssistantView): """View to handle EntityState requests.""" - url = '/api/states/{entity_id}' - name = 'api:entity-state' + url = "/api/states/{entity_id}" + name = "api:entity-state" @ha.callback def get(self, request, entity_id): """Retrieve state of entity.""" - user = request['hass_user'] + user = request["hass_user"] if not user.permissions.check_entity(entity_id, POLICY_READ): raise Unauthorized(entity_id=entity_id) - state = request.app['hass'].states.get(entity_id) + state = request.app["hass"].states.get(entity_id) if state: return self.json(state) return self.json_message("Entity not found.", HTTP_NOT_FOUND) async def post(self, request, entity_id): """Update state of entity.""" - if not request['hass_user'].is_admin: + if not request["hass_user"].is_admin: raise Unauthorized(entity_id=entity_id) - hass = request.app['hass'] + hass = request.app["hass"] try: data = await request.json() except ValueError: - return self.json_message( - "Invalid JSON specified.", HTTP_BAD_REQUEST) + return self.json_message("Invalid JSON specified.", HTTP_BAD_REQUEST) - new_state = data.get('state') + new_state = data.get("state") if new_state is None: return self.json_message("No state specified.", HTTP_BAD_REQUEST) - attributes = data.get('attributes') - force_update = data.get('force_update', False) + attributes = data.get("attributes") + force_update = data.get("force_update", False) is_new_state = hass.states.get(entity_id) is None # Write state - hass.states.async_set(entity_id, new_state, attributes, force_update, - self.context(request)) + hass.states.async_set( + entity_id, new_state, attributes, force_update, self.context(request) + ) # Read the state back for our response status_code = HTTP_CREATED if is_new_state else 200 resp = self.json(hass.states.get(entity_id), status_code) - resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id)) + resp.headers.add("Location", URL_API_STATES_ENTITY.format(entity_id)) return resp @ha.callback def delete(self, request, entity_id): """Remove entity.""" - if not request['hass_user'].is_admin: + if not request["hass_user"].is_admin: raise Unauthorized(entity_id=entity_id) - if request.app['hass'].states.async_remove(entity_id): + if request.app["hass"].states.async_remove(entity_id): return self.json_message("Entity removed.") return self.json_message("Entity not found.", HTTP_NOT_FOUND) @@ -257,47 +272,49 @@ class APIEventListenersView(HomeAssistantView): """View to handle EventListeners requests.""" url = URL_API_EVENTS - name = 'api:event-listeners' + name = "api:event-listeners" @ha.callback def get(self, request): """Get event listeners.""" - return self.json(async_events_json(request.app['hass'])) + return self.json(async_events_json(request.app["hass"])) class APIEventView(HomeAssistantView): """View to handle Event requests.""" - url = '/api/events/{event_type}' - name = 'api:event' + url = "/api/events/{event_type}" + name = "api:event" async def post(self, request, event_type): """Fire events.""" - if not request['hass_user'].is_admin: + if not request["hass_user"].is_admin: raise Unauthorized() body = await request.text() try: event_data = json.loads(body) if body else None except ValueError: return self.json_message( - "Event data should be valid JSON.", HTTP_BAD_REQUEST) + "Event data should be valid JSON.", HTTP_BAD_REQUEST + ) if event_data is not None and not isinstance(event_data, dict): return self.json_message( - "Event data should be a JSON object", HTTP_BAD_REQUEST) + "Event data should be a JSON object", HTTP_BAD_REQUEST + ) # Special case handling for event STATE_CHANGED # We will try to convert state dicts back to State objects if event_type == ha.EVENT_STATE_CHANGED and event_data: - for key in ('old_state', 'new_state'): + for key in ("old_state", "new_state"): state = ha.State.from_dict(event_data.get(key)) if state: event_data[key] = state - request.app['hass'].bus.async_fire( - event_type, event_data, ha.EventOrigin.remote, - self.context(request)) + request.app["hass"].bus.async_fire( + event_type, event_data, ha.EventOrigin.remote, self.context(request) + ) return self.json_message("Event {} fired.".format(event_type)) @@ -306,37 +323,37 @@ class APIServicesView(HomeAssistantView): """View to handle Services requests.""" url = URL_API_SERVICES - name = 'api:services' + name = "api:services" async def get(self, request): """Get registered services.""" - services = await async_services_json(request.app['hass']) + services = await async_services_json(request.app["hass"]) return self.json(services) class APIDomainServicesView(HomeAssistantView): """View to handle DomainServices requests.""" - url = '/api/services/{domain}/{service}' - name = 'api:domain-services' + url = "/api/services/{domain}/{service}" + name = "api:domain-services" async def post(self, request, domain, service): """Call a service. Returns a list of changed states. """ - hass = request.app['hass'] + hass = request.app["hass"] body = await request.text() try: data = json.loads(body) if body else None except ValueError: - return self.json_message( - "Data should be valid JSON.", HTTP_BAD_REQUEST) + return self.json_message("Data should be valid JSON.", HTTP_BAD_REQUEST) with AsyncTrackStates(hass) as changed_states: try: await hass.services.async_call( - domain, service, data, True, self.context(request)) + domain, service, data, True, self.context(request) + ) except (vol.Invalid, ServiceNotFound): raise HTTPBadRequest() @@ -347,54 +364,56 @@ class APIComponentsView(HomeAssistantView): """View to handle Components requests.""" url = URL_API_COMPONENTS - name = 'api:components' + name = "api:components" @ha.callback def get(self, request): """Get current loaded components.""" - return self.json(request.app['hass'].config.components) + return self.json(request.app["hass"].config.components) class APITemplateView(HomeAssistantView): """View to handle Template requests.""" url = URL_API_TEMPLATE - name = 'api:template' + name = "api:template" async def post(self, request): """Render a template.""" - if not request['hass_user'].is_admin: + if not request["hass_user"].is_admin: raise Unauthorized() try: data = await request.json() - tpl = template.Template(data['template'], request.app['hass']) - return tpl.async_render(data.get('variables')) + tpl = template.Template(data["template"], request.app["hass"]) + return tpl.async_render(data.get("variables")) except (ValueError, TemplateError) as ex: return self.json_message( - "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST) + "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST + ) class APIErrorLog(HomeAssistantView): """View to fetch the API error log.""" url = URL_API_ERROR_LOG - name = 'api:error_log' + name = "api:error_log" async def get(self, request): """Retrieve API error log.""" - if not request['hass_user'].is_admin: + if not request["hass_user"].is_admin: raise Unauthorized() - return web.FileResponse(request.app['hass'].data[DATA_LOGGING]) + return web.FileResponse(request.app["hass"].data[DATA_LOGGING]) async def async_services_json(hass): """Generate services data to JSONify.""" descriptions = await async_get_all_descriptions(hass) - return [{'domain': key, 'services': value} - for key, value in descriptions.items()] + return [{"domain": key, "services": value} for key, value in descriptions.items()] def async_events_json(hass): """Generate event data to JSONify.""" - return [{'event': key, 'listener_count': value} - for key, value in hass.bus.async_listeners().items()] + return [ + {"event": key, "listener_count": value} + for key, value in hass.bus.async_listeners().items() + ] diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index ccf7c495f39..0b95cb9f0cb 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -10,29 +10,35 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_state_change from homeassistant.components.notify import ( - ATTR_DATA, ATTR_TARGET, DOMAIN, PLATFORM_SCHEMA, BaseNotificationService) + ATTR_DATA, + ATTR_TARGET, + DOMAIN, + PLATFORM_SCHEMA, + BaseNotificationService, +) -APNS_DEVICES = 'apns.yaml' -CONF_CERTFILE = 'cert_file' -CONF_TOPIC = 'topic' -CONF_SANDBOX = 'sandbox' -DEVICE_TRACKER_DOMAIN = 'device_tracker' -SERVICE_REGISTER = 'apns_register' +APNS_DEVICES = "apns.yaml" +CONF_CERTFILE = "cert_file" +CONF_TOPIC = "topic" +CONF_SANDBOX = "sandbox" +DEVICE_TRACKER_DOMAIN = "device_tracker" +SERVICE_REGISTER = "apns_register" -ATTR_PUSH_ID = 'push_id' +ATTR_PUSH_ID = "push_id" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PLATFORM): 'apns', - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_CERTFILE): cv.isfile, - vol.Required(CONF_TOPIC): cv.string, - vol.Optional(CONF_SANDBOX, default=False): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PLATFORM): "apns", + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_CERTFILE): cv.isfile, + vol.Required(CONF_TOPIC): cv.string, + vol.Optional(CONF_SANDBOX, default=False): cv.boolean, + } +) -REGISTER_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_PUSH_ID): cv.string, - vol.Optional(ATTR_NAME): cv.string, -}) +REGISTER_SERVICE_SCHEMA = vol.Schema( + {vol.Required(ATTR_PUSH_ID): cv.string, vol.Optional(ATTR_NAME): cv.string} +) def get_service(hass, config, discovery_info=None): @@ -44,8 +50,8 @@ def get_service(hass, config, discovery_info=None): service = ApnsNotificationService(hass, name, topic, sandbox, cert_file) hass.services.register( - DOMAIN, 'apns_{}'.format(name), service.register, - schema=REGISTER_SERVICE_SCHEMA) + DOMAIN, "apns_{}".format(name), service.register, schema=REGISTER_SERVICE_SCHEMA + ) return service @@ -92,7 +98,7 @@ class ApnsDevice: The full id of a device that is tracked by the device tracking component. """ - return '{}.{}'.format(DEVICE_TRACKER_DOMAIN, self.tracking_id) + return "{}.{}".format(DEVICE_TRACKER_DOMAIN, self.tracking_id) @property def disabled(self): @@ -118,13 +124,11 @@ def _write_device(out, device): """Write a single device to file.""" attributes = [] if device.name is not None: - attributes.append( - 'name: {}'.format(device.name)) + attributes.append("name: {}".format(device.name)) if device.tracking_device_id is not None: - attributes.append( - 'tracking_device_id: {}'.format(device.tracking_device_id)) + attributes.append("tracking_device_id: {}".format(device.tracking_device_id)) if device.disabled: - attributes.append('disabled: True') + attributes.append("disabled: True") out.write(device.push_id) out.write(": {") @@ -144,7 +148,7 @@ class ApnsNotificationService(BaseNotificationService): self.app_name = app_name self.sandbox = sandbox self.certificate = cert_file - self.yaml_path = hass.config.path(app_name + '_' + APNS_DEVICES) + self.yaml_path = hass.config.path(app_name + "_" + APNS_DEVICES) self.devices = {} self.device_states = {} self.topic = topic @@ -153,12 +157,11 @@ class ApnsNotificationService(BaseNotificationService): self.devices = { str(key): ApnsDevice( str(key), - value.get('name'), - value.get('tracking_device_id'), - value.get('disabled', False) + value.get("name"), + value.get("tracking_device_id"), + value.get("disabled", False), ) - for (key, value) in - load_yaml_config_file(self.yaml_path).items() + for (key, value) in load_yaml_config_file(self.yaml_path).items() } except FileNotFoundError: pass @@ -168,8 +171,7 @@ class ApnsNotificationService(BaseNotificationService): for (key, device) in self.devices.items() if device.tracking_device_id is not None ] - track_state_change( - hass, tracking_ids, self.device_state_changed_listener) + track_state_change(hass, tracking_ids, self.device_state_changed_listener) def device_state_changed_listener(self, entity_id, from_s, to_s): """ @@ -181,7 +183,7 @@ class ApnsNotificationService(BaseNotificationService): def write_devices(self): """Write all known devices to file.""" - with open(self.yaml_path, 'w+') as out: + with open(self.yaml_path, "w+") as out: for _, device in self.devices.items(): _write_device(out, device) @@ -191,14 +193,15 @@ class ApnsNotificationService(BaseNotificationService): device_name = call.data.get(ATTR_NAME) current_device = self.devices.get(push_id) - current_tracking_id = None if current_device is None \ - else current_device.tracking_device_id + current_tracking_id = ( + None if current_device is None else current_device.tracking_device_id + ) device = ApnsDevice(push_id, device_name, current_tracking_id) if current_device is None: self.devices[push_id] = device - with open(self.yaml_path, 'a') as out: + with open(self.yaml_path, "a") as out: _write_device(out, device) return True @@ -215,9 +218,8 @@ class ApnsNotificationService(BaseNotificationService): from apns2.errors import Unregistered apns = APNsClient( - self.certificate, - use_sandbox=self.sandbox, - use_alternative_port=False) + self.certificate, use_sandbox=self.sandbox, use_alternative_port=False + ) device_state = kwargs.get(ATTR_TARGET) message_data = kwargs.get(ATTR_DATA) @@ -230,15 +232,16 @@ class ApnsNotificationService(BaseNotificationService): elif isinstance(message, template_helper.Template): rendered_message = message.render() else: - rendered_message = '' + rendered_message = "" payload = Payload( alert=rendered_message, - badge=message_data.get('badge'), - sound=message_data.get('sound'), - category=message_data.get('category'), - custom=message_data.get('custom', {}), - content_available=message_data.get('content_available', False)) + badge=message_data.get("badge"), + sound=message_data.get("sound"), + category=message_data.get("category"), + custom=message_data.get("custom", {}), + content_available=message_data.get("content_available", False), + ) device_update = False @@ -246,13 +249,11 @@ class ApnsNotificationService(BaseNotificationService): if not device.disabled: state = None if device.tracking_device_id is not None: - state = self.device_states.get( - device.full_tracking_device_id) + state = self.device_states.get(device.full_tracking_device_id) if device_state is None or state == str(device_state): try: - apns.send_notification( - push_id, payload, topic=self.topic) + apns.send_notification(push_id, payload, topic=self.topic) except Unregistered: logging.error("Device %s has unregistered", push_id) device_update = True diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 80da26195ee..51c2ee7e1a5 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -13,31 +13,31 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DOMAIN = 'apple_tv' +DOMAIN = "apple_tv" -SERVICE_SCAN = 'apple_tv_scan' -SERVICE_AUTHENTICATE = 'apple_tv_authenticate' +SERVICE_SCAN = "apple_tv_scan" +SERVICE_AUTHENTICATE = "apple_tv_authenticate" -ATTR_ATV = 'atv' -ATTR_POWER = 'power' +ATTR_ATV = "atv" +ATTR_POWER = "power" -CONF_LOGIN_ID = 'login_id' -CONF_START_OFF = 'start_off' -CONF_CREDENTIALS = 'credentials' +CONF_LOGIN_ID = "login_id" +CONF_START_OFF = "start_off" +CONF_CREDENTIALS = "credentials" -DEFAULT_NAME = 'Apple TV' +DEFAULT_NAME = "Apple TV" -DATA_APPLE_TV = 'data_apple_tv' -DATA_ENTITIES = 'data_apple_tv_entities' +DATA_APPLE_TV = "data_apple_tv" +DATA_ENTITIES = "data_apple_tv_entities" -KEY_CONFIG = 'apple_tv_configuring' +KEY_CONFIG = "apple_tv_configuring" -NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification' -NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication' -NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification' -NOTIFICATION_SCAN_TITLE = 'Apple TV Scan' +NOTIFICATION_AUTH_ID = "apple_tv_auth_notification" +NOTIFICATION_AUTH_TITLE = "Apple TV Authentication" +NOTIFICATION_SCAN_ID = "apple_tv_scan_notification" +NOTIFICATION_SCAN_TITLE = "Apple TV Scan" -T = TypeVar('T') # pylint: disable=invalid-name +T = TypeVar("T") # pylint: disable=invalid-name # This version of ensure_list interprets an empty dict as no value @@ -48,22 +48,30 @@ def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]: return value if isinstance(value, list) else [value] -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(ensure_list, [vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_LOGIN_ID): cv.string, - vol.Optional(CONF_CREDENTIALS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_START_OFF, default=False): cv.boolean, - })]) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_LOGIN_ID): cv.string, + vol.Optional(CONF_CREDENTIALS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_START_OFF, default=False): cv.boolean, + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) # Currently no attributes but it might change later APPLE_TV_SCAN_SCHEMA = vol.Schema({}) -APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, -}) +APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) def request_configuration(hass, config, atv, credentials): @@ -73,54 +81,62 @@ def request_configuration(hass, config, atv, credentials): async def configuration_callback(callback_data): """Handle the submitted configuration.""" from pyatv import exceptions - pin = callback_data.get('pin') + + pin = callback_data.get("pin") try: await atv.airplay.finish_authentication(pin) hass.components.persistent_notification.async_create( - 'Authentication succeeded!

Add the following ' - 'to credentials: in your apple_tv configuration:

' - '{0}'.format(credentials), + "Authentication succeeded!

Add the following " + "to credentials: in your apple_tv configuration:

" + "{0}".format(credentials), title=NOTIFICATION_AUTH_TITLE, - notification_id=NOTIFICATION_AUTH_ID) + notification_id=NOTIFICATION_AUTH_ID, + ) except exceptions.DeviceAuthenticationError as ex: hass.components.persistent_notification.async_create( - 'Authentication failed! Did you enter correct PIN?

' - 'Details: {0}'.format(ex), + "Authentication failed! Did you enter correct PIN?

" + "Details: {0}".format(ex), title=NOTIFICATION_AUTH_TITLE, - notification_id=NOTIFICATION_AUTH_ID) + notification_id=NOTIFICATION_AUTH_ID, + ) hass.async_add_job(configurator.request_done, instance) instance = configurator.request_config( - 'Apple TV Authentication', configuration_callback, - description='Please enter PIN code shown on screen.', - submit_caption='Confirm', - fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}] + "Apple TV Authentication", + configuration_callback, + description="Please enter PIN code shown on screen.", + submit_caption="Confirm", + fields=[{"id": "pin", "name": "PIN Code", "type": "password"}], ) async def scan_for_apple_tvs(hass): """Scan for devices and present a notification of the ones found.""" import pyatv + atvs = await pyatv.scan_for_apple_tvs(hass.loop, timeout=3) devices = [] for atv in atvs: login_id = atv.login_id if login_id is None: - login_id = 'Home Sharing disabled' - devices.append('Name: {0}
Host: {1}
Login ID: {2}'.format( - atv.name, atv.address, login_id)) + login_id = "Home Sharing disabled" + devices.append( + "Name: {0}
Host: {1}
Login ID: {2}".format( + atv.name, atv.address, login_id + ) + ) if not devices: - devices = ['No device(s) found'] + devices = ["No device(s) found"] hass.components.persistent_notification.async_create( - 'The following devices were found:

' + - '

'.join(devices), + "The following devices were found:

" + "

".join(devices), title=NOTIFICATION_SCAN_TITLE, - notification_id=NOTIFICATION_SCAN_ID) + notification_id=NOTIFICATION_SCAN_ID, + ) async def async_setup(hass, config): @@ -137,8 +153,11 @@ async def async_setup(hass, config): return if entity_ids: - devices = [device for device in hass.data[DATA_ENTITIES] - if device.entity_id in entity_ids] + devices = [ + device + for device in hass.data[DATA_ENTITIES] + if device.entity_id in entity_ids + ] else: devices = hass.data[DATA_ENTITIES] @@ -149,19 +168,22 @@ async def async_setup(hass, config): atv = device.atv credentials = await atv.airplay.generate_credentials() await atv.airplay.load_credentials(credentials) - _LOGGER.debug('Generated new credentials: %s', credentials) + _LOGGER.debug("Generated new credentials: %s", credentials) await atv.airplay.start_authentication() - hass.async_add_job(request_configuration, - hass, config, atv, credentials) + hass.async_add_job(request_configuration, hass, config, atv, credentials) async def atv_discovered(service, info): """Set up an Apple TV that was auto discovered.""" - await _setup_atv(hass, config, { - CONF_NAME: info['name'], - CONF_HOST: info['host'], - CONF_LOGIN_ID: info['properties']['hG'], - CONF_START_OFF: False - }) + await _setup_atv( + hass, + config, + { + CONF_NAME: info["name"], + CONF_HOST: info["host"], + CONF_LOGIN_ID: info["properties"]["hG"], + CONF_START_OFF: False, + }, + ) discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered) @@ -170,12 +192,15 @@ async def async_setup(hass, config): await asyncio.wait(tasks) hass.services.async_register( - DOMAIN, SERVICE_SCAN, async_service_handler, - schema=APPLE_TV_SCAN_SCHEMA) + DOMAIN, SERVICE_SCAN, async_service_handler, schema=APPLE_TV_SCAN_SCHEMA + ) hass.services.async_register( - DOMAIN, SERVICE_AUTHENTICATE, async_service_handler, - schema=APPLE_TV_AUTHENTICATE_SCHEMA) + DOMAIN, + SERVICE_AUTHENTICATE, + async_service_handler, + schema=APPLE_TV_AUTHENTICATE_SCHEMA, + ) return True @@ -183,6 +208,7 @@ async def async_setup(hass, config): async def _setup_atv(hass, hass_config, atv_config): """Set up an Apple TV.""" import pyatv + name = atv_config.get(CONF_NAME) host = atv_config.get(CONF_HOST) login_id = atv_config.get(CONF_LOGIN_ID) @@ -199,16 +225,17 @@ async def _setup_atv(hass, hass_config, atv_config): await atv.airplay.load_credentials(credentials) power = AppleTVPowerManager(hass, atv, start_off) - hass.data[DATA_APPLE_TV][host] = { - ATTR_ATV: atv, - ATTR_POWER: power - } + hass.data[DATA_APPLE_TV][host] = {ATTR_ATV: atv, ATTR_POWER: power} - hass.async_create_task(discovery.async_load_platform( - hass, 'media_player', DOMAIN, atv_config, hass_config)) + hass.async_create_task( + discovery.async_load_platform( + hass, "media_player", DOMAIN, atv_config, hass_config + ) + ) - hass.async_create_task(discovery.async_load_platform( - hass, 'remote', DOMAIN, atv_config, hass_config)) + hass.async_create_task( + discovery.async_load_platform(hass, "remote", DOMAIN, atv_config, hass_config) + ) class AppleTVPowerManager: diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 9698ef4c704..8ecaeab424c 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -3,12 +3,29 @@ import logging from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_TVSHOW, + MEDIA_TYPE_VIDEO, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, - STATE_PAUSED, STATE_PLAYING, STATE_STANDBY) + CONF_HOST, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, + STATE_STANDBY, +) from homeassistant.core import callback import homeassistant.util.dt as dt_util @@ -16,13 +33,20 @@ from . import ATTR_ATV, ATTR_POWER, DATA_APPLE_TV, DATA_ENTITIES _LOGGER = logging.getLogger(__name__) -SUPPORT_APPLE_TV = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \ - SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_SEEK | \ - SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK +SUPPORT_APPLE_TV = ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PLAY_MEDIA + | SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_SEEK + | SUPPORT_STOP + | SUPPORT_NEXT_TRACK + | SUPPORT_PREVIOUS_TRACK +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Apple TV platform.""" if not discovery_info: return @@ -89,15 +113,21 @@ class AppleTvDevice(MediaPlayerDevice): if self._playing: from pyatv import const + state = self._playing.play_state - if state in (const.PLAY_STATE_IDLE, const.PLAY_STATE_NO_MEDIA, - const.PLAY_STATE_LOADING): + if state in ( + const.PLAY_STATE_IDLE, + const.PLAY_STATE_NO_MEDIA, + const.PLAY_STATE_LOADING, + ): return STATE_IDLE if state == const.PLAY_STATE_PLAYING: return STATE_PLAYING - if state in (const.PLAY_STATE_PAUSED, - const.PLAY_STATE_FAST_FORWARD, - const.PLAY_STATE_FAST_BACKWARD): + if state in ( + const.PLAY_STATE_PAUSED, + const.PLAY_STATE_FAST_FORWARD, + const.PLAY_STATE_FAST_BACKWARD, + ): # Catch fast forward/backward here so "play" is default action return STATE_PAUSED return STATE_STANDBY # Bad or unknown state? @@ -111,8 +141,7 @@ class AppleTvDevice(MediaPlayerDevice): @callback def playstatus_error(self, updater, exception): """Inform about an error and restart push updates.""" - _LOGGER.warning('A %s error occurred: %s', - exception.__class__, exception) + _LOGGER.warning("A %s error occurred: %s", exception.__class__, exception) # This will wait 10 seconds before restarting push updates. If the # connection continues to fail, it will flood the log (every 10 @@ -127,6 +156,7 @@ class AppleTvDevice(MediaPlayerDevice): """Content type of current playing media.""" if self._playing: from pyatv import const + media_type = self._playing.media_type if media_type == const.MEDIA_TYPE_VIDEO: return MEDIA_TYPE_VIDEO @@ -169,7 +199,7 @@ class AppleTvDevice(MediaPlayerDevice): """Fetch media image of current playing image.""" state = self.state if self._playing and state not in [STATE_OFF, STATE_IDLE]: - return (await self.atv.metadata.artwork()), 'image/png' + return (await self.atv.metadata.artwork()), "image/png" return None, None @@ -178,11 +208,11 @@ class AppleTvDevice(MediaPlayerDevice): """Title of current playing media.""" if self._playing: if self.state == STATE_IDLE: - return 'Nothing playing' + return "Nothing playing" title = self._playing.title - return title if title else 'No title' + return title if title else "No title" - return 'Establishing a connection to {0}...'.format(self._name) + return "Establishing a connection to {0}...".format(self._name) @property def supported_features(self): diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index 2839e3a5324..1229b756e72 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -5,8 +5,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME from . import ATTR_ATV, ATTR_POWER, DATA_APPLE_TV -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Apple TV remote platform.""" if not discovery_info: return diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index 3bde7021d7c..c5ae8ed8414 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -7,60 +7,61 @@ import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - CONF_HOST, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP) + ATTR_GPS_ACCURACY, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_HOST, + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify -DOMAIN = 'aprs' +DOMAIN = "aprs" _LOGGER = logging.getLogger(__name__) -ATTR_ALTITUDE = 'altitude' -ATTR_COURSE = 'course' -ATTR_COMMENT = 'comment' -ATTR_FROM = 'from' -ATTR_FORMAT = 'format' -ATTR_POS_AMBIGUITY = 'posambiguity' -ATTR_SPEED = 'speed' +ATTR_ALTITUDE = "altitude" +ATTR_COURSE = "course" +ATTR_COMMENT = "comment" +ATTR_FROM = "from" +ATTR_FORMAT = "format" +ATTR_POS_AMBIGUITY = "posambiguity" +ATTR_SPEED = "speed" -CONF_CALLSIGNS = 'callsigns' +CONF_CALLSIGNS = "callsigns" -DEFAULT_HOST = 'rotate.aprs2.net' -DEFAULT_PASSWORD = '-1' +DEFAULT_HOST = "rotate.aprs2.net" +DEFAULT_PASSWORD = "-1" DEFAULT_TIMEOUT = 30.0 FILTER_PORT = 14580 -MSG_FORMATS = ['compressed', 'uncompressed', 'mic-e'] +MSG_FORMATS = ["compressed", "uncompressed", "mic-e"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_CALLSIGNS): cv.ensure_list, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, - default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_HOST, - default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_TIMEOUT, - default=DEFAULT_TIMEOUT): vol.Coerce(float), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_CALLSIGNS): cv.ensure_list, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(float), + } +) def make_filter(callsigns: list) -> str: """Make a server-side filter from a list of callsigns.""" - return ' '.join('b/{0}'.format(cs.upper()) for cs in callsigns) + return " ".join("b/{0}".format(cs.upper()) for cs in callsigns) def gps_accuracy(gps, posambiguity: int) -> int: """Calculate the GPS accuracy based on APRS posambiguity.""" import geopy.distance - pos_a_map = {0: 0, - 1: 1 / 600, - 2: 1 / 60, - 3: 1 / 6, - 4: 1} + pos_a_map = {0: 0, 1: 1 / 600, 2: 1 / 60, 3: 1 / 6, 4: 1} if posambiguity in pos_a_map: degrees = pos_a_map[posambiguity] @@ -69,8 +70,7 @@ def gps_accuracy(gps, posambiguity: int) -> int: accuracy = round(dist_m) else: - message = "APRS position ambiguity must be 0-4, not '{0}'.".format( - posambiguity) + message = "APRS position ambiguity must be 0-4, not '{0}'.".format(posambiguity) raise ValueError(message) return accuracy @@ -85,8 +85,7 @@ def setup_scanner(hass, config, see, discovery_info=None): password = config.get(CONF_PASSWORD) host = config.get(CONF_HOST) timeout = config.get(CONF_TIMEOUT) - aprs_listener = AprsListenerThread( - callsign, password, host, server_filter, see) + aprs_listener = AprsListenerThread(callsign, password, host, server_filter, see) def aprs_disconnect(event): """Stop the APRS connection.""" @@ -110,8 +109,9 @@ def setup_scanner(hass, config, see, discovery_info=None): class AprsListenerThread(threading.Thread): """APRS message listener.""" - def __init__(self, callsign: str, password: str, host: str, - server_filter: str, see): + def __init__( + self, callsign: str, password: str, host: str, server_filter: str, see + ): """Initialize the class.""" super().__init__() @@ -126,7 +126,8 @@ class AprsListenerThread(threading.Thread): self.start_success = False self.ais = aprslib.IS( - self.callsign, passwd=password, host=self.host, port=FILTER_PORT) + self.callsign, passwd=password, host=self.host, port=FILTER_PORT + ) def start_complete(self, success: bool, message: str): """Complete startup process.""" @@ -141,19 +142,21 @@ class AprsListenerThread(threading.Thread): from aprslib import LoginError try: - _LOGGER.info("Opening connection to %s with callsign %s.", - self.host, self.callsign) + _LOGGER.info( + "Opening connection to %s with callsign %s.", self.host, self.callsign + ) self.ais.connect() self.start_complete( True, - "Connected to {0} with callsign {1}.".format( - self.host, self.callsign)) + "Connected to {0} with callsign {1}.".format(self.host, self.callsign), + ) self.ais.consumer(callback=self.rx_msg, immortal=True) except (AprsConnectionError, LoginError) as err: self.start_complete(False, str(err)) except OSError: - _LOGGER.info("Closing connection to %s with callsign %s.", - self.host, self.callsign) + _LOGGER.info( + "Closing connection to %s with callsign %s.", self.host, self.callsign + ) def stop(self): """Close the connection to the APRS network.""" @@ -171,16 +174,12 @@ class AprsListenerThread(threading.Thread): if ATTR_POS_AMBIGUITY in msg: pos_amb = msg[ATTR_POS_AMBIGUITY] try: - attrs[ATTR_GPS_ACCURACY] = gps_accuracy((lat, lon), - pos_amb) + attrs[ATTR_GPS_ACCURACY] = gps_accuracy((lat, lon), pos_amb) except ValueError: _LOGGER.warning( - "APRS message contained invalid posambiguity: %s", - str(pos_amb)) - for attr in [ATTR_ALTITUDE, - ATTR_COMMENT, - ATTR_COURSE, - ATTR_SPEED]: + "APRS message contained invalid posambiguity: %s", str(pos_amb) + ) + for attr in [ATTR_ALTITUDE, ATTR_COMMENT, ATTR_COURSE, ATTR_SPEED]: if attr in msg: attrs[attr] = msg[attr] diff --git a/homeassistant/components/aqualogic/__init__.py b/homeassistant/components/aqualogic/__init__.py index 65718463218..cabe00b6c6d 100644 --- a/homeassistant/components/aqualogic/__init__.py +++ b/homeassistant/components/aqualogic/__init__.py @@ -6,24 +6,29 @@ import threading import voluptuous as vol -from homeassistant.const import (CONF_HOST, CONF_PORT, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) -DOMAIN = 'aqualogic' -UPDATE_TOPIC = DOMAIN + '_update' -CONF_UNIT = 'unit' +DOMAIN = "aqualogic" +UPDATE_TOPIC = DOMAIN + "_update" +CONF_UNIT = "unit" RECONNECT_INTERVAL = timedelta(seconds=10) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port} + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 454cdbd7f6b..1cc06fc446f 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -4,8 +4,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, TEMP_FAHRENHEIT) +from homeassistant.const import CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -15,33 +14,35 @@ from . import DOMAIN, UPDATE_TOPIC _LOGGER = logging.getLogger(__name__) TEMP_UNITS = [TEMP_CELSIUS, TEMP_FAHRENHEIT] -PERCENT_UNITS = ['%', '%'] -SALT_UNITS = ['g/L', 'PPM'] -WATT_UNITS = ['W', 'W'] +PERCENT_UNITS = ["%", "%"] +SALT_UNITS = ["g/L", "PPM"] +WATT_UNITS = ["W", "W"] NO_UNITS = [None, None] # sensor_type [ description, unit, icon ] # sensor_type corresponds to property names in aqualogic.core.AquaLogic SENSOR_TYPES = { - 'air_temp': ['Air Temperature', TEMP_UNITS, 'mdi:thermometer'], - 'pool_temp': ['Pool Temperature', TEMP_UNITS, 'mdi:oil-temperature'], - 'spa_temp': ['Spa Temperature', TEMP_UNITS, 'mdi:oil-temperature'], - 'pool_chlorinator': ['Pool Chlorinator', PERCENT_UNITS, 'mdi:gauge'], - 'spa_chlorinator': ['Spa Chlorinator', PERCENT_UNITS, 'mdi:gauge'], - 'salt_level': ['Salt Level', SALT_UNITS, 'mdi:gauge'], - 'pump_speed': ['Pump Speed', PERCENT_UNITS, 'mdi:speedometer'], - 'pump_power': ['Pump Power', WATT_UNITS, 'mdi:gauge'], - 'status': ['Status', NO_UNITS, 'mdi:alert'] + "air_temp": ["Air Temperature", TEMP_UNITS, "mdi:thermometer"], + "pool_temp": ["Pool Temperature", TEMP_UNITS, "mdi:oil-temperature"], + "spa_temp": ["Spa Temperature", TEMP_UNITS, "mdi:oil-temperature"], + "pool_chlorinator": ["Pool Chlorinator", PERCENT_UNITS, "mdi:gauge"], + "spa_chlorinator": ["Spa Chlorinator", PERCENT_UNITS, "mdi:gauge"], + "salt_level": ["Salt Level", SALT_UNITS, "mdi:gauge"], + "pump_speed": ["Pump Speed", PERCENT_UNITS, "mdi:speedometer"], + "pump_power": ["Pump Power", WATT_UNITS, "mdi:gauge"], + "status": ["Status", NO_UNITS, "mdi:alert"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ) + } +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the sensor platform.""" sensors = [] @@ -94,7 +95,8 @@ class AquaLogicSensor(Entity): async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_update_callback) + UPDATE_TOPIC, self.async_update_callback + ) @callback def async_update_callback(self): diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index b8bd8e41244..b5a7a409647 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -13,26 +13,28 @@ from . import DOMAIN, UPDATE_TOPIC _LOGGER = logging.getLogger(__name__) SWITCH_TYPES = { - 'lights': 'Lights', - 'filter': 'Filter', - 'filter_low_speed': 'Filter Low Speed', - 'aux_1': 'Aux 1', - 'aux_2': 'Aux 2', - 'aux_3': 'Aux 3', - 'aux_4': 'Aux 4', - 'aux_5': 'Aux 5', - 'aux_6': 'Aux 6', - 'aux_7': 'Aux 7', + "lights": "Lights", + "filter": "Filter", + "filter_low_speed": "Filter Low Speed", + "aux_1": "Aux 1", + "aux_2": "Aux 2", + "aux_3": "Aux 3", + "aux_4": "Aux 4", + "aux_5": "Aux 5", + "aux_6": "Aux 6", + "aux_7": "Aux 7", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCH_TYPES)): - vol.All(cv.ensure_list, [vol.In(SWITCH_TYPES)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCH_TYPES)): vol.All( + cv.ensure_list, [vol.In(SWITCH_TYPES)] + ) + } +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the switch platform.""" switches = [] @@ -49,19 +51,20 @@ class AquaLogicSwitch(SwitchDevice): def __init__(self, processor, switch_type): """Initialize switch.""" from aqualogic.core import States + self._processor = processor self._type = switch_type self._state_name = { - 'lights': States.LIGHTS, - 'filter': States.FILTER, - 'filter_low_speed': States.FILTER_LOW_SPEED, - 'aux_1': States.AUX_1, - 'aux_2': States.AUX_2, - 'aux_3': States.AUX_3, - 'aux_4': States.AUX_4, - 'aux_5': States.AUX_5, - 'aux_6': States.AUX_6, - 'aux_7': States.AUX_7 + "lights": States.LIGHTS, + "filter": States.FILTER, + "filter_low_speed": States.FILTER_LOW_SPEED, + "aux_1": States.AUX_1, + "aux_2": States.AUX_2, + "aux_3": States.AUX_3, + "aux_4": States.AUX_4, + "aux_5": States.AUX_5, + "aux_6": States.AUX_6, + "aux_7": States.AUX_7, }[switch_type] @property @@ -100,7 +103,8 @@ class AquaLogicSwitch(SwitchDevice): async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_update_callback) + UPDATE_TOPIC, self.async_update_callback + ) @callback def async_update_callback(self): diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index a4e88f02a59..016db478fc9 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -3,52 +3,76 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP) + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT, - CONF_USERNAME, STATE_OFF, STATE_ON) + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_TIMEOUT, + CONF_USERNAME, + STATE_OFF, + STATE_ON, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Sharp Aquos TV' +DEFAULT_NAME = "Sharp Aquos TV" DEFAULT_PORT = 10002 -DEFAULT_USERNAME = 'admin' -DEFAULT_PASSWORD = 'password' +DEFAULT_USERNAME = "admin" +DEFAULT_PASSWORD = "password" DEFAULT_TIMEOUT = 0.5 DEFAULT_RETRIES = 2 -SUPPORT_SHARPTV = SUPPORT_TURN_OFF | \ - SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_SELECT_SOURCE | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \ - SUPPORT_VOLUME_SET | SUPPORT_PLAY +SUPPORT_SHARPTV = ( + SUPPORT_TURN_OFF + | SUPPORT_NEXT_TRACK + | SUPPORT_PAUSE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_SELECT_SOURCE + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_SET + | SUPPORT_PLAY +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.string, - vol.Optional('retries', default=DEFAULT_RETRIES): cv.string, - vol.Optional('power_on_enabled', default=False): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.string, + vol.Optional("retries", default=DEFAULT_RETRIES): cv.string, + vol.Optional("power_on_enabled", default=False): cv.boolean, + } +) -SOURCES = {0: 'TV / Antenna', - 1: 'HDMI_IN_1', - 2: 'HDMI_IN_2', - 3: 'HDMI_IN_3', - 4: 'HDMI_IN_4', - 5: 'COMPONENT IN', - 6: 'VIDEO_IN_1', - 7: 'VIDEO_IN_2', - 8: 'PC_IN'} +SOURCES = { + 0: "TV / Antenna", + 1: "HDMI_IN_1", + 2: "HDMI_IN_2", + 3: "HDMI_IN_3", + 4: "HDMI_IN_4", + 5: "COMPONENT IN", + 6: "VIDEO_IN_1", + 7: "VIDEO_IN_2", + 8: "PC_IN", +} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -59,11 +83,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = config.get(CONF_PORT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - power_on_enabled = config.get('power_on_enabled') + power_on_enabled = config.get("power_on_enabled") if discovery_info: - _LOGGER.debug('%s', discovery_info) - vals = discovery_info.split(':') + _LOGGER.debug("%s", discovery_info) + vals = discovery_info.split(":") if len(vals) > 1: port = vals[1] @@ -81,6 +105,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def _retry(func): """Handle query retries.""" + def wrapper(obj, *args, **kwargs): """Wrap all query functions.""" update_retries = 5 @@ -92,6 +117,7 @@ def _retry(func): update_retries -= 1 if update_retries == 0: obj.set_state(STATE_OFF) + return wrapper diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 0fffa2bbb5c..bdb3bf67bbe 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -44,10 +44,8 @@ def _zone_name_validator(config): for zone, zone_config in config[CONF_ZONE].items(): if CONF_NAME not in zone_config: zone_config[CONF_NAME] = "{} ({}:{}) - {}".format( - DEFAULT_NAME, - config[CONF_HOST], - config[CONF_PORT], - zone) + DEFAULT_NAME, config[CONF_HOST], config[CONF_PORT], zone + ) return config @@ -59,16 +57,19 @@ ZONE_SCHEMA = vol.Schema( ) DEVICE_SCHEMA = vol.Schema( - vol.All({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.positive_int, - vol.Optional( - CONF_ZONE, default={1: _optional_zone(None)} - ): {vol.In([1, 2]): _optional_zone}, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.positive_int, - }, _zone_name_validator) + vol.All( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.positive_int, + vol.Optional(CONF_ZONE, default={1: _optional_zone(None)}): { + vol.In([1, 2]): _optional_zone + }, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.positive_int, + }, + _zone_name_validator, + ) ) CONFIG_SCHEMA = vol.Schema( @@ -82,37 +83,27 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): hass.data[DOMAIN_DATA_CONFIG] = {} for device in config[DOMAIN]: - hass.data[DOMAIN_DATA_CONFIG][ - (device[CONF_HOST], device[CONF_PORT]) - ] = device + hass.data[DOMAIN_DATA_CONFIG][(device[CONF_HOST], device[CONF_PORT])] = device hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOST: device[CONF_HOST], - CONF_PORT: device[CONF_PORT], - }, + data={CONF_HOST: device[CONF_HOST], CONF_PORT: device[CONF_PORT]}, ) ) return True -async def async_setup_entry( - hass: HomeAssistantType, entry: config_entries.ConfigEntry -): +async def async_setup_entry(hass: HomeAssistantType, entry: config_entries.ConfigEntry): """Set up an access point from a config entry.""" client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT]) config = hass.data[DOMAIN_DATA_CONFIG].get( (entry.data[CONF_HOST], entry.data[CONF_PORT]), DEVICE_SCHEMA( - { - CONF_HOST: entry.data[CONF_HOST], - CONF_PORT: entry.data[CONF_PORT], - } + {CONF_HOST: entry.data[CONF_HOST], CONF_PORT: entry.data[CONF_PORT]} ), ) @@ -121,9 +112,7 @@ async def async_setup_entry( "config": config, } - asyncio.ensure_future( - _run_client(hass, client, config[CONF_SCAN_INTERVAL]) - ) + asyncio.ensure_future(_run_client(hass, client, config[CONF_SCAN_INTERVAL])) hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "media_player") @@ -145,9 +134,7 @@ async def _run_client(hass, client, interval): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop) def _listen(_): - hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_CLIENT_DATA, client.host - ) + hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_CLIENT_DATA, client.host) while run: try: diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index b22f40a641d..971abc3e26d 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -2,12 +2,7 @@ import logging from typing import Optional -from arcam.fmj import ( - DecodeMode2CH, - DecodeModeMCH, - IncomingAudioFormat, - SourceCodes, -) +from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes from arcam.fmj.state import State from homeassistant import config_entries @@ -45,9 +40,9 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, - config_entry: config_entries.ConfigEntry, - async_add_entities, + hass: HomeAssistantType, + config_entry: config_entries.ConfigEntry, + async_add_entities, ): """Set up the configuration entry.""" data = hass.data[DOMAIN_DATA_ENTRIES][config_entry.entry_id] @@ -91,20 +86,14 @@ class ArcamFmj(MediaPlayerDevice): audio_format, _ = self._state.get_incoming_audio_format() return bool( audio_format - in ( - IncomingAudioFormat.PCM, - IncomingAudioFormat.ANALOGUE_DIRECT, - None, - ) + in (IncomingAudioFormat.PCM, IncomingAudioFormat.ANALOGUE_DIRECT, None) ) @property def device_info(self): """Return a device description for device registry.""" return { - "identifiers": { - (DOMAIN, self._state.client.host, self._state.client.port) - }, + "identifiers": {(DOMAIN, self._state.client.host, self._state.client.port)}, "model": "FMJ", "manufacturer": "Arcam", } @@ -135,7 +124,7 @@ class ArcamFmj(MediaPlayerDevice): return support async def async_added_to_hass(self): - """Once registed add listener for events.""" + """Once registered, add listener for events.""" await self._state.start() @callback @@ -153,9 +142,7 @@ class ArcamFmj(MediaPlayerDevice): if host == self._state.client.host: self.async_schedule_update_ha_state(force_refresh=True) - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_CLIENT_DATA, _data - ) + self.hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_CLIENT_DATA, _data) self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_CLIENT_STARTED, _started @@ -190,13 +177,9 @@ class ArcamFmj(MediaPlayerDevice): """Select a specific source.""" try: if self._get_2ch(): - await self._state.set_decode_mode_2ch( - DecodeMode2CH[sound_mode] - ) + await self._state.set_decode_mode_2ch(DecodeMode2CH[sound_mode]) else: - await self._state.set_decode_mode_mch( - DecodeModeMCH[sound_mode] - ) + await self._state.set_decode_mode_mch(DecodeModeMCH[sound_mode]) except KeyError: _LOGGER.error("Unsupported sound_mode %s", sound_mode) return diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py index a6841e07564..4dcde93e749 100644 --- a/homeassistant/components/arduino/__init__.py +++ b/homeassistant/components/arduino/__init__.py @@ -3,8 +3,7 @@ import logging import voluptuous as vol -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_PORT import homeassistant.helpers.config_validation as cv @@ -12,13 +11,11 @@ _LOGGER = logging.getLogger(__name__) BOARD = None -DOMAIN = 'arduino' +DOMAIN = "arduino" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_PORT): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA +) def setup(hass, config): @@ -39,8 +36,10 @@ def setup(hass, config): _LOGGER.error("The StandardFirmata sketch should be 2.2 or newer") return False except IndexError: - _LOGGER.warning("The version of the StandardFirmata sketch was not" - "detected. This may lead to side effects") + _LOGGER.warning( + "The version of the StandardFirmata sketch was not" + "detected. This may lead to side effects" + ) def stop_arduino(event): """Stop the Arduino service.""" @@ -61,26 +60,22 @@ class ArduinoBoard: def __init__(self, port): """Initialize the board.""" from PyMata.pymata import PyMata + self._port = port self._board = PyMata(self._port, verbose=False) def set_mode(self, pin, direction, mode): """Set the mode and the direction of a given pin.""" - if mode == 'analog' and direction == 'in': - self._board.set_pin_mode( - pin, self._board.INPUT, self._board.ANALOG) - elif mode == 'analog' and direction == 'out': - self._board.set_pin_mode( - pin, self._board.OUTPUT, self._board.ANALOG) - elif mode == 'digital' and direction == 'in': - self._board.set_pin_mode( - pin, self._board.INPUT, self._board.DIGITAL) - elif mode == 'digital' and direction == 'out': - self._board.set_pin_mode( - pin, self._board.OUTPUT, self._board.DIGITAL) - elif mode == 'pwm': - self._board.set_pin_mode( - pin, self._board.OUTPUT, self._board.PWM) + if mode == "analog" and direction == "in": + self._board.set_pin_mode(pin, self._board.INPUT, self._board.ANALOG) + elif mode == "analog" and direction == "out": + self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.ANALOG) + elif mode == "digital" and direction == "in": + self._board.set_pin_mode(pin, self._board.INPUT, self._board.DIGITAL) + elif mode == "digital" and direction == "out": + self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.DIGITAL) + elif mode == "pwm": + self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.PWM) def get_analog_inputs(self): """Get the values from the pins.""" diff --git a/homeassistant/components/arduino/sensor.py b/homeassistant/components/arduino/sensor.py index 0cc6e006b89..a92432537ca 100644 --- a/homeassistant/components/arduino/sensor.py +++ b/homeassistant/components/arduino/sensor.py @@ -11,17 +11,14 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_PINS = 'pins' -CONF_TYPE = 'analog' +CONF_PINS = "pins" +CONF_TYPE = "analog" -PIN_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, -}) +PIN_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PINS): - vol.Schema({cv.positive_int: PIN_SCHEMA}), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_PINS): vol.Schema({cv.positive_int: PIN_SCHEMA})} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -46,7 +43,7 @@ class ArduinoSensor(Entity): self._pin = pin self._name = name self.pin_type = pin_type - self.direction = 'in' + self.direction = "in" self._value = None arduino.BOARD.set_mode(self._pin, self.direction, self.pin_type) diff --git a/homeassistant/components/arduino/switch.py b/homeassistant/components/arduino/switch.py index 92e91196a9a..63d83c8575e 100644 --- a/homeassistant/components/arduino/switch.py +++ b/homeassistant/components/arduino/switch.py @@ -4,27 +4,28 @@ import logging import voluptuous as vol from homeassistant.components import arduino -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_PINS = 'pins' -CONF_TYPE = 'digital' -CONF_NEGATE = 'negate' -CONF_INITIAL = 'initial' +CONF_PINS = "pins" +CONF_TYPE = "digital" +CONF_NEGATE = "negate" +CONF_INITIAL = "initial" -PIN_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_INITIAL, default=False): cv.boolean, - vol.Optional(CONF_NEGATE, default=False): cv.boolean, -}) +PIN_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL, default=False): cv.boolean, + vol.Optional(CONF_NEGATE, default=False): cv.boolean, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PINS, default={}): - vol.Schema({cv.positive_int: PIN_SCHEMA}), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_PINS, default={}): vol.Schema({cv.positive_int: PIN_SCHEMA})} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -50,7 +51,7 @@ class ArduinoSwitch(SwitchDevice): self._pin = pin self._name = options.get(CONF_NAME) self.pin_type = CONF_TYPE - self.direction = 'out' + self.direction = "out" self._state = options.get(CONF_INITIAL) diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 3fd669a2bba..96ffa371864 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -6,9 +6,11 @@ import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA) -from homeassistant.const import ( - CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS) + BinarySensorDevice, + PLATFORM_SCHEMA, + DEVICE_CLASSES_SCHEMA, +) +from homeassistant.const import CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -16,12 +18,14 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_RESOURCE): cv.url, - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_PIN): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_RESOURCE): cv.url, + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_PIN): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -33,8 +37,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: response = requests.get(resource, timeout=10).json() except requests.exceptions.MissingSchema: - _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// to your URL") + _LOGGER.error( + "Missing resource or schema in configuration. " "Add http:// to your URL" + ) return False except requests.exceptions.ConnectionError: _LOGGER.error("No route to device at %s", resource) @@ -42,9 +47,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): arest = ArestData(resource, pin) - add_entities([ArestBinarySensor( - arest, resource, config.get(CONF_NAME, response[CONF_NAME]), - device_class, pin)], True) + add_entities( + [ + ArestBinarySensor( + arest, + resource, + config.get(CONF_NAME, response[CONF_NAME]), + device_class, + pin, + ) + ], + True, + ) class ArestBinarySensor(BinarySensorDevice): @@ -60,7 +74,8 @@ class ArestBinarySensor(BinarySensorDevice): if self._pin is not None: request = requests.get( - '{}/mode/{}/i'.format(self._resource, self._pin), timeout=10) + "{}/mode/{}/i".format(self._resource, self._pin), timeout=10 + ) if request.status_code != 200: _LOGGER.error("Can't set mode of %s", self._resource) @@ -72,7 +87,7 @@ class ArestBinarySensor(BinarySensorDevice): @property def is_on(self): """Return true if the binary sensor is on.""" - return bool(self.arest.data.get('state')) + return bool(self.arest.data.get("state")) @property def device_class(self): @@ -97,8 +112,9 @@ class ArestData: def update(self): """Get the latest data from aREST device.""" try: - response = requests.get('{}/digital/{}'.format( - self._resource, self._pin), timeout=10) - self.data = {'state': response.json()['return_value']} + response = requests.get( + "{}/digital/{}".format(self._resource, self._pin), timeout=10 + ) + self.data = {"state": response.json()["return_value"]} except requests.exceptions.ConnectionError: _LOGGER.error("No route to device '%s'", self._resource) diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index fc443cd60b6..533adeccb5e 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -7,8 +7,12 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_RESOURCE, - CONF_MONITORED_VARIABLES, CONF_NAME) + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, + CONF_RESOURCE, + CONF_MONITORED_VARIABLES, + CONF_NAME, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -18,25 +22,31 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) -CONF_FUNCTIONS = 'functions' -CONF_PINS = 'pins' +CONF_FUNCTIONS = "functions" +CONF_PINS = "pins" -DEFAULT_NAME = 'aREST sensor' +DEFAULT_NAME = "aREST sensor" -PIN_VARIABLE_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, -}) +PIN_VARIABLE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_RESOURCE): cv.url, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PINS, default={}): - vol.Schema({cv.string: PIN_VARIABLE_SCHEMA}), - vol.Optional(CONF_MONITORED_VARIABLES, default={}): - vol.Schema({cv.string: PIN_VARIABLE_SCHEMA}), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_RESOURCE): cv.url, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PINS, default={}): vol.Schema( + {cv.string: PIN_VARIABLE_SCHEMA} + ), + vol.Optional(CONF_MONITORED_VARIABLES, default={}): vol.Schema( + {cv.string: PIN_VARIABLE_SCHEMA} + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -48,8 +58,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: response = requests.get(resource, timeout=10).json() except requests.exceptions.MissingSchema: - _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// to your URL") + _LOGGER.error( + "Missing resource or schema in configuration. " "Add http:// to your URL" + ) return False except requests.exceptions.ConnectionError: _LOGGER.error("No route to device at %s", resource) @@ -66,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def _render(value): try: - return value_template.async_render({'value': value}) + return value_template.async_render({"value": value}) except TemplateError: _LOGGER.exception("Error parsing value") return value @@ -77,25 +88,37 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if var_conf is not None: for variable, var_data in var_conf.items(): - if variable not in response['variables']: + if variable not in response["variables"]: _LOGGER.error("Variable: %s does not exist", variable) continue renderer = make_renderer(var_data.get(CONF_VALUE_TEMPLATE)) - dev.append(ArestSensor( - arest, resource, config.get(CONF_NAME, response[CONF_NAME]), - var_data.get(CONF_NAME, variable), variable=variable, - unit_of_measurement=var_data.get(CONF_UNIT_OF_MEASUREMENT), - renderer=renderer)) + dev.append( + ArestSensor( + arest, + resource, + config.get(CONF_NAME, response[CONF_NAME]), + var_data.get(CONF_NAME, variable), + variable=variable, + unit_of_measurement=var_data.get(CONF_UNIT_OF_MEASUREMENT), + renderer=renderer, + ) + ) if pins is not None: for pinnum, pin in pins.items(): renderer = make_renderer(pin.get(CONF_VALUE_TEMPLATE)) - dev.append(ArestSensor( - ArestData(resource, pinnum), resource, - config.get(CONF_NAME, response[CONF_NAME]), pin.get(CONF_NAME), - pin=pinnum, unit_of_measurement=pin.get( - CONF_UNIT_OF_MEASUREMENT), renderer=renderer)) + dev.append( + ArestSensor( + ArestData(resource, pinnum), + resource, + config.get(CONF_NAME, response[CONF_NAME]), + pin.get(CONF_NAME), + pin=pinnum, + unit_of_measurement=pin.get(CONF_UNIT_OF_MEASUREMENT), + renderer=renderer, + ) + ) add_entities(dev, True) @@ -103,12 +126,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ArestSensor(Entity): """Implementation of an aREST sensor for exposed variables.""" - def __init__(self, arest, resource, location, name, variable=None, - pin=None, unit_of_measurement=None, renderer=None): + def __init__( + self, + arest, + resource, + location, + name, + variable=None, + pin=None, + unit_of_measurement=None, + renderer=None, + ): """Initialize the sensor.""" self.arest = arest self._resource = resource - self._name = '{} {}'.format(location.title(), name.title()) + self._name = "{} {}".format(location.title(), name.title()) self._variable = variable self._pin = pin self._state = None @@ -117,7 +149,8 @@ class ArestSensor(Entity): if self._pin is not None: request = requests.get( - '{}/mode/{}/i'.format(self._resource, self._pin), timeout=10) + "{}/mode/{}/i".format(self._resource, self._pin), timeout=10 + ) if request.status_code != 200: _LOGGER.error("Can't set mode of %s", self._resource) @@ -136,11 +169,10 @@ class ArestSensor(Entity): """Return the state of the sensor.""" values = self.arest.data - if 'error' in values: - return values['error'] + if "error" in values: + return values["error"] - value = self._renderer( - values.get('value', values.get(self._variable, None))) + value = self._renderer(values.get("value", values.get(self._variable, None))) return value def update(self): @@ -169,17 +201,20 @@ class ArestData: try: if self._pin is None: response = requests.get(self._resource, timeout=10) - self.data = response.json()['variables'] + self.data = response.json()["variables"] else: try: - if str(self._pin[0]) == 'A': - response = requests.get('{}/analog/{}'.format( - self._resource, self._pin[1:]), timeout=10) - self.data = {'value': response.json()['return_value']} + if str(self._pin[0]) == "A": + response = requests.get( + "{}/analog/{}".format(self._resource, self._pin[1:]), + timeout=10, + ) + self.data = {"value": response.json()["return_value"]} except TypeError: - response = requests.get('{}/digital/{}'.format( - self._resource, self._pin), timeout=10) - self.data = {'value': response.json()['return_value']} + response = requests.get( + "{}/digital/{}".format(self._resource, self._pin), timeout=10 + ) + self.data = {"value": response.json()["return_value"]} self.available = True except requests.exceptions.ConnectionError: _LOGGER.error("No route to device %s", self._resource) diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index 717acc2f336..558df89100e 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -5,31 +5,37 @@ import logging import requests import voluptuous as vol -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME, CONF_RESOURCE) +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_RESOURCE import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_FUNCTIONS = 'functions' -CONF_PINS = 'pins' -CONF_INVERT = 'invert' +CONF_FUNCTIONS = "functions" +CONF_PINS = "pins" +CONF_INVERT = "invert" -DEFAULT_NAME = 'aREST switch' +DEFAULT_NAME = "aREST switch" -PIN_FUNCTION_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_INVERT, default=False): cv.boolean, -}) +PIN_FUNCTION_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INVERT, default=False): cv.boolean, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_RESOURCE): cv.url, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PINS, default={}): - vol.Schema({cv.string: PIN_FUNCTION_SCHEMA}), - vol.Optional(CONF_FUNCTIONS, default={}): - vol.Schema({cv.string: PIN_FUNCTION_SCHEMA}), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_RESOURCE): cv.url, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PINS, default={}): vol.Schema( + {cv.string: PIN_FUNCTION_SCHEMA} + ), + vol.Optional(CONF_FUNCTIONS, default={}): vol.Schema( + {cv.string: PIN_FUNCTION_SCHEMA} + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -39,8 +45,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: response = requests.get(resource, timeout=10) except requests.exceptions.MissingSchema: - _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// to your URL") + _LOGGER.error( + "Missing resource or schema in configuration. " "Add http:// to your URL" + ) return False except requests.exceptions.ConnectionError: _LOGGER.error("No route to device at %s", resource) @@ -49,15 +56,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] pins = config.get(CONF_PINS) for pinnum, pin in pins.items(): - dev.append(ArestSwitchPin( - resource, config.get(CONF_NAME, response.json()[CONF_NAME]), - pin.get(CONF_NAME), pinnum, pin.get(CONF_INVERT))) + dev.append( + ArestSwitchPin( + resource, + config.get(CONF_NAME, response.json()[CONF_NAME]), + pin.get(CONF_NAME), + pinnum, + pin.get(CONF_INVERT), + ) + ) functions = config.get(CONF_FUNCTIONS) for funcname, func in functions.items(): - dev.append(ArestSwitchFunction( - resource, config.get(CONF_NAME, response.json()[CONF_NAME]), - func.get(CONF_NAME), funcname)) + dev.append( + ArestSwitchFunction( + resource, + config.get(CONF_NAME, response.json()[CONF_NAME]), + func.get(CONF_NAME), + funcname, + ) + ) add_entities(dev) @@ -68,7 +86,7 @@ class ArestSwitchBase(SwitchDevice): def __init__(self, resource, location, name): """Initialize the switch.""" self._resource = resource - self._name = '{} {}'.format(location.title(), name.title()) + self._name = "{} {}".format(location.title(), name.title()) self._state = None self._available = True @@ -96,15 +114,14 @@ class ArestSwitchFunction(ArestSwitchBase): super().__init__(resource, location, name) self._func = func - request = requests.get( - '{}/{}'.format(self._resource, self._func), timeout=10) + request = requests.get("{}/{}".format(self._resource, self._func), timeout=10) if request.status_code != 200: _LOGGER.error("Can't find function") return try: - request.json()['return_value'] + request.json()["return_value"] except KeyError: _LOGGER.error("No return_value received") except ValueError: @@ -113,33 +130,38 @@ class ArestSwitchFunction(ArestSwitchBase): def turn_on(self, **kwargs): """Turn the device on.""" request = requests.get( - '{}/{}'.format(self._resource, self._func), timeout=10, - params={'params': '1'}) + "{}/{}".format(self._resource, self._func), + timeout=10, + params={"params": "1"}, + ) if request.status_code == 200: self._state = True else: - _LOGGER.error( - "Can't turn on function %s at %s", self._func, self._resource) + _LOGGER.error("Can't turn on function %s at %s", self._func, self._resource) def turn_off(self, **kwargs): """Turn the device off.""" request = requests.get( - '{}/{}'.format(self._resource, self._func), timeout=10, - params={'params': '0'}) + "{}/{}".format(self._resource, self._func), + timeout=10, + params={"params": "0"}, + ) if request.status_code == 200: self._state = False else: _LOGGER.error( - "Can't turn off function %s at %s", self._func, self._resource) + "Can't turn off function %s at %s", self._func, self._resource + ) def update(self): """Get the latest data from aREST API and update the state.""" try: request = requests.get( - '{}/{}'.format(self._resource, self._func), timeout=10) - self._state = request.json()['return_value'] != 0 + "{}/{}".format(self._resource, self._func), timeout=10 + ) + self._state = request.json()["return_value"] != 0 self._available = True except requests.exceptions.ConnectionError: _LOGGER.warning("No route to device %s", self._resource) @@ -156,7 +178,8 @@ class ArestSwitchPin(ArestSwitchBase): self.invert = invert request = requests.get( - '{}/mode/{}/o'.format(self._resource, self._pin), timeout=10) + "{}/mode/{}/o".format(self._resource, self._pin), timeout=10 + ) if request.status_code != 200: _LOGGER.error("Can't set mode") self._available = False @@ -165,35 +188,34 @@ class ArestSwitchPin(ArestSwitchBase): """Turn the device on.""" turn_on_payload = int(not self.invert) request = requests.get( - '{}/digital/{}/{}'.format(self._resource, self._pin, - turn_on_payload), - timeout=10) + "{}/digital/{}/{}".format(self._resource, self._pin, turn_on_payload), + timeout=10, + ) if request.status_code == 200: self._state = True else: - _LOGGER.error( - "Can't turn on pin %s at %s", self._pin, self._resource) + _LOGGER.error("Can't turn on pin %s at %s", self._pin, self._resource) def turn_off(self, **kwargs): """Turn the device off.""" turn_off_payload = int(self.invert) request = requests.get( - '{}/digital/{}/{}'.format(self._resource, self._pin, - turn_off_payload), - timeout=10) + "{}/digital/{}/{}".format(self._resource, self._pin, turn_off_payload), + timeout=10, + ) if request.status_code == 200: self._state = False else: - _LOGGER.error( - "Can't turn off pin %s at %s", self._pin, self._resource) + _LOGGER.error("Can't turn off pin %s at %s", self._pin, self._resource) def update(self): """Get the latest data from aREST API and update the state.""" try: request = requests.get( - '{}/digital/{}'.format(self._resource, self._pin), timeout=10) + "{}/digital/{}".format(self._resource, self._pin), timeout=10 + ) status_value = int(self.invert) - self._state = request.json()['return_value'] != status_value + self._state = request.json()["return_value"] != status_value self._available = True except requests.exceptions.ConnectionError: _LOGGER.warning("No route to device %s", self._resource) diff --git a/homeassistant/components/arlo/__init__.py b/homeassistant/components/arlo/__init__.py index 38230c2f05f..80fa37b6787 100644 --- a/homeassistant/components/arlo/__init__.py +++ b/homeassistant/components/arlo/__init__.py @@ -6,8 +6,7 @@ import voluptuous as vol from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.helpers import config_validation as cv -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL) +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send @@ -15,25 +14,29 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Data provided by arlo.netgear.com" -DATA_ARLO = 'data_arlo' -DEFAULT_BRAND = 'Netgear Arlo' -DOMAIN = 'arlo' +DATA_ARLO = "data_arlo" +DEFAULT_BRAND = "Netgear Arlo" +DOMAIN = "arlo" -NOTIFICATION_ID = 'arlo_notification' -NOTIFICATION_TITLE = 'Arlo Component Setup' +NOTIFICATION_ID = "arlo_notification" +NOTIFICATION_TITLE = "Arlo Component Setup" SCAN_INTERVAL = timedelta(seconds=60) SIGNAL_UPDATE_ARLO = "arlo_update" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -51,8 +54,7 @@ def setup(hass, config): return False # assign refresh period to base station thread - arlo_base_station = next(( - station for station in arlo.base_stations), None) + arlo_base_station = next((station for station in arlo.base_stations), None) if arlo_base_station is not None: arlo_base_station.refresh_rate = scan_interval.total_seconds() @@ -65,22 +67,22 @@ def setup(hass, config): except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), + "Error: {}
" + "You will need to restart hass after fixing." + "".format(ex), title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) + notification_id=NOTIFICATION_ID, + ) return False def hub_refresh(event_time): """Call ArloHub to refresh information.""" _LOGGER.debug("Updating Arlo Hub component") - hass.data[DATA_ARLO].update(update_cameras=True, - update_base_station=True) + hass.data[DATA_ARLO].update(update_cameras=True, update_base_station=True) dispatcher_send(hass, SIGNAL_UPDATE_ARLO) # register service - hass.services.register(DOMAIN, 'update', hub_refresh) + hass.services.register(DOMAIN, "update", hub_refresh) # register scan interval for ArloHub track_time_interval(hass, hub_refresh, scan_interval) diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index a7addfb86ea..a56b2a63372 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -4,10 +4,16 @@ import logging import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA, AlarmControlPanel) + PLATFORM_SCHEMA, + AlarmControlPanel, +) from homeassistant.const import ( - ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED) + ATTR_ATTRIBUTION, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -16,21 +22,23 @@ from . import ATTRIBUTION, DATA_ARLO, SIGNAL_UPDATE_ARLO _LOGGER = logging.getLogger(__name__) -ARMED = 'armed' +ARMED = "armed" -CONF_HOME_MODE_NAME = 'home_mode_name' -CONF_AWAY_MODE_NAME = 'away_mode_name' -CONF_NIGHT_MODE_NAME = 'night_mode_name' +CONF_HOME_MODE_NAME = "home_mode_name" +CONF_AWAY_MODE_NAME = "away_mode_name" +CONF_NIGHT_MODE_NAME = "night_mode_name" -DISARMED = 'disarmed' +DISARMED = "disarmed" -ICON = 'mdi:security' +ICON = "mdi:security" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string, - vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string, - vol.Optional(CONF_NIGHT_MODE_NAME, default=ARMED): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string, + vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string, + vol.Optional(CONF_NIGHT_MODE_NAME, default=ARMED): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -45,8 +53,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): night_mode_name = config.get(CONF_NIGHT_MODE_NAME) base_stations = [] for base_station in arlo.base_stations: - base_stations.append(ArloBaseStation(base_station, home_mode_name, - away_mode_name, night_mode_name)) + base_stations.append( + ArloBaseStation( + base_station, home_mode_name, away_mode_name, night_mode_name + ) + ) add_entities(base_stations, True) @@ -68,8 +79,7 @@ class ArloBaseStation(AlarmControlPanel): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) @callback def _update_callback(self): @@ -116,7 +126,7 @@ class ArloBaseStation(AlarmControlPanel): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - 'device_id': self._base_station.device_id + "device_id": self._base_station.device_id, } def _get_state_from_mode(self, mode): diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index 166e0781044..a05dc40a9ef 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -15,30 +15,26 @@ from . import DATA_ARLO, DEFAULT_BRAND, SIGNAL_UPDATE_ARLO _LOGGER = logging.getLogger(__name__) -ARLO_MODE_ARMED = 'armed' -ARLO_MODE_DISARMED = 'disarmed' +ARLO_MODE_ARMED = "armed" +ARLO_MODE_DISARMED = "disarmed" -ATTR_BRIGHTNESS = 'brightness' -ATTR_FLIPPED = 'flipped' -ATTR_MIRRORED = 'mirrored' -ATTR_MOTION = 'motion_detection_sensitivity' -ATTR_POWERSAVE = 'power_save_mode' -ATTR_SIGNAL_STRENGTH = 'signal_strength' -ATTR_UNSEEN_VIDEOS = 'unseen_videos' -ATTR_LAST_REFRESH = 'last_refresh' +ATTR_BRIGHTNESS = "brightness" +ATTR_FLIPPED = "flipped" +ATTR_MIRRORED = "mirrored" +ATTR_MOTION = "motion_detection_sensitivity" +ATTR_POWERSAVE = "power_save_mode" +ATTR_SIGNAL_STRENGTH = "signal_strength" +ATTR_UNSEEN_VIDEOS = "unseen_videos" +ATTR_LAST_REFRESH = "last_refresh" -CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' -DEFAULT_ARGUMENTS = '-pred 1' +CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" +DEFAULT_ARGUMENTS = "-pred 1" -POWERSAVE_MODE_MAPPING = { - 1: 'best_battery_life', - 2: 'optimized', - 3: 'best_video' -} +POWERSAVE_MODE_MAPPING = {1: "best_battery_life", 2: "optimized", 3: "best_video"} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -72,8 +68,7 @@ class ArloCam(Camera): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) @callback def _update_callback(self): @@ -83,23 +78,26 @@ class ArloCam(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" from haffmpeg.camera import CameraMjpeg + video = self._camera.last_video if not video: - error_msg = \ - 'Video not found for {0}. Is it older than {1} days?'.format( - self.name, self._camera.min_days_vdo_cache) + error_msg = "Video not found for {0}. Is it older than {1} days?".format( + self.name, self._camera.min_days_vdo_cache + ) _LOGGER.error(error_msg) return stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - await stream.open_camera( - video.video_url, extra_cmd=self._ffmpeg_arguments) + await stream.open_camera(video.video_url, extra_cmd=self._ffmpeg_arguments) try: stream_reader = await stream.get_reader() return await async_aiohttp_proxy_stream( - self.hass, request, stream_reader, - self._ffmpeg.ffmpeg_stream_content_type) + self.hass, + request, + stream_reader, + self._ffmpeg.ffmpeg_stream_content_type, + ) finally: await stream.close() @@ -112,17 +110,21 @@ class ArloCam(Camera): def device_state_attributes(self): """Return the state attributes.""" return { - name: value for name, value in ( + name: value + for name, value in ( (ATTR_BATTERY_LEVEL, self._camera.battery_level), (ATTR_BRIGHTNESS, self._camera.brightness), (ATTR_FLIPPED, self._camera.flip_state), (ATTR_MIRRORED, self._camera.mirror_state), (ATTR_MOTION, self._camera.motion_detection_sensitivity), - (ATTR_POWERSAVE, POWERSAVE_MODE_MAPPING.get( - self._camera.powersave_mode)), + ( + ATTR_POWERSAVE, + POWERSAVE_MODE_MAPPING.get(self._camera.powersave_mode), + ), (ATTR_SIGNAL_STRENGTH, self._camera.signal_strength), (ATTR_UNSEEN_VIDEOS, self._camera.unseen_videos), - ) if value is not None + ) + if value is not None } @property diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index f83caec386b..aadd5a48d37 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -5,8 +5,12 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) + ATTR_ATTRIBUTION, + CONF_MONITORED_CONDITIONS, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -19,20 +23,23 @@ _LOGGER = logging.getLogger(__name__) # sensor_type [ description, unit, icon ] SENSOR_TYPES = { - 'last_capture': ['Last', None, 'run-fast'], - 'total_cameras': ['Arlo Cameras', None, 'video'], - 'captured_today': ['Captured Today', None, 'file-video'], - 'battery_level': ['Battery Level', '%', 'battery-50'], - 'signal_strength': ['Signal Strength', None, 'signal'], - 'temperature': ['Temperature', TEMP_CELSIUS, 'thermometer'], - 'humidity': ['Humidity', '%', 'water-percent'], - 'air_quality': ['Air Quality', 'ppm', 'biohazard'] + "last_capture": ["Last", None, "run-fast"], + "total_cameras": ["Arlo Cameras", None, "video"], + "captured_today": ["Captured Today", None, "file-video"], + "battery_level": ["Battery Level", "%", "battery-50"], + "signal_strength": ["Signal Strength", None, "signal"], + "temperature": ["Temperature", TEMP_CELSIUS, "thermometer"], + "humidity": ["Humidity", "%", "water-percent"], + "air_quality": ["Air Quality", "ppm", "biohazard"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ) + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -43,23 +50,24 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - if sensor_type == 'total_cameras': - sensors.append(ArloSensor( - SENSOR_TYPES[sensor_type][0], arlo, sensor_type)) + if sensor_type == "total_cameras": + sensors.append(ArloSensor(SENSOR_TYPES[sensor_type][0], arlo, sensor_type)) else: for camera in arlo.cameras: - if sensor_type in ('temperature', 'humidity', 'air_quality'): + if sensor_type in ("temperature", "humidity", "air_quality"): continue - name = '{0} {1}'.format( - SENSOR_TYPES[sensor_type][0], camera.name) + name = "{0} {1}".format(SENSOR_TYPES[sensor_type][0], camera.name) sensors.append(ArloSensor(name, camera, sensor_type)) for base_station in arlo.base_stations: - if sensor_type in ('temperature', 'humidity', 'air_quality') \ - and base_station.model_id == 'ABC1000': - name = '{0} {1}'.format( - SENSOR_TYPES[sensor_type][0], base_station.name) + if ( + sensor_type in ("temperature", "humidity", "air_quality") + and base_station.model_id == "ABC1000" + ): + name = "{0} {1}".format( + SENSOR_TYPES[sensor_type][0], base_station.name + ) sensors.append(ArloSensor(name, base_station, sensor_type)) add_entities(sensors, True) @@ -70,12 +78,12 @@ class ArloSensor(Entity): def __init__(self, name, device, sensor_type): """Initialize an Arlo sensor.""" - _LOGGER.debug('ArloSensor created for %s', name) + _LOGGER.debug("ArloSensor created for %s", name) self._name = name self._data = device self._sensor_type = sensor_type self._state = None - self._icon = 'mdi:{}'.format(SENSOR_TYPES.get(self._sensor_type)[2]) + self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[2]) @property def name(self): @@ -84,8 +92,7 @@ class ArloSensor(Entity): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) @callback def _update_callback(self): @@ -100,9 +107,10 @@ class ArloSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - if self._sensor_type == 'battery_level' and self._state is not None: - return icon_for_battery_level(battery_level=int(self._state), - charging=False) + if self._sensor_type == "battery_level" and self._state is not None: + return icon_for_battery_level( + battery_level=int(self._state), charging=False + ) return self._icon @property @@ -113,57 +121,57 @@ class ArloSensor(Entity): @property def device_class(self): """Return the device class of the sensor.""" - if self._sensor_type == 'temperature': + if self._sensor_type == "temperature": return DEVICE_CLASS_TEMPERATURE - if self._sensor_type == 'humidity': + if self._sensor_type == "humidity": return DEVICE_CLASS_HUMIDITY return None def update(self): """Get the latest data and updates the state.""" _LOGGER.debug("Updating Arlo sensor %s", self.name) - if self._sensor_type == 'total_cameras': + if self._sensor_type == "total_cameras": self._state = len(self._data.cameras) - elif self._sensor_type == 'captured_today': + elif self._sensor_type == "captured_today": self._state = len(self._data.captured_today) - elif self._sensor_type == 'last_capture': + elif self._sensor_type == "last_capture": try: video = self._data.last_video self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S") except (AttributeError, IndexError): - error_msg = \ - 'Video not found for {0}. Older than {1} days?'.format( - self.name, self._data.min_days_vdo_cache) + error_msg = "Video not found for {0}. Older than {1} days?".format( + self.name, self._data.min_days_vdo_cache + ) _LOGGER.debug(error_msg) self._state = None - elif self._sensor_type == 'battery_level': + elif self._sensor_type == "battery_level": try: self._state = self._data.battery_level except TypeError: self._state = None - elif self._sensor_type == 'signal_strength': + elif self._sensor_type == "signal_strength": try: self._state = self._data.signal_strength except TypeError: self._state = None - elif self._sensor_type == 'temperature': + elif self._sensor_type == "temperature": try: self._state = self._data.ambient_temperature except TypeError: self._state = None - elif self._sensor_type == 'humidity': + elif self._sensor_type == "humidity": try: self._state = self._data.ambient_humidity except TypeError: self._state = None - elif self._sensor_type == 'air_quality': + elif self._sensor_type == "air_quality": try: self._state = self._data.ambient_air_quality except TypeError: @@ -175,9 +183,9 @@ class ArloSensor(Entity): attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs['brand'] = DEFAULT_BRAND + attrs["brand"] = DEFAULT_BRAND - if self._sensor_type != 'total_cameras': - attrs['model'] = self._data.model_id + if self._sensor_type != "total_cameras": + attrs["model"] = self._data.model_id return attrs diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index cde144e68f6..f93533b6beb 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -6,21 +6,27 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME _LOGGER = logging.getLogger(__name__) _DEVICES_REGEX = re.compile( - r'(?P([^\s]+)?)\s+' + - r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' + - r'(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))\s+') + r"(?P([^\s]+)?)\s+" + + r"(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+" + + r"(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))\s+" +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + } +) def get_scanner(hass, config): @@ -48,15 +54,15 @@ class ArubaDeviceScanner(DeviceScanner): def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" self._update_info() - return [client['mac'] for client in self.last_results] + return [client["mac"] for client in self.last_results] def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" if not self.last_results: return None for client in self.last_results: - if client['mac'] == device: - return client['name'] + if client["mac"] == device: + return client["name"] return None def _update_info(self): @@ -77,13 +83,21 @@ class ArubaDeviceScanner(DeviceScanner): def get_aruba_data(self): """Retrieve data from Aruba Access Point and return parsed result.""" import pexpect - connect = 'ssh {}@{}' + + connect = "ssh {}@{}" ssh = pexpect.spawn(connect.format(self.username, self.host)) - query = ssh.expect(['password:', pexpect.TIMEOUT, pexpect.EOF, - 'continue connecting (yes/no)?', - 'Host key verification failed.', - 'Connection refused', - 'Connection timed out'], timeout=120) + query = ssh.expect( + [ + "password:", + pexpect.TIMEOUT, + pexpect.EOF, + "continue connecting (yes/no)?", + "Host key verification failed.", + "Connection refused", + "Connection timed out", + ], + timeout=120, + ) if query == 1: _LOGGER.error("Timeout") return @@ -91,8 +105,8 @@ class ArubaDeviceScanner(DeviceScanner): _LOGGER.error("Unexpected response from router") return if query == 3: - ssh.sendline('yes') - ssh.expect('password:') + ssh.sendline("yes") + ssh.expect("password:") elif query == 4: _LOGGER.error("Host key changed") return @@ -103,19 +117,19 @@ class ArubaDeviceScanner(DeviceScanner): _LOGGER.error("Connection timed out") return ssh.sendline(self.password) - ssh.expect('#') - ssh.sendline('show clients') - ssh.expect('#') - devices_result = ssh.before.split(b'\r\n') - ssh.sendline('exit') + ssh.expect("#") + ssh.sendline("show clients") + ssh.expect("#") + devices_result = ssh.before.split(b"\r\n") + ssh.sendline("exit") devices = {} for device in devices_result: - match = _DEVICES_REGEX.search(device.decode('utf-8')) + match = _DEVICES_REGEX.search(device.decode("utf-8")) if match: - devices[match.group('ip')] = { - 'ip': match.group('ip'), - 'mac': match.group('mac').upper(), - 'name': match.group('name') + devices[match.group("ip")] = { + "ip": match.group("ip"), + "mac": match.group("mac").upper(), + "name": match.group("name"), } return devices diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 94b552c6eba..23cd811a3e0 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -10,10 +10,10 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -DOMAIN = 'arwn' +DOMAIN = "arwn" -DATA_ARWN = 'arwn' -TOPIC = 'arwn/#' +DATA_ARWN = "arwn" +TOPIC = "arwn/#" def discover_sensors(topic, payload): @@ -21,39 +21,41 @@ def discover_sensors(topic, payload): Async friendly. """ - parts = topic.split('/') - unit = payload.get('units', '') + parts = topic.split("/") + unit = payload.get("units", "") domain = parts[1] - if domain == 'temperature': + if domain == "temperature": name = parts[2] - if unit == 'F': + if unit == "F": unit = TEMP_FAHRENHEIT else: unit = TEMP_CELSIUS - return ArwnSensor(name, 'temp', unit) + return ArwnSensor(name, "temp", unit) if domain == "moisture": name = parts[2] + " Moisture" - return ArwnSensor(name, 'moisture', unit, "mdi:water-percent") + return ArwnSensor(name, "moisture", unit, "mdi:water-percent") if domain == "rain": if len(parts) >= 3 and parts[2] == "today": - return ArwnSensor("Rain Since Midnight", 'since_midnight', - "in", "mdi:water") - if domain == 'barometer': - return ArwnSensor('Barometer', 'pressure', unit, - "mdi:thermometer-lines") - if domain == 'wind': - return (ArwnSensor('Wind Speed', 'speed', unit, "mdi:speedometer"), - ArwnSensor('Wind Gust', 'gust', unit, "mdi:speedometer"), - ArwnSensor('Wind Direction', 'direction', '°', "mdi:compass")) + return ArwnSensor( + "Rain Since Midnight", "since_midnight", "in", "mdi:water" + ) + if domain == "barometer": + return ArwnSensor("Barometer", "pressure", unit, "mdi:thermometer-lines") + if domain == "wind": + return ( + ArwnSensor("Wind Speed", "speed", unit, "mdi:speedometer"), + ArwnSensor("Wind Gust", "gust", unit, "mdi:speedometer"), + ArwnSensor("Wind Direction", "direction", "°", "mdi:compass"), + ) def _slug(name): - return 'sensor.arwn_{}'.format(slugify(name)) + return "sensor.arwn_{}".format(slugify(name)) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the ARWN platform.""" + @callback def async_sensor_event_received(msg): """Process events as sensors. @@ -78,24 +80,25 @@ async def async_setup_platform(hass, config, async_add_entities, store = hass.data[DATA_ARWN] = {} if isinstance(sensors, ArwnSensor): - sensors = (sensors, ) + sensors = (sensors,) - if 'timestamp' in event: - del event['timestamp'] + if "timestamp" in event: + del event["timestamp"] for sensor in sensors: if sensor.name not in store: sensor.hass = hass sensor.set_event(event) store[sensor.name] = sensor - _LOGGER.debug("Registering new sensor %(name)s => %(event)s", - dict(name=sensor.name, event=event)) + _LOGGER.debug( + "Registering new sensor %(name)s => %(event)s", + dict(name=sensor.name, event=event), + ) async_add_entities((sensor,), True) else: store[sensor.name].set_event(event) - await mqtt.async_subscribe( - hass, TOPIC, async_sensor_event_received, 0) + await mqtt.async_subscribe(hass, TOPIC, async_sensor_event_received, 0) return True diff --git a/homeassistant/components/asterisk_cdr/mailbox.py b/homeassistant/components/asterisk_cdr/mailbox.py index 647067b60d4..4146ca9ddf9 100644 --- a/homeassistant/components/asterisk_cdr/mailbox.py +++ b/homeassistant/components/asterisk_cdr/mailbox.py @@ -11,7 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) -MAILBOX_NAME = 'asterisk_cdr' +MAILBOX_NAME = "asterisk_cdr" async def async_get_handler(hass, config, discovery_info=None): @@ -26,8 +26,7 @@ class AsteriskCDR(Mailbox): """Initialize Asterisk CDR.""" super().__init__(hass, name) self.cdr = [] - async_dispatcher_connect( - self.hass, SIGNAL_CDR_UPDATE, self._update_callback) + async_dispatcher_connect(self.hass, SIGNAL_CDR_UPDATE, self._update_callback) @callback def _update_callback(self, msg): @@ -40,16 +39,18 @@ class AsteriskCDR(Mailbox): cdr = [] for entry in self.hass.data[ASTERISK_DOMAIN].cdr: timestamp = datetime.datetime.strptime( - entry['time'], "%Y-%m-%d %H:%M:%S").timestamp() + entry["time"], "%Y-%m-%d %H:%M:%S" + ).timestamp() info = { - 'origtime': timestamp, - 'callerid': entry['callerid'], - 'duration': entry['duration'], + "origtime": timestamp, + "callerid": entry["callerid"], + "duration": entry["duration"], } - sha = hashlib.sha256(str(entry).encode('utf-8')).hexdigest() + sha = hashlib.sha256(str(entry).encode("utf-8")).hexdigest() msg = "Destination: {}\nApplication: {}\n Context: {}".format( - entry['dest'], entry['application'], entry['context']) - cdr.append({'info': info, 'sha': sha, 'text': msg}) + entry["dest"], entry["application"], entry["context"] + ) + cdr.append({"info": info, "sha": sha, "text": msg}) self.cdr = cdr async def async_get_messages(self): diff --git a/homeassistant/components/asterisk_mbox/__init__.py b/homeassistant/components/asterisk_mbox/__init__.py index a354226bbc0..6c9412d07d8 100644 --- a/homeassistant/components/asterisk_mbox/__init__.py +++ b/homeassistant/components/asterisk_mbox/__init__.py @@ -7,26 +7,30 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, dispatcher_connect) +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_connect _LOGGER = logging.getLogger(__name__) -DOMAIN = 'asterisk_mbox' +DOMAIN = "asterisk_mbox" SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform" -SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request' -SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' -SIGNAL_CDR_UPDATE = 'asterisk_mbox.message_updated' -SIGNAL_CDR_REQUEST = 'asterisk_mbox.message_request' +SIGNAL_MESSAGE_REQUEST = "asterisk_mbox.message_request" +SIGNAL_MESSAGE_UPDATE = "asterisk_mbox.message_updated" +SIGNAL_CDR_UPDATE = "asterisk_mbox.message_updated" +SIGNAL_CDR_REQUEST = "asterisk_mbox.message_request" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_PORT): cv.port, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_PORT): cv.port, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -48,58 +52,63 @@ class AsteriskData: def __init__(self, hass, host, port, password, config): """Init the Asterisk data object.""" from asterisk_mbox import Client as asteriskClient + self.hass = hass self.config = config self.messages = None self.cdr = None - dispatcher_connect( - self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages) - dispatcher_connect( - self.hass, SIGNAL_CDR_REQUEST, self._request_cdr) - dispatcher_connect( - self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform) + dispatcher_connect(self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages) + dispatcher_connect(self.hass, SIGNAL_CDR_REQUEST, self._request_cdr) + dispatcher_connect(self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform) # Only connect after signal connection to ensure we don't miss any self.client = asteriskClient(host, port, password, self.handle_data) @callback def _discover_platform(self, component): _LOGGER.debug("Adding mailbox %s", component) - self.hass.async_create_task(discovery.async_load_platform( - self.hass, "mailbox", component, {}, self.config)) + self.hass.async_create_task( + discovery.async_load_platform( + self.hass, "mailbox", component, {}, self.config + ) + ) @callback def handle_data(self, command, msg): """Handle changes to the mailbox.""" from asterisk_mbox.commands import ( - CMD_MESSAGE_LIST, CMD_MESSAGE_CDR_AVAILABLE, CMD_MESSAGE_CDR) + CMD_MESSAGE_LIST, + CMD_MESSAGE_CDR_AVAILABLE, + CMD_MESSAGE_CDR, + ) if command == CMD_MESSAGE_LIST: - _LOGGER.debug("AsteriskVM sent updated message list: Len %d", - len(msg)) + _LOGGER.debug("AsteriskVM sent updated message list: Len %d", len(msg)) old_messages = self.messages self.messages = sorted( - msg, key=lambda item: item['info']['origtime'], reverse=True) + msg, key=lambda item: item["info"]["origtime"], reverse=True + ) if not isinstance(old_messages, list): - async_dispatcher_send( - self.hass, SIGNAL_DISCOVER_PLATFORM, DOMAIN) - async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE, - self.messages) + async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM, DOMAIN) + async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE, self.messages) elif command == CMD_MESSAGE_CDR: - _LOGGER.debug("AsteriskVM sent updated CDR list: Len %d", - len(msg.get('entries', []))) - self.cdr = msg['entries'] + _LOGGER.debug( + "AsteriskVM sent updated CDR list: Len %d", len(msg.get("entries", [])) + ) + self.cdr = msg["entries"] async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr) elif command == CMD_MESSAGE_CDR_AVAILABLE: if not isinstance(self.cdr, list): _LOGGER.debug("AsteriskVM adding CDR platform") self.cdr = [] - async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM, - "asterisk_cdr") + async_dispatcher_send( + self.hass, SIGNAL_DISCOVER_PLATFORM, "asterisk_cdr" + ) async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST) else: - _LOGGER.debug("AsteriskVM sent unknown message '%d' len: %d", - command, len(msg)) + _LOGGER.debug( + "AsteriskVM sent unknown message '%d' len: %d", command, len(msg) + ) @callback def _request_messages(self): diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py index f79c8922214..4d3c255fd5b 100644 --- a/homeassistant/components/asterisk_mbox/mailbox.py +++ b/homeassistant/components/asterisk_mbox/mailbox.py @@ -1,8 +1,7 @@ """Support for the Asterisk Voicemail interface.""" import logging -from homeassistant.components.mailbox import ( - CONTENT_TYPE_MPEG, Mailbox, StreamError) +from homeassistant.components.mailbox import CONTENT_TYPE_MPEG, Mailbox, StreamError from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -10,8 +9,8 @@ from . import DOMAIN as ASTERISK_DOMAIN _LOGGER = logging.getLogger(__name__) -SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request' -SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' +SIGNAL_MESSAGE_REQUEST = "asterisk_mbox.message_request" +SIGNAL_MESSAGE_UPDATE = "asterisk_mbox.message_updated" async def async_get_handler(hass, config, discovery_info=None): @@ -26,7 +25,8 @@ class AsteriskMailbox(Mailbox): """Initialize Asterisk mailbox.""" super().__init__(hass, name) async_dispatcher_connect( - self.hass, SIGNAL_MESSAGE_UPDATE, self._update_callback) + self.hass, SIGNAL_MESSAGE_UPDATE, self._update_callback + ) @callback def _update_callback(self, msg): @@ -51,6 +51,7 @@ class AsteriskMailbox(Mailbox): async def async_get_media(self, msgid): """Return the media blob for the msgid.""" from asterisk_mbox import ServerError + client = self.hass.data[ASTERISK_DOMAIN].client try: return client.mp3(msgid, sync=True) diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index cc51a15f8e8..e0c6830adfe 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -4,53 +4,69 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_MODE, - CONF_PROTOCOL) + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + CONF_PORT, + CONF_MODE, + CONF_PROTOCOL, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform _LOGGER = logging.getLogger(__name__) -CONF_PUB_KEY = 'pub_key' -CONF_REQUIRE_IP = 'require_ip' -CONF_SENSORS = 'sensors' -CONF_SSH_KEY = 'ssh_key' +CONF_PUB_KEY = "pub_key" +CONF_REQUIRE_IP = "require_ip" +CONF_SENSORS = "sensors" +CONF_SSH_KEY = "ssh_key" DOMAIN = "asuswrt" DATA_ASUSWRT = DOMAIN DEFAULT_SSH_PORT = 22 -SECRET_GROUP = 'Password or SSH Key' -SENSOR_TYPES = ['upload_speed', 'download_speed', 'download', 'upload'] +SECRET_GROUP = "Password or SSH Key" +SENSOR_TYPES = ["upload_speed", "download_speed", "download", "upload"] -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']), - vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']), - vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, - vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, - vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, - vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, - vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile, - vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)]), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PROTOCOL, default="ssh"): vol.In(["ssh", "telnet"]), + vol.Optional(CONF_MODE, default="router"): vol.In(["router", "ap"]), + vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, + vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, + vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, + vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, + vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile, + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): """Set up the asuswrt component.""" from aioasuswrt.asuswrt import AsusWrt + conf = config[DOMAIN] - api = AsusWrt(conf[CONF_HOST], conf.get(CONF_PORT), - conf.get(CONF_PROTOCOL) == 'telnet', - conf[CONF_USERNAME], - conf.get(CONF_PASSWORD, ''), - conf.get('ssh_key', conf.get('pub_key', '')), - conf.get(CONF_MODE), conf.get(CONF_REQUIRE_IP)) + api = AsusWrt( + conf[CONF_HOST], + conf.get(CONF_PORT), + conf.get(CONF_PROTOCOL) == "telnet", + conf[CONF_USERNAME], + conf.get(CONF_PASSWORD, ""), + conf.get("ssh_key", conf.get("pub_key", "")), + conf.get(CONF_MODE), + conf.get(CONF_REQUIRE_IP), + ) await api.connection.async_connect() if not api.is_connected: @@ -59,9 +75,13 @@ async def async_setup(hass, config): hass.data[DATA_ASUSWRT] = api - hass.async_create_task(async_load_platform( - hass, 'sensor', DOMAIN, config[DOMAIN].get(CONF_SENSORS), config)) - hass.async_create_task(async_load_platform( - hass, 'device_tracker', DOMAIN, {}, config)) + hass.async_create_task( + async_load_platform( + hass, "sensor", DOMAIN, config[DOMAIN].get(CONF_SENSORS), config + ) + ) + hass.async_create_task( + async_load_platform(hass, "device_tracker", DOMAIN, {}, config) + ) return True diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index a7b13abbc05..5e3297da8ff 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -47,6 +47,6 @@ class AsusWrtDeviceScanner(DeviceScanner): Return boolean if scanning successful. """ - _LOGGER.debug('Checking Devices') + _LOGGER.debug("Checking Devices") self.last_results = await self.connection.async_get_connected_devices() diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 8ae629bd12d..b5ce8539f44 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -8,8 +8,7 @@ from . import DATA_ASUSWRT _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the asuswrt sensors.""" if discovery_info is None: return @@ -18,13 +17,13 @@ async def async_setup_platform( devices = [] - if 'download' in discovery_info: + if "download" in discovery_info: devices.append(AsuswrtTotalRXSensor(api)) - if 'upload' in discovery_info: + if "upload" in discovery_info: devices.append(AsuswrtTotalTXSensor(api)) - if 'download_speed' in discovery_info: + if "download_speed" in discovery_info: devices.append(AsuswrtRXSensor(api)) - if 'upload_speed' in discovery_info: + if "upload_speed" in discovery_info: devices.append(AsuswrtTXSensor(api)) add_entities(devices) @@ -33,7 +32,7 @@ async def async_setup_platform( class AsuswrtSensor(Entity): """Representation of a asuswrt sensor.""" - _name = 'generic' + _name = "generic" def __init__(self, api): """Initialize the sensor.""" @@ -61,8 +60,8 @@ class AsuswrtSensor(Entity): class AsuswrtRXSensor(AsuswrtSensor): """Representation of a asuswrt download speed sensor.""" - _name = 'Asuswrt Download Speed' - _unit = 'Mbit/s' + _name = "Asuswrt Download Speed" + _unit = "Mbit/s" @property def unit_of_measurement(self): @@ -79,8 +78,8 @@ class AsuswrtRXSensor(AsuswrtSensor): class AsuswrtTXSensor(AsuswrtSensor): """Representation of a asuswrt upload speed sensor.""" - _name = 'Asuswrt Upload Speed' - _unit = 'Mbit/s' + _name = "Asuswrt Upload Speed" + _unit = "Mbit/s" @property def unit_of_measurement(self): @@ -97,8 +96,8 @@ class AsuswrtTXSensor(AsuswrtSensor): class AsuswrtTotalRXSensor(AsuswrtSensor): """Representation of a asuswrt total download sensor.""" - _name = 'Asuswrt Download' - _unit = 'Gigabyte' + _name = "Asuswrt Download" + _unit = "Gigabyte" @property def unit_of_measurement(self): @@ -115,8 +114,8 @@ class AsuswrtTotalRXSensor(AsuswrtSensor): class AsuswrtTotalTXSensor(AsuswrtSensor): """Representation of a asuswrt total upload sensor.""" - _name = 'Asuswrt Upload' - _unit = 'Gigabyte' + _name = "Asuswrt Upload" + _unit = "Gigabyte" @property def unit_of_measurement(self): diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index e18c25706c1..93b5ec6ec78 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -7,7 +7,11 @@ from requests import RequestException import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP) + CONF_PASSWORD, + CONF_USERNAME, + CONF_TIMEOUT, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers import discovery from homeassistant.util import Throttle @@ -19,34 +23,37 @@ DEFAULT_TIMEOUT = 10 ACTIVITY_FETCH_LIMIT = 10 ACTIVITY_INITIAL_FETCH_LIMIT = 20 -CONF_LOGIN_METHOD = 'login_method' -CONF_INSTALL_ID = 'install_id' +CONF_LOGIN_METHOD = "login_method" +CONF_INSTALL_ID = "install_id" -NOTIFICATION_ID = 'august_notification' +NOTIFICATION_ID = "august_notification" NOTIFICATION_TITLE = "August Setup" -AUGUST_CONFIG_FILE = '.august.conf' +AUGUST_CONFIG_FILE = ".august.conf" -DATA_AUGUST = 'august' -DOMAIN = 'august' -DEFAULT_ENTITY_NAMESPACE = 'august' +DATA_AUGUST = "august" +DOMAIN = "august" +DEFAULT_ENTITY_NAMESPACE = "august" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) DEFAULT_SCAN_INTERVAL = timedelta(seconds=5) -LOGIN_METHODS = ['phone', 'email'] +LOGIN_METHODS = ["phone", "email"] -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS), - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_INSTALL_ID): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS), + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_INSTALL_ID): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -AUGUST_COMPONENTS = [ - 'camera', 'binary_sensor', 'lock' -] +AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock"] def request_configuration(hass, config, api, authenticator): @@ -57,12 +64,12 @@ def request_configuration(hass, config, api, authenticator): """Run when the configuration callback is called.""" from august.authenticator import ValidationResult - result = authenticator.validate_verification_code( - data.get('verification_code')) + result = authenticator.validate_verification_code(data.get("verification_code")) if result == ValidationResult.INVALID_VERIFICATION_CODE: - configurator.notify_errors(_CONFIGURING[DOMAIN], - "Invalid verification code") + configurator.notify_errors( + _CONFIGURING[DOMAIN], "Invalid verification code" + ) elif result == ValidationResult.VALIDATED: setup_august(hass, config, api, authenticator) @@ -77,12 +84,11 @@ def request_configuration(hass, config, api, authenticator): NOTIFICATION_TITLE, august_configuration_callback, description="Please check your {} ({}) and enter the verification " - "code below".format(login_method, username), - submit_caption='Verify', - fields=[{ - 'id': 'verification_code', - 'name': "Verification code", - 'type': 'string'}] + "code below".format(login_method, username), + submit_caption="Verify", + fields=[ + {"id": "verification_code", "name": "Verification code", "type": "string"} + ], ) @@ -101,7 +107,8 @@ def setup_august(hass, config, api, authenticator): "You will need to restart hass after fixing." "".format(ex), title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) + notification_id=NOTIFICATION_ID, + ) state = authentication.state @@ -109,8 +116,7 @@ def setup_august(hass, config, api, authenticator): if DOMAIN in _CONFIGURING: hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN)) - hass.data[DATA_AUGUST] = AugustData( - hass, api, authentication.access_token) + hass.data[DATA_AUGUST] = AugustData(hass, api, authentication.access_token) for component in AUGUST_COMPONENTS: discovery.load_platform(hass, component, DOMAIN, {}, config) @@ -147,7 +153,8 @@ def setup(hass, config): conf.get(CONF_USERNAME), conf.get(CONF_PASSWORD), install_id=conf.get(CONF_INSTALL_ID), - access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE)) + access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE), + ) def close_http_session(event): """Close API sessions used to connect to August.""" @@ -219,17 +226,17 @@ class AugustData: """Update data object with latest from August API.""" _LOGGER.debug("Start retrieving device activities") for house_id in self.house_ids: - _LOGGER.debug("Updating device activity for house id %s", - house_id) + _LOGGER.debug("Updating device activity for house id %s", house_id) - activities = self._api.get_house_activities(self._access_token, - house_id, - limit=limit) + activities = self._api.get_house_activities( + self._access_token, house_id, limit=limit + ) device_ids = {a.device_id for a in activities} for device_id in device_ids: - self._activities_by_id[device_id] = [a for a in activities if - a.device_id == device_id] + self._activities_by_id[device_id] = [ + a for a in activities if a.device_id == device_id + ] _LOGGER.debug("Completed retrieving device activities") def get_doorbell_detail(self, doorbell_id): @@ -243,15 +250,17 @@ class AugustData: _LOGGER.debug("Start retrieving doorbell details") for doorbell in self._doorbells: - _LOGGER.debug("Updating doorbell status for %s", - doorbell.device_name) + _LOGGER.debug("Updating doorbell status for %s", doorbell.device_name) try: - detail_by_id[doorbell.device_id] =\ - self._api.get_doorbell_detail( - self._access_token, doorbell.device_id) + detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail( + self._access_token, doorbell.device_id + ) except RequestException as ex: - _LOGGER.error("Request error trying to retrieve doorbell" - " status for %s. %s", doorbell.device_name, ex) + _LOGGER.error( + "Request error trying to retrieve doorbell" " status for %s. %s", + doorbell.device_name, + ex, + ) detail_by_id[doorbell.device_id] = None except Exception: detail_by_id[doorbell.device_id] = None @@ -287,15 +296,18 @@ class AugustData: _LOGGER.debug("Start retrieving door status") for lock in self._locks: - _LOGGER.debug("Updating door status for %s", - lock.device_name) + _LOGGER.debug("Updating door status for %s", lock.device_name) try: state_by_id[lock.device_id] = self._api.get_lock_door_status( - self._access_token, lock.device_id) + self._access_token, lock.device_id + ) except RequestException as ex: - _LOGGER.error("Request error trying to retrieve door" - " status for %s. %s", lock.device_name, ex) + _LOGGER.error( + "Request error trying to retrieve door" " status for %s. %s", + lock.device_name, + ex, + ) state_by_id[lock.device_id] = None except Exception: state_by_id[lock.device_id] = None @@ -311,14 +323,17 @@ class AugustData: _LOGGER.debug("Start retrieving locks status") for lock in self._locks: - _LOGGER.debug("Updating lock status for %s", - lock.device_name) + _LOGGER.debug("Updating lock status for %s", lock.device_name) try: status_by_id[lock.device_id] = self._api.get_lock_status( - self._access_token, lock.device_id) + self._access_token, lock.device_id + ) except RequestException as ex: - _LOGGER.error("Request error trying to retrieve door" - " status for %s. %s", lock.device_name, ex) + _LOGGER.error( + "Request error trying to retrieve door" " status for %s. %s", + lock.device_name, + ex, + ) status_by_id[lock.device_id] = None except Exception: status_by_id[lock.device_id] = None @@ -326,10 +341,14 @@ class AugustData: try: detail_by_id[lock.device_id] = self._api.get_lock_detail( - self._access_token, lock.device_id) + self._access_token, lock.device_id + ) except RequestException as ex: - _LOGGER.error("Request error trying to retrieve door" - " details for %s. %s", lock.device_name, ex) + _LOGGER.error( + "Request error trying to retrieve door" " details for %s. %s", + lock.device_name, + ex, + ) detail_by_id[lock.device_id] = None except Exception: detail_by_id[lock.device_id] = None diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index d1f69645802..d68582d30c5 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -27,21 +27,21 @@ def _retrieve_online_state(data, doorbell): def _retrieve_motion_state(data, doorbell): from august.activity import ActivityType - return _activity_time_based_state(data, doorbell, - [ActivityType.DOORBELL_MOTION, - ActivityType.DOORBELL_DING]) + + return _activity_time_based_state( + data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING] + ) def _retrieve_ding_state(data, doorbell): from august.activity import ActivityType - return _activity_time_based_state(data, doorbell, - [ActivityType.DOORBELL_DING]) + + return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING]) def _activity_time_based_state(data, doorbell, activity_types): """Get the latest state of the sensor.""" - latest = data.get_latest_device_activity(doorbell.device_id, - *activity_types) + latest = data.get_latest_device_activity(doorbell.device_id, *activity_types) if latest is not None: start = latest.activity_start_time @@ -51,14 +51,12 @@ def _activity_time_based_state(data, doorbell, activity_types): # Sensor types: Name, device_class, state_provider -SENSOR_TYPES_DOOR = { - 'door_open': ['Open', 'door', _retrieve_door_state], -} +SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _retrieve_door_state]} SENSOR_TYPES_DOORBELL = { - 'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state], - 'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state], - 'doorbell_online': ['Online', 'connectivity', _retrieve_online_state], + "doorbell_ding": ["Ding", "occupancy", _retrieve_ding_state], + "doorbell_motion": ["Motion", "motion", _retrieve_motion_state], + "doorbell_online": ["Online", "connectivity", _retrieve_online_state], } @@ -68,31 +66,33 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = [] from august.lock import LockDoorStatus + for door in data.locks: for sensor_type in SENSOR_TYPES_DOOR: state_provider = SENSOR_TYPES_DOOR[sensor_type][2] if state_provider(data, door) is LockDoorStatus.UNKNOWN: _LOGGER.debug( "Not adding sensor class %s for lock %s ", - SENSOR_TYPES_DOOR[sensor_type][1], door.device_name + SENSOR_TYPES_DOOR[sensor_type][1], + door.device_name, ) continue _LOGGER.debug( "Adding sensor class %s for %s", - SENSOR_TYPES_DOOR[sensor_type][1], door.device_name + SENSOR_TYPES_DOOR[sensor_type][1], + door.device_name, ) devices.append(AugustDoorBinarySensor(data, sensor_type, door)) for doorbell in data.doorbells: for sensor_type in SENSOR_TYPES_DOORBELL: - _LOGGER.debug("Adding doorbell sensor class %s for %s", - SENSOR_TYPES_DOORBELL[sensor_type][1], - doorbell.device_name) - devices.append( - AugustDoorbellBinarySensor(data, sensor_type, - doorbell) + _LOGGER.debug( + "Adding doorbell sensor class %s for %s", + SENSOR_TYPES_DOORBELL[sensor_type][1], + doorbell.device_name, ) + devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell)) add_entities(devices, True) @@ -126,8 +126,9 @@ class AugustDoorBinarySensor(BinarySensorDevice): @property def name(self): """Return the name of the binary sensor.""" - return "{} {}".format(self._door.device_name, - SENSOR_TYPES_DOOR[self._sensor_type][0]) + return "{} {}".format( + self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][0] + ) def update(self): """Get the latest state of the sensor.""" @@ -136,14 +137,15 @@ class AugustDoorBinarySensor(BinarySensorDevice): self._available = self._state is not None from august.lock import LockDoorStatus + self._state = self._state == LockDoorStatus.OPEN @property def unique_id(self) -> str: """Get the unique of the door open binary sensor.""" - return '{:s}_{:s}'.format(self._door.device_id, - SENSOR_TYPES_DOOR[self._sensor_type][0] - .lower()) + return "{:s}_{:s}".format( + self._door.device_id, SENSOR_TYPES_DOOR[self._sensor_type][0].lower() + ) class AugustDoorbellBinarySensor(BinarySensorDevice): @@ -175,8 +177,9 @@ class AugustDoorbellBinarySensor(BinarySensorDevice): @property def name(self): """Return the name of the binary sensor.""" - return "{} {}".format(self._doorbell.device_name, - SENSOR_TYPES_DOORBELL[self._sensor_type][0]) + return "{} {}".format( + self._doorbell.device_name, SENSOR_TYPES_DOORBELL[self._sensor_type][0] + ) def update(self): """Get the latest state of the sensor.""" @@ -187,6 +190,7 @@ class AugustDoorbellBinarySensor(BinarySensorDevice): @property def unique_id(self) -> str: """Get the unique id of the doorbell sensor.""" - return '{:s}_{:s}'.format(self._doorbell.device_id, - SENSOR_TYPES_DOORBELL[self._sensor_type][0] - .lower()) + return "{:s}_{:s}".format( + self._doorbell.device_id, + SENSOR_TYPES_DOORBELL[self._sensor_type][0].lower(), + ) diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 0bf8a28f904..a8335d1aa52 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -51,12 +51,12 @@ class AugustCamera(Camera): @property def brand(self): """Return the camera brand.""" - return 'August' + return "August" @property def model(self): """Return the camera model.""" - return 'Doorbell' + return "Doorbell" def camera_image(self): """Return bytes of camera image.""" @@ -64,12 +64,13 @@ class AugustCamera(Camera): if self._image_url is not latest.image_url: self._image_url = latest.image_url - self._image_content = requests.get(self._image_url, - timeout=self._timeout).content + self._image_content = requests.get( + self._image_url, timeout=self._timeout + ).content return self._image_content @property def unique_id(self) -> str: """Get the unique id of the camera.""" - return '{:s}_camera'.format(self._doorbell.device_id) + return "{:s}_camera".format(self._doorbell.device_id) diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 5ad2bdc3b5b..e919c47dd4c 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -52,9 +52,10 @@ class AugustLock(LockDevice): self._lock_detail = self._data.get_lock_detail(self._lock.device_id) from august.activity import ActivityType + activity = self._data.get_latest_device_activity( - self._lock.device_id, - ActivityType.LOCK_OPERATION) + self._lock.device_id, ActivityType.LOCK_OPERATION + ) if activity is not None: self._changed_by = activity.operated_by @@ -73,6 +74,7 @@ class AugustLock(LockDevice): def is_locked(self): """Return true if device is on.""" from august.lock import LockStatus + return self._lock_status is LockStatus.LOCKED @property @@ -86,11 +88,9 @@ class AugustLock(LockDevice): if self._lock_detail is None: return None - return { - ATTR_BATTERY_LEVEL: self._lock_detail.battery_level, - } + return {ATTR_BATTERY_LEVEL: self._lock_detail.battery_level} @property def unique_id(self) -> str: """Get the unique id of the lock.""" - return '{:s}_lock'.format(self._lock.device_id) + return "{:s}_lock".format(self._lock.device_id) diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index 58546382a50..0d983f35e37 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -6,20 +6,18 @@ from aiohttp.hdrs import USER_AGENT import requests import voluptuous as vol -from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric " \ - "Administration" -CONF_THRESHOLD = 'forecast_threshold' +ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric " "Administration" +CONF_THRESHOLD = "forecast_threshold" -DEFAULT_DEVICE_CLASS = 'visible' -DEFAULT_NAME = 'Aurora Visibility' +DEFAULT_DEVICE_CLASS = "visible" +DEFAULT_NAME = "Aurora Visibility" DEFAULT_THRESHOLD = 75 HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0" @@ -28,10 +26,12 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -44,12 +44,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): threshold = config.get(CONF_THRESHOLD) try: - aurora_data = AuroraData( - hass.config.latitude, hass.config.longitude, threshold) + aurora_data = AuroraData(hass.config.latitude, hass.config.longitude, threshold) aurora_data.update() except requests.exceptions.HTTPError as error: - _LOGGER.error( - "Connection to aurora forecast service failed: %s", error) + _LOGGER.error("Connection to aurora forecast service failed: %s", error) return False add_entities([AuroraSensor(aurora_data, name)], True) @@ -66,7 +64,7 @@ class AuroraSensor(BinarySensorDevice): @property def name(self): """Return the name of the sensor.""" - return '{}'.format(self._name) + return "{}".format(self._name) @property def is_on(self): @@ -84,8 +82,8 @@ class AuroraSensor(BinarySensorDevice): attrs = {} if self.aurora_data: - attrs['visibility_level'] = self.aurora_data.visibility_level - attrs['message'] = self.aurora_data.is_visible_text + attrs["visibility_level"] = self.aurora_data.visibility_level + attrs["message"] = self.aurora_data.is_visible_text attrs[ATTR_ATTRIBUTION] = ATTRIBUTION return attrs @@ -122,8 +120,7 @@ class AuroraData: self.is_visible_text = "nothing's out" except requests.exceptions.HTTPError as error: - _LOGGER.error( - "Connection to aurora forecast service failed: %s", error) + _LOGGER.error("Connection to aurora forecast service failed: %s", error) return False def get_aurora_forecast(self): @@ -136,9 +133,11 @@ class AuroraData: ] # Convert lat and long for data points in table - converted_latitude = round((self.latitude / 180) - * self.number_of_latitude_intervals) - converted_longitude = round((self.longitude / 360) - * self.number_of_longitude_intervals) + converted_latitude = round( + (self.latitude / 180) * self.number_of_latitude_intervals + ) + converted_longitude = round( + (self.longitude / 360) * self.number_of_longitude_intervals + ) return forecast_table[converted_latitude][converted_longitude] diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index d77fae246d7..456b5080484 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -7,8 +7,12 @@ from aurorapy.client import AuroraSerialClient, AuroraError from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_ADDRESS, CONF_DEVICE, CONF_NAME, DEVICE_CLASS_POWER, - POWER_WATT) + CONF_ADDRESS, + CONF_DEVICE, + CONF_NAME, + DEVICE_CLASS_POWER, + POWER_WATT, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -17,11 +21,13 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_ADDRESS = 2 DEFAULT_NAME = "Solar PV" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICE): cv.string, - vol.Optional(CONF_ADDRESS, default=DEFAULT_ADDRESS): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_DEVICE): cv.string, + vol.Optional(CONF_ADDRESS, default=DEFAULT_ADDRESS): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -32,9 +38,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config[CONF_NAME] _LOGGER.debug("Intitialising com port=%s address=%s", comport, address) - client = AuroraSerialClient(address, comport, parity='N', timeout=1) + client = AuroraSerialClient(address, comport, parity="N", timeout=1) - devices.append(AuroraABBSolarPVMonitorSensor(client, name, 'Power')) + devices.append(AuroraABBSolarPVMonitorSensor(client, name, "Power")) add_entities(devices, True) diff --git a/homeassistant/components/auth/.translations/bg.json b/homeassistant/components/auth/.translations/bg.json index 63cf17f0b22..d07e20a854c 100644 --- a/homeassistant/components/auth/.translations/bg.json +++ b/homeassistant/components/auth/.translations/bg.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043b\u0438\u0447\u043d\u0438 \u0443\u0441\u043b\u0443\u0433\u0438 \u0437\u0430 \u0443\u0432\u0435\u0434\u043e\u043c\u044f\u0432\u0430\u043d\u0435." + }, + "error": { + "invalid_code": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043a\u043e\u0434, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + }, + "step": { + "init": { + "description": "\u041c\u043e\u043b\u044f, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u043d\u0430 \u043e\u0442 \u0443\u0441\u043b\u0443\u0433\u0438\u0442\u0435 \u0437\u0430 \u0443\u0432\u0435\u0434\u043e\u043c\u044f\u0432\u0430\u043d\u0435:", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435 \u043d\u0430 \u0435\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430, \u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0430 \u0447\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0437\u0430 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435" + }, + "setup": { + "description": "\u0415\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430 \u0435 \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u043d\u0430 \u0447\u0440\u0435\u0437 **notify.{notify_service}**. \u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u044f \u043f\u043e-\u0434\u043e\u043b\u0443:", + "title": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430" + } + }, + "title": "\u0423\u0432\u0435\u0434\u043e\u043c\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0435\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430" + }, "totp": { "error": { "invalid_code": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043a\u043e\u0434, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e. \u0410\u043a\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u0442\u0435 \u0442\u0430\u0437\u0438 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e, \u043c\u043e\u043b\u044f, \u0443\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435, \u0447\u0435 \u0447\u0430\u0441\u043e\u0432\u043d\u0438\u043a\u044a\u0442 \u043d\u0430 Home Assistant \u0435 \u0441\u0432\u0435\u0440\u0435\u043d." diff --git a/homeassistant/components/auth/.translations/pt-BR.json b/homeassistant/components/auth/.translations/pt-BR.json index faf854153b0..e08c27a32e6 100644 --- a/homeassistant/components/auth/.translations/pt-BR.json +++ b/homeassistant/components/auth/.translations/pt-BR.json @@ -1,11 +1,23 @@ { "mfa_setup": { "notify": { + "abort": { + "no_available_service": "Nenhum servi\u00e7o de notifica\u00e7\u00e3o dispon\u00edvel." + }, + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente." + }, "step": { + "init": { + "description": "Por favor, selecione um dos servi\u00e7os de notifica\u00e7\u00e3o:", + "title": "Configurar a senha de uso \u00fanico entregue pelo componente de notifica\u00e7\u00e3o" + }, "setup": { + "description": "A senha de uso \u00fanico foi enviada via ** notify. {notify_service} **. Por favor, insira abaixo:", "title": "Verificar a configura\u00e7\u00e3o" } - } + }, + "title": "Notificar a senha de uso \u00fanico" }, "totp": { "error": { @@ -13,6 +25,7 @@ }, "step": { "init": { + "description": "Para ativar a autentica\u00e7\u00e3o de dois fatores usando senhas de uso \u00fanico com base em tempo, digitalize o c\u00f3digo QR com seu aplicativo de autentica\u00e7\u00e3o. Se voc\u00ea n\u00e3o tiver um, recomendamos o [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ou [Authy] (https://authy.com/). \n\n {qr_code} \n \n Depois de digitalizar o c\u00f3digo, insira o c\u00f3digo de seis d\u00edgitos do aplicativo para verificar a configura\u00e7\u00e3o. Se voc\u00ea tiver problemas para escanear o c\u00f3digo QR, fa\u00e7a uma configura\u00e7\u00e3o manual com o c\u00f3digo ** ` {code} ` **.", "title": "Configure a autentica\u00e7\u00e3o de dois fatores usando o TOTP" } }, diff --git a/homeassistant/components/auth/.translations/vi.json b/homeassistant/components/auth/.translations/vi.json new file mode 100644 index 00000000000..02ac69bb983 --- /dev/null +++ b/homeassistant/components/auth/.translations/vi.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "M\u00e3 kh\u00f4ng h\u1ee3p l\u1ec7, vui l\u00f2ng th\u1eed l\u1ea1i. N\u1ebfu b\u1ea1n g\u1eb7p l\u1ed7i n\u00e0y m\u1ed9t c\u00e1ch nh\u1ea5t qu\u00e1n, vui l\u00f2ng \u0111\u1ea3m b\u1ea3o \u0111\u1ed3ng h\u1ed3 c\u1ee7a h\u1ec7 th\u1ed1ng Home Assistant l\u00e0 ch\u00ednh x\u00e1c." + }, + "step": { + "init": { + "description": "\u0110\u1ec3 k\u00edch ho\u1ea1t x\u00e1c th\u1ef1c hai y\u1ebfu t\u1ed1 b\u1eb1ng m\u1eadt kh\u1ea9u m\u1ed9t l\u1ea7n d\u1ef1a tr\u00ean th\u1eddi gian, h\u00e3y qu\u00e9t m\u00e3 QR b\u1eb1ng \u1ee9ng d\u1ee5ng x\u00e1c th\u1ef1c c\u1ee7a b\u1ea1n. N\u1ebfu b\u1ea1n kh\u00f4ng c\u00f3, ch\u00fang t\u00f4i khuy\u00ean b\u1ea1n n\u00ean d\u00f9ng [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ho\u1eb7c [Authy] (https://authy.com/). \n\n {qr_code} \n \n Sau khi qu\u00e9t m\u00e3, nh\u1eadp m\u00e3 s\u00e1u ch\u1eef s\u1ed1 t\u1eeb \u1ee9ng d\u1ee5ng c\u1ee7a b\u1ea1n \u0111\u1ec3 x\u00e1c minh thi\u1ebft l\u1eadp. N\u1ebfu b\u1ea1n g\u1eb7p v\u1ea5n \u0111\u1ec1 khi qu\u00e9t m\u00e3 QR, h\u00e3y th\u1ef1c hi\u1ec7n c\u00e0i \u0111\u1eb7t th\u1ee7 c\u00f4ng v\u1edbi m\u00e3 ** ` {code} ` **.", + "title": "Thi\u1ebft l\u1eadp x\u00e1c th\u1ef1c hai y\u1ebfu t\u1ed1 b\u1eb1ng TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index f1deaf0cb85..d0da9d39fe8 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -121,8 +121,11 @@ from datetime import timedelta from aiohttp import web import voluptuous as vol -from homeassistant.auth.models import User, Credentials, \ - TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN +from homeassistant.auth.models import ( + User, + Credentials, + TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, +) from homeassistant.loader import bind_hass from homeassistant.components import websocket_api from homeassistant.components.http import KEY_REAL_IP @@ -137,44 +140,46 @@ from . import indieauth from . import login_flow from . import mfa_setup_flow -DOMAIN = 'auth' -WS_TYPE_CURRENT_USER = 'auth/current_user' -SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_CURRENT_USER, -}) +DOMAIN = "auth" +WS_TYPE_CURRENT_USER = "auth/current_user" +SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_CURRENT_USER} +) -WS_TYPE_LONG_LIVED_ACCESS_TOKEN = 'auth/long_lived_access_token' -SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \ - websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_LONG_LIVED_ACCESS_TOKEN, - vol.Required('lifespan'): int, # days - vol.Required('client_name'): str, - vol.Optional('client_icon'): str, - }) +WS_TYPE_LONG_LIVED_ACCESS_TOKEN = "auth/long_lived_access_token" +SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_LONG_LIVED_ACCESS_TOKEN, + vol.Required("lifespan"): int, # days + vol.Required("client_name"): str, + vol.Optional("client_icon"): str, + } +) -WS_TYPE_REFRESH_TOKENS = 'auth/refresh_tokens' -SCHEMA_WS_REFRESH_TOKENS = \ - websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_REFRESH_TOKENS, - }) +WS_TYPE_REFRESH_TOKENS = "auth/refresh_tokens" +SCHEMA_WS_REFRESH_TOKENS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_REFRESH_TOKENS} +) -WS_TYPE_DELETE_REFRESH_TOKEN = 'auth/delete_refresh_token' -SCHEMA_WS_DELETE_REFRESH_TOKEN = \ - websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_DELETE_REFRESH_TOKEN, - vol.Required('refresh_token_id'): str, - }) +WS_TYPE_DELETE_REFRESH_TOKEN = "auth/delete_refresh_token" +SCHEMA_WS_DELETE_REFRESH_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_DELETE_REFRESH_TOKEN, + vol.Required("refresh_token_id"): str, + } +) -WS_TYPE_SIGN_PATH = 'auth/sign_path' -SCHEMA_WS_SIGN_PATH = \ - websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_SIGN_PATH, - vol.Required('path'): str, - vol.Optional('expires', default=30): int, - }) +WS_TYPE_SIGN_PATH = "auth/sign_path" +SCHEMA_WS_SIGN_PATH = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_SIGN_PATH, + vol.Required("path"): str, + vol.Optional("expires", default=30): int, + } +) -RESULT_TYPE_CREDENTIALS = 'credentials' -RESULT_TYPE_USER = 'user' +RESULT_TYPE_CREDENTIALS = "credentials" +RESULT_TYPE_USER = "user" _LOGGER = logging.getLogger(__name__) @@ -195,28 +200,23 @@ async def async_setup(hass, config): hass.http.register_view(LinkUserView(retrieve_result)) hass.components.websocket_api.async_register_command( - WS_TYPE_CURRENT_USER, websocket_current_user, - SCHEMA_WS_CURRENT_USER + WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER ) hass.components.websocket_api.async_register_command( WS_TYPE_LONG_LIVED_ACCESS_TOKEN, websocket_create_long_lived_access_token, - SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN + SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN, ) hass.components.websocket_api.async_register_command( - WS_TYPE_REFRESH_TOKENS, - websocket_refresh_tokens, - SCHEMA_WS_REFRESH_TOKENS + WS_TYPE_REFRESH_TOKENS, websocket_refresh_tokens, SCHEMA_WS_REFRESH_TOKENS ) hass.components.websocket_api.async_register_command( WS_TYPE_DELETE_REFRESH_TOKEN, websocket_delete_refresh_token, - SCHEMA_WS_DELETE_REFRESH_TOKEN + SCHEMA_WS_DELETE_REFRESH_TOKEN, ) hass.components.websocket_api.async_register_command( - WS_TYPE_SIGN_PATH, - websocket_sign_path, - SCHEMA_WS_SIGN_PATH + WS_TYPE_SIGN_PATH, websocket_sign_path, SCHEMA_WS_SIGN_PATH ) await login_flow.async_setup(hass, store_result) @@ -228,8 +228,8 @@ async def async_setup(hass, config): class TokenView(HomeAssistantView): """View to issue or revoke tokens.""" - url = '/auth/token' - name = 'api:auth:token' + url = "/auth/token" + name = "api:auth:token" requires_auth = False cors_allowed = True @@ -240,29 +240,29 @@ class TokenView(HomeAssistantView): @log_invalid_auth async def post(self, request): """Grant a token.""" - hass = request.app['hass'] + hass = request.app["hass"] data = await request.post() - grant_type = data.get('grant_type') + grant_type = data.get("grant_type") # IndieAuth 6.3.5 # The revocation endpoint is the same as the token endpoint. # The revocation request includes an additional parameter, # action=revoke. - if data.get('action') == 'revoke': + if data.get("action") == "revoke": return await self._async_handle_revoke_token(hass, data) - if grant_type == 'authorization_code': + if grant_type == "authorization_code": return await self._async_handle_auth_code( - hass, data, str(request[KEY_REAL_IP])) + hass, data, str(request[KEY_REAL_IP]) + ) - if grant_type == 'refresh_token': + if grant_type == "refresh_token": return await self._async_handle_refresh_token( - hass, data, str(request[KEY_REAL_IP])) + hass, data, str(request[KEY_REAL_IP]) + ) - return self.json({ - 'error': 'unsupported_grant_type', - }, status_code=400) + return self.json({"error": "unsupported_grant_type"}, status_code=400) async def _async_handle_revoke_token(self, hass, data): """Handle revoke token request.""" @@ -270,7 +270,7 @@ class TokenView(HomeAssistantView): # 2.2 The authorization server responds with HTTP status code 200 # if the token has been revoked successfully or if the client # submitted an invalid token. - token = data.get('token') + token = data.get("token") if token is None: return web.Response(status=200) @@ -285,117 +285,112 @@ class TokenView(HomeAssistantView): async def _async_handle_auth_code(self, hass, data, remote_addr): """Handle authorization code request.""" - client_id = data.get('client_id') + client_id = data.get("client_id") if client_id is None or not indieauth.verify_client_id(client_id): - return self.json({ - 'error': 'invalid_request', - 'error_description': 'Invalid client id', - }, status_code=400) + return self.json( + {"error": "invalid_request", "error_description": "Invalid client id"}, + status_code=400, + ) - code = data.get('code') + code = data.get("code") if code is None: - return self.json({ - 'error': 'invalid_request', - 'error_description': 'Invalid code', - }, status_code=400) + return self.json( + {"error": "invalid_request", "error_description": "Invalid code"}, + status_code=400, + ) user = self._retrieve_user(client_id, RESULT_TYPE_USER, code) if user is None or not isinstance(user, User): - return self.json({ - 'error': 'invalid_request', - 'error_description': 'Invalid code', - }, status_code=400) + return self.json( + {"error": "invalid_request", "error_description": "Invalid code"}, + status_code=400, + ) # refresh user user = await hass.auth.async_get_user(user.id) if not user.is_active: - return self.json({ - 'error': 'access_denied', - 'error_description': 'User is not active', - }, status_code=403) + return self.json( + {"error": "access_denied", "error_description": "User is not active"}, + status_code=403, + ) - refresh_token = await hass.auth.async_create_refresh_token(user, - client_id) - access_token = hass.auth.async_create_access_token( - refresh_token, remote_addr) + refresh_token = await hass.auth.async_create_refresh_token(user, client_id) + access_token = hass.auth.async_create_access_token(refresh_token, remote_addr) - return self.json({ - 'access_token': access_token, - 'token_type': 'Bearer', - 'refresh_token': refresh_token.token, - 'expires_in': - int(refresh_token.access_token_expiration.total_seconds()), - }) + return self.json( + { + "access_token": access_token, + "token_type": "Bearer", + "refresh_token": refresh_token.token, + "expires_in": int( + refresh_token.access_token_expiration.total_seconds() + ), + } + ) async def _async_handle_refresh_token(self, hass, data, remote_addr): """Handle authorization code request.""" - client_id = data.get('client_id') + client_id = data.get("client_id") if client_id is not None and not indieauth.verify_client_id(client_id): - return self.json({ - 'error': 'invalid_request', - 'error_description': 'Invalid client id', - }, status_code=400) + return self.json( + {"error": "invalid_request", "error_description": "Invalid client id"}, + status_code=400, + ) - token = data.get('refresh_token') + token = data.get("refresh_token") if token is None: - return self.json({ - 'error': 'invalid_request', - }, status_code=400) + return self.json({"error": "invalid_request"}, status_code=400) refresh_token = await hass.auth.async_get_refresh_token_by_token(token) if refresh_token is None: - return self.json({ - 'error': 'invalid_grant', - }, status_code=400) + return self.json({"error": "invalid_grant"}, status_code=400) if refresh_token.client_id != client_id: - return self.json({ - 'error': 'invalid_request', - }, status_code=400) + return self.json({"error": "invalid_request"}, status_code=400) - access_token = hass.auth.async_create_access_token( - refresh_token, remote_addr) + access_token = hass.auth.async_create_access_token(refresh_token, remote_addr) - return self.json({ - 'access_token': access_token, - 'token_type': 'Bearer', - 'expires_in': - int(refresh_token.access_token_expiration.total_seconds()), - }) + return self.json( + { + "access_token": access_token, + "token_type": "Bearer", + "expires_in": int( + refresh_token.access_token_expiration.total_seconds() + ), + } + ) class LinkUserView(HomeAssistantView): """View to link existing users to new credentials.""" - url = '/auth/link_user' - name = 'api:auth:link_user' + url = "/auth/link_user" + name = "api:auth:link_user" def __init__(self, retrieve_credentials): """Initialize the link user view.""" self._retrieve_credentials = retrieve_credentials - @RequestDataValidator(vol.Schema({ - 'code': str, - 'client_id': str, - })) + @RequestDataValidator(vol.Schema({"code": str, "client_id": str})) async def post(self, request, data): """Link a user.""" - hass = request.app['hass'] - user = request['hass_user'] + hass = request.app["hass"] + user = request["hass_user"] credentials = self._retrieve_credentials( - data['client_id'], RESULT_TYPE_CREDENTIALS, data['code']) + data["client_id"], RESULT_TYPE_CREDENTIALS, data["code"] + ) if credentials is None: - return self.json_message('Invalid code', status_code=400) + return self.json_message("Invalid code", status_code=400) await hass.auth.async_link_user(user, credentials) - return self.json_message('User linked') + return self.json_message("User linked") @callback @@ -411,11 +406,14 @@ def _create_auth_code_store(): elif isinstance(result, Credentials): result_type = RESULT_TYPE_CREDENTIALS else: - raise ValueError('result has to be either User or Credentials') + raise ValueError("result has to be either User or Credentials") code = uuid.uuid4().hex - temp_results[(client_id, result_type, code)] = \ - (dt_util.utcnow(), result_type, result) + temp_results[(client_id, result_type, code)] = ( + dt_util.utcnow(), + result_type, + result, + ) return code @callback @@ -443,89 +441,121 @@ def _create_auth_code_store(): @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_current_user( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): """Return the current user.""" user = connection.user enabled_modules = await hass.auth.async_get_enabled_mfa(user) connection.send_message( - websocket_api.result_message(msg['id'], { - 'id': user.id, - 'name': user.name, - 'is_owner': user.is_owner, - 'is_admin': user.is_admin, - 'credentials': [{'auth_provider_type': c.auth_provider_type, - 'auth_provider_id': c.auth_provider_id} - for c in user.credentials], - 'mfa_modules': [{ - 'id': module.id, - 'name': module.name, - 'enabled': module.id in enabled_modules, - } for module in hass.auth.auth_mfa_modules], - })) + websocket_api.result_message( + msg["id"], + { + "id": user.id, + "name": user.name, + "is_owner": user.is_owner, + "is_admin": user.is_admin, + "credentials": [ + { + "auth_provider_type": c.auth_provider_type, + "auth_provider_id": c.auth_provider_id, + } + for c in user.credentials + ], + "mfa_modules": [ + { + "id": module.id, + "name": module.name, + "enabled": module.id in enabled_modules, + } + for module in hass.auth.auth_mfa_modules + ], + }, + ) + ) @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_create_long_lived_access_token( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): """Create or a long-lived access token.""" refresh_token = await hass.auth.async_create_refresh_token( connection.user, - client_name=msg['client_name'], - client_icon=msg.get('client_icon'), + client_name=msg["client_name"], + client_icon=msg.get("client_icon"), token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, - access_token_expiration=timedelta(days=msg['lifespan'])) + access_token_expiration=timedelta(days=msg["lifespan"]), + ) - access_token = hass.auth.async_create_access_token( - refresh_token) + access_token = hass.auth.async_create_access_token(refresh_token) - connection.send_message( - websocket_api.result_message(msg['id'], access_token)) + connection.send_message(websocket_api.result_message(msg["id"], access_token)) @websocket_api.ws_require_user() @callback def websocket_refresh_tokens( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): """Return metadata of users refresh tokens.""" current_id = connection.refresh_token_id - connection.send_message(websocket_api.result_message(msg['id'], [{ - 'id': refresh.id, - 'client_id': refresh.client_id, - 'client_name': refresh.client_name, - 'client_icon': refresh.client_icon, - 'type': refresh.token_type, - 'created_at': refresh.created_at, - 'is_current': refresh.id == current_id, - 'last_used_at': refresh.last_used_at, - 'last_used_ip': refresh.last_used_ip, - } for refresh in connection.user.refresh_tokens.values()])) + connection.send_message( + websocket_api.result_message( + msg["id"], + [ + { + "id": refresh.id, + "client_id": refresh.client_id, + "client_name": refresh.client_name, + "client_icon": refresh.client_icon, + "type": refresh.token_type, + "created_at": refresh.created_at, + "is_current": refresh.id == current_id, + "last_used_at": refresh.last_used_at, + "last_used_ip": refresh.last_used_ip, + } + for refresh in connection.user.refresh_tokens.values() + ], + ) + ) @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_delete_refresh_token( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): """Handle a delete refresh token request.""" - refresh_token = connection.user.refresh_tokens.get(msg['refresh_token_id']) + refresh_token = connection.user.refresh_tokens.get(msg["refresh_token_id"]) if refresh_token is None: return websocket_api.error_message( - msg['id'], 'invalid_token_id', 'Received invalid token') + msg["id"], "invalid_token_id", "Received invalid token" + ) await hass.auth.async_remove_refresh_token(refresh_token) - connection.send_message( - websocket_api.result_message(msg['id'], {})) + connection.send_message(websocket_api.result_message(msg["id"], {})) @websocket_api.ws_require_user() @callback def websocket_sign_path( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): """Handle a sign path request.""" - connection.send_message(websocket_api.result_message(msg['id'], { - 'path': async_sign_path(hass, connection.refresh_token_id, msg['path'], - timedelta(seconds=msg['expires'])) - })) + connection.send_message( + websocket_api.result_message( + msg["id"], + { + "path": async_sign_path( + hass, + connection.refresh_token_id, + msg["path"], + timedelta(seconds=msg["expires"]), + ) + }, + ) + ) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index a56671c9dcd..6a0a516bee2 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -23,8 +23,8 @@ async def verify_redirect_uri(hass, client_id, redirect_uri): # Verify redirect url and client url have same scheme and domain. is_valid = ( - client_id_parts.scheme == redirect_parts.scheme and - client_id_parts.netloc == redirect_parts.netloc + client_id_parts.scheme == redirect_parts.scheme + and client_id_parts.netloc == redirect_parts.netloc ) if is_valid: @@ -47,13 +47,13 @@ class LinkTagParser(HTMLParser): def handle_starttag(self, tag, attrs): """Handle finding a start tag.""" - if tag != 'link': + if tag != "link": return attrs = dict(attrs) - if attrs.get('rel') == self.rel: - self.found.append(attrs.get('href')) + if attrs.get("rel") == self.rel: + self.found.append(attrs.get("href")) async def fetch_redirect_uris(hass, url): @@ -68,7 +68,7 @@ async def fetch_redirect_uris(hass, url): We do not implement extracting redirect uris from headers. """ - parser = LinkTagParser('redirect_uri') + parser = LinkTagParser("redirect_uri") chunks = 0 try: async with aiohttp.ClientSession() as session: @@ -87,12 +87,12 @@ async def fetch_redirect_uris(hass, url): _LOGGER.error("SSL error while looking up redirect_uri %s", url) pass except aiohttp.client_exceptions.ClientOSError as ex: - _LOGGER.error("OS error while looking up redirect_uri %s: %s", url, - ex.strerror) + _LOGGER.error("OS error while looking up redirect_uri %s: %s", url, ex.strerror) pass except aiohttp.client_exceptions.ClientConnectionError: - _LOGGER.error(("Low level connection error while looking up " - "redirect_uri %s"), url) + _LOGGER.error( + ("Low level connection error while looking up " "redirect_uri %s"), url + ) pass except aiohttp.client_exceptions.ClientError: _LOGGER.error("Unknown error while looking up redirect_uri %s", url) @@ -125,8 +125,8 @@ def _parse_url(url): # If a URL with no path component is ever encountered, # it MUST be treated as if it had the path /. - if parts.path == '': - parts = parts._replace(path='/') + if parts.path == "": + parts = parts._replace(path="/") return parts @@ -140,34 +140,35 @@ def _parse_client_id(client_id): # Client identifier URLs # MUST have either an https or http scheme - if parts.scheme not in ('http', 'https'): + if parts.scheme not in ("http", "https"): raise ValueError() # MUST contain a path component # Handled by url canonicalization. # MUST NOT contain single-dot or double-dot path segments - if any(segment in ('.', '..') for segment in parts.path.split('/')): + if any(segment in (".", "..") for segment in parts.path.split("/")): raise ValueError( - 'Client ID cannot contain single-dot or double-dot path segments') + "Client ID cannot contain single-dot or double-dot path segments" + ) # MUST NOT contain a fragment component - if parts.fragment != '': - raise ValueError('Client ID cannot contain a fragment') + if parts.fragment != "": + raise ValueError("Client ID cannot contain a fragment") # MUST NOT contain a username or password component if parts.username is not None: - raise ValueError('Client ID cannot contain username') + raise ValueError("Client ID cannot contain username") if parts.password is not None: - raise ValueError('Client ID cannot contain password') + raise ValueError("Client ID cannot contain password") # MAY contain a port try: # parts raises ValueError when port cannot be parsed as int parts.port except ValueError: - raise ValueError('Client ID contains invalid port') + raise ValueError("Client ID contains invalid port") # Additionally, hostnames # MUST be domain names or a loopback interface and @@ -183,7 +184,7 @@ def _parse_client_id(client_id): netloc = parts.netloc # Strip the [, ] from ipv6 addresses before parsing - if netloc[0] == '[' and netloc[-1] == ']': + if netloc[0] == "[" and netloc[-1] == "]": netloc = netloc[1:-1] address = ip_address(netloc) @@ -194,4 +195,4 @@ def _parse_client_id(client_id): if address is None or is_local(address): return parts - raise ValueError('Hostname should be a domain name or local IP address') + raise ValueError("Hostname should be a domain name or local IP address") diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 7fd767f4a43..0f5da5d7527 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -71,8 +71,7 @@ import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.components.http import KEY_REAL_IP -from homeassistant.components.http.ban import process_wrong_login, \ - log_invalid_auth +from homeassistant.components.http.ban import process_wrong_login, log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from . import indieauth @@ -81,56 +80,55 @@ from . import indieauth async def async_setup(hass, store_result): """Component to allow users to login.""" hass.http.register_view(AuthProvidersView) - hass.http.register_view( - LoginFlowIndexView(hass.auth.login_flow, store_result)) - hass.http.register_view( - LoginFlowResourceView(hass.auth.login_flow, store_result)) + hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result)) + hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result)) class AuthProvidersView(HomeAssistantView): """View to get available auth providers.""" - url = '/auth/providers' - name = 'api:auth:providers' + url = "/auth/providers" + name = "api:auth:providers" requires_auth = False async def get(self, request): """Get available auth providers.""" - hass = request.app['hass'] + hass = request.app["hass"] if not hass.components.onboarding.async_is_user_onboarded(): return self.json_message( - message='Onboarding not finished', + message="Onboarding not finished", status_code=400, - message_code='onboarding_required' + message_code="onboarding_required", ) - return self.json([{ - 'name': provider.name, - 'id': provider.id, - 'type': provider.type, - } for provider in hass.auth.auth_providers]) + return self.json( + [ + {"name": provider.name, "id": provider.id, "type": provider.type} + for provider in hass.auth.auth_providers + ] + ) def _prepare_result_json(result): """Convert result to JSON.""" - if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: data = result.copy() - data.pop('result') - data.pop('data') + data.pop("result") + data.pop("data") return data - if result['type'] != data_entry_flow.RESULT_TYPE_FORM: + if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result import voluptuous_serialize data = result.copy() - schema = data['data_schema'] + schema = data["data_schema"] if schema is None: - data['data_schema'] = [] + data["data_schema"] = [] else: - data['data_schema'] = voluptuous_serialize.convert(schema) + data["data_schema"] = voluptuous_serialize.convert(schema) return data @@ -138,8 +136,8 @@ def _prepare_result_json(result): class LoginFlowIndexView(HomeAssistantView): """View to create a config flow.""" - url = '/auth/login_flow' - name = 'api:auth:login_flow' + url = "/auth/login_flow" + name = "api:auth:login_flow" requires_auth = False def __init__(self, flow_mgr, store_result): @@ -151,39 +149,45 @@ class LoginFlowIndexView(HomeAssistantView): """Do not allow index of flows in progress.""" return web.Response(status=405) - @RequestDataValidator(vol.Schema({ - vol.Required('client_id'): str, - vol.Required('handler'): vol.Any(str, list), - vol.Required('redirect_uri'): str, - vol.Optional('type', default='authorize'): str, - })) + @RequestDataValidator( + vol.Schema( + { + vol.Required("client_id"): str, + vol.Required("handler"): vol.Any(str, list), + vol.Required("redirect_uri"): str, + vol.Optional("type", default="authorize"): str, + } + ) + ) @log_invalid_auth async def post(self, request, data): """Create a new login flow.""" if not await indieauth.verify_redirect_uri( - request.app['hass'], data['client_id'], data['redirect_uri']): - return self.json_message('invalid client id or redirect uri', 400) + request.app["hass"], data["client_id"], data["redirect_uri"] + ): + return self.json_message("invalid client id or redirect uri", 400) - if isinstance(data['handler'], list): - handler = tuple(data['handler']) + if isinstance(data["handler"], list): + handler = tuple(data["handler"]) else: - handler = data['handler'] + handler = data["handler"] try: result = await self._flow_mgr.async_init( - handler, context={ - 'ip_address': request[KEY_REAL_IP], - 'credential_only': data.get('type') == 'link_user', - }) + handler, + context={ + "ip_address": request[KEY_REAL_IP], + "credential_only": data.get("type") == "link_user", + }, + ) except data_entry_flow.UnknownHandler: - return self.json_message('Invalid handler specified', 404) + return self.json_message("Invalid handler specified", 404) except data_entry_flow.UnknownStep: - return self.json_message('Handler does not support init', 400) + return self.json_message("Handler does not support init", 400) - if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - result.pop('data') - result['result'] = self._store_result( - data['client_id'], result['result']) + if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + result.pop("data") + result["result"] = self._store_result(data["client_id"], result["result"]) return self.json(result) return self.json(_prepare_result_json(result)) @@ -192,8 +196,8 @@ class LoginFlowIndexView(HomeAssistantView): class LoginFlowResourceView(HomeAssistantView): """View to interact with the flow manager.""" - url = '/auth/login_flow/{flow_id}' - name = 'api:auth:login_flow:resource' + url = "/auth/login_flow/{flow_id}" + name = "api:auth:login_flow:resource" requires_auth = False def __init__(self, flow_mgr, store_result): @@ -203,44 +207,43 @@ class LoginFlowResourceView(HomeAssistantView): async def get(self, request): """Do not allow getting status of a flow in progress.""" - return self.json_message('Invalid flow specified', 404) + return self.json_message("Invalid flow specified", 404) - @RequestDataValidator(vol.Schema({ - 'client_id': str - }, extra=vol.ALLOW_EXTRA)) + @RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA)) @log_invalid_auth async def post(self, request, flow_id, data): """Handle progressing a login flow request.""" - client_id = data.pop('client_id') + client_id = data.pop("client_id") if not indieauth.verify_client_id(client_id): - return self.json_message('Invalid client id', 400) + return self.json_message("Invalid client id", 400) try: # do not allow change ip during login flow for flow in self._flow_mgr.async_progress(): - if (flow['flow_id'] == flow_id and - flow['context']['ip_address'] != - request.get(KEY_REAL_IP)): - return self.json_message('IP address changed', 400) + if flow["flow_id"] == flow_id and flow["context"][ + "ip_address" + ] != request.get(KEY_REAL_IP): + return self.json_message("IP address changed", 400) result = await self._flow_mgr.async_configure(flow_id, data) except data_entry_flow.UnknownFlow: - return self.json_message('Invalid flow specified', 404) + return self.json_message("Invalid flow specified", 404) except vol.Invalid: - return self.json_message('User input malformed', 400) + return self.json_message("User input malformed", 400) - if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: # @log_invalid_auth does not work here since it returns HTTP 200 # need manually log failed login attempts - if (result.get('errors') is not None and - result['errors'].get('base') in ['invalid_auth', - 'invalid_code']): + if result.get("errors") is not None and result["errors"].get("base") in [ + "invalid_auth", + "invalid_code", + ]: await process_wrong_login(request) return self.json(_prepare_result_json(result)) - result.pop('data') - result['result'] = self._store_result(client_id, result['result']) + result.pop("data") + result["result"] = self._store_result(client_id, result["result"]) return self.json(result) @@ -249,6 +252,6 @@ class LoginFlowResourceView(HomeAssistantView): try: self._flow_mgr.async_abort(flow_id) except data_entry_flow.UnknownFlow: - return self.json_message('Invalid flow specified', 404) + return self.json_message("Invalid flow specified", 404) - return self.json_message('Flow aborted') + return self.json_message("Flow aborted") diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 121d95aede3..c18bc276a44 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -7,82 +7,93 @@ from homeassistant import data_entry_flow from homeassistant.components import websocket_api from homeassistant.core import callback, HomeAssistant -WS_TYPE_SETUP_MFA = 'auth/setup_mfa' -SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_SETUP_MFA, - vol.Exclusive('mfa_module_id', 'module_or_flow_id'): str, - vol.Exclusive('flow_id', 'module_or_flow_id'): str, - vol.Optional('user_input'): object, -}) +WS_TYPE_SETUP_MFA = "auth/setup_mfa" +SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_SETUP_MFA, + vol.Exclusive("mfa_module_id", "module_or_flow_id"): str, + vol.Exclusive("flow_id", "module_or_flow_id"): str, + vol.Optional("user_input"): object, + } +) -WS_TYPE_DEPOSE_MFA = 'auth/depose_mfa' -SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_DEPOSE_MFA, - vol.Required('mfa_module_id'): str, -}) +WS_TYPE_DEPOSE_MFA = "auth/depose_mfa" +SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_DEPOSE_MFA, vol.Required("mfa_module_id"): str} +) -DATA_SETUP_FLOW_MGR = 'auth_mfa_setup_flow_manager' +DATA_SETUP_FLOW_MGR = "auth_mfa_setup_flow_manager" _LOGGER = logging.getLogger(__name__) async def async_setup(hass): """Init mfa setup flow manager.""" + async def _async_create_setup_flow(handler, context, data): - """Create a setup flow. hanlder is a mfa module.""" + """Create a setup flow. handler is a mfa module.""" mfa_module = hass.auth.get_auth_mfa_module(handler) if mfa_module is None: - raise ValueError('Mfa module {} is not found'.format(handler)) + raise ValueError("Mfa module {} is not found".format(handler)) - user_id = data.pop('user_id') + user_id = data.pop("user_id") return await mfa_module.async_setup_flow(user_id) async def _async_finish_setup_flow(flow, flow_result): - _LOGGER.debug('flow_result: %s', flow_result) + _LOGGER.debug("flow_result: %s", flow_result) return flow_result hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager( - hass, _async_create_setup_flow, _async_finish_setup_flow) + hass, _async_create_setup_flow, _async_finish_setup_flow + ) hass.components.websocket_api.async_register_command( - WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA) + WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA + ) hass.components.websocket_api.async_register_command( - WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA) + WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA + ) @callback @websocket_api.ws_require_user(allow_system_user=False) def websocket_setup_mfa( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): """Return a setup flow for mfa auth module.""" + async def async_setup_flow(msg): """Return a setup flow for mfa auth module.""" flow_manager = hass.data[DATA_SETUP_FLOW_MGR] - flow_id = msg.get('flow_id') + flow_id = msg.get("flow_id") if flow_id is not None: - result = await flow_manager.async_configure( - flow_id, msg.get('user_input')) + result = await flow_manager.async_configure(flow_id, msg.get("user_input")) connection.send_message( - websocket_api.result_message( - msg['id'], _prepare_result_json(result))) + websocket_api.result_message(msg["id"], _prepare_result_json(result)) + ) return - mfa_module_id = msg.get('mfa_module_id') + mfa_module_id = msg.get("mfa_module_id") mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id) if mfa_module is None: - connection.send_message(websocket_api.error_message( - msg['id'], 'no_module', - 'MFA module {} is not found'.format(mfa_module_id))) + connection.send_message( + websocket_api.error_message( + msg["id"], + "no_module", + "MFA module {} is not found".format(mfa_module_id), + ) + ) return result = await flow_manager.async_init( - mfa_module_id, data={'user_id': connection.user.id}) + mfa_module_id, data={"user_id": connection.user.id} + ) connection.send_message( - websocket_api.result_message( - msg['id'], _prepare_result_json(result))) + websocket_api.result_message(msg["id"], _prepare_result_json(result)) + ) hass.async_create_task(async_setup_flow(msg)) @@ -90,45 +101,49 @@ def websocket_setup_mfa( @callback @websocket_api.ws_require_user(allow_system_user=False) def websocket_depose_mfa( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): """Remove user from mfa module.""" + async def async_depose(msg): """Remove user from mfa auth module.""" - mfa_module_id = msg['mfa_module_id'] + mfa_module_id = msg["mfa_module_id"] try: await hass.auth.async_disable_user_mfa( - connection.user, msg['mfa_module_id']) + connection.user, msg["mfa_module_id"] + ) except ValueError as err: - connection.send_message(websocket_api.error_message( - msg['id'], 'disable_failed', - 'Cannot disable MFA Module {}: {}'.format( - mfa_module_id, err))) + connection.send_message( + websocket_api.error_message( + msg["id"], + "disable_failed", + "Cannot disable MFA Module {}: {}".format(mfa_module_id, err), + ) + ) return - connection.send_message( - websocket_api.result_message( - msg['id'], 'done')) + connection.send_message(websocket_api.result_message(msg["id"], "done")) hass.async_create_task(async_depose(msg)) def _prepare_result_json(result): """Convert result to JSON.""" - if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: data = result.copy() return data - if result['type'] != data_entry_flow.RESULT_TYPE_FORM: + if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result import voluptuous_serialize data = result.copy() - schema = data['data_schema'] + schema = data["data_schema"] if schema is None: - data['data_schema'] = [] + data["data_schema"] = [] else: - data['data_schema'] = voluptuous_serialize.convert(schema) + data["data_schema"] = voluptuous_serialize.convert(schema) return data diff --git a/homeassistant/components/automatic/device_tracker.py b/homeassistant/components/automatic/device_tracker.py index 04e069a04f9..09cf3f67114 100644 --- a/homeassistant/components/automatic/device_tracker.py +++ b/homeassistant/components/automatic/device_tracker.py @@ -9,8 +9,14 @@ from aiohttp import web import voluptuous as vol from homeassistant.components.device_tracker import ( - ATTR_ATTRIBUTES, ATTR_DEV_ID, ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_HOST_NAME, - ATTR_MAC, PLATFORM_SCHEMA) + ATTR_ATTRIBUTES, + ATTR_DEV_ID, + ATTR_GPS, + ATTR_GPS_ACCURACY, + ATTR_HOST_NAME, + ATTR_MAC, + PLATFORM_SCHEMA, +) from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -20,28 +26,30 @@ from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) -ATTR_FUEL_LEVEL = 'fuel_level' -AUTOMATIC_CONFIG_FILE = '.automatic/session-{}.json' +ATTR_FUEL_LEVEL = "fuel_level" +AUTOMATIC_CONFIG_FILE = ".automatic/session-{}.json" -CONF_CLIENT_ID = 'client_id' -CONF_CURRENT_LOCATION = 'current_location' -CONF_DEVICES = 'devices' -CONF_SECRET = 'secret' +CONF_CLIENT_ID = "client_id" +CONF_CURRENT_LOCATION = "current_location" +CONF_DEVICES = "devices" +CONF_SECRET = "secret" -DATA_CONFIGURING = 'automatic_configurator_clients' -DATA_REFRESH_TOKEN = 'refresh_token' -DEFAULT_SCOPE = ['location', 'trip', 'vehicle:events', 'vehicle:profile'] +DATA_CONFIGURING = "automatic_configurator_clients" +DATA_REFRESH_TOKEN = "refresh_token" +DEFAULT_SCOPE = ["location", "trip", "vehicle:events", "vehicle:profile"] DEFAULT_TIMEOUT = 5 -EVENT_AUTOMATIC_UPDATE = 'automatic_update' +EVENT_AUTOMATIC_UPDATE = "automatic_update" -FULL_SCOPE = DEFAULT_SCOPE + ['current_location'] +FULL_SCOPE = DEFAULT_SCOPE + ["current_location"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_SECRET): cv.string, - vol.Optional(CONF_CURRENT_LOCATION, default=False): cv.boolean, - vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [cv.string]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_SECRET): cv.string, + vol.Optional(CONF_CURRENT_LOCATION, default=False): cv.boolean, + vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [cv.string]), + } +) def _get_refresh_token_from_file(hass, filename): @@ -67,10 +75,8 @@ def _write_refresh_token_to_file(hass, filename, refresh_token): path = hass.config.path(filename) os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, 'w+') as data_file: - json.dump({ - DATA_REFRESH_TOKEN: refresh_token - }, data_file) + with open(path, "w+") as data_file: + json.dump({DATA_REFRESH_TOKEN: refresh_token}, data_file) @asyncio.coroutine @@ -86,20 +92,21 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): client_id=config[CONF_CLIENT_ID], client_secret=config[CONF_SECRET], client_session=async_get_clientsession(hass), - request_kwargs={'timeout': DEFAULT_TIMEOUT}) + request_kwargs={"timeout": DEFAULT_TIMEOUT}, + ) filename = AUTOMATIC_CONFIG_FILE.format(config[CONF_CLIENT_ID]) refresh_token = yield from hass.async_add_job( - _get_refresh_token_from_file, hass, filename) + _get_refresh_token_from_file, hass, filename + ) @asyncio.coroutine def initialize_data(session): """Initialize the AutomaticData object from the created session.""" hass.async_add_job( - _write_refresh_token_to_file, hass, filename, - session.refresh_token) - data = AutomaticData( - hass, client, session, config.get(CONF_DEVICES), async_see) + _write_refresh_token_to_file, hass, filename, session.refresh_token + ) + data = AutomaticData(hass, client, session, config.get(CONF_DEVICES), async_see) # Load the initial vehicle data vehicles = yield from session.get_vehicles() @@ -112,8 +119,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): if refresh_token is not None: try: - session = yield from client.create_session_from_refresh_token( - refresh_token) + session = yield from client.create_session_from_refresh_token(refresh_token) yield from initialize_data(session) return True except aioautomatic.exceptions.AutomaticError as err: @@ -121,8 +127,8 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): configurator = hass.components.configurator request_id = configurator.async_request_config( - "Automatic", description=( - "Authorization required for Automatic device tracker."), + "Automatic", + description=("Authorization required for Automatic device tracker."), link_name="Click here to authorize Home Assistant.", link_url=client.generate_oauth_url(scope), entity_picture="/static/images/logo_automatic.png", @@ -132,8 +138,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): def initialize_callback(code, state): """Call after OAuth2 response is returned.""" try: - session = yield from client.create_session_from_oauth_code( - code, state) + session = yield from client.create_session_from_oauth_code(code, state) yield from initialize_data(session) configurator.async_request_done(request_id) except aioautomatic.exceptions.AutomaticError as err: @@ -152,32 +157,32 @@ class AutomaticAuthCallbackView(HomeAssistantView): """Handle OAuth finish callback requests.""" requires_auth = False - url = '/api/automatic/callback' - name = 'api:automatic:callback' + url = "/api/automatic/callback" + name = "api:automatic:callback" @callback def get(self, request): # pylint: disable=no-self-use """Finish OAuth callback request.""" - hass = request.app['hass'] + hass = request.app["hass"] params = request.query - response = web.HTTPFound('/states') + response = web.HTTPFound("/states") - if 'state' not in params or 'code' not in params: - if 'error' in params: - _LOGGER.error( - "Error authorizing Automatic: %s", params['error']) + if "state" not in params or "code" not in params: + if "error" in params: + _LOGGER.error("Error authorizing Automatic: %s", params["error"]) return response - _LOGGER.error( - "Error authorizing Automatic. Invalid response returned") + _LOGGER.error("Error authorizing Automatic. Invalid response returned") return response - if DATA_CONFIGURING not in hass.data or \ - params['state'] not in hass.data[DATA_CONFIGURING]: + if ( + DATA_CONFIGURING not in hass.data + or params["state"] not in hass.data[DATA_CONFIGURING] + ): _LOGGER.error("Automatic configuration request not found") return response - code = params['code'] - state = params['state'] + code = params["code"] + state = params["state"] initialize_callback = hass.data[DATA_CONFIGURING][state] hass.async_create_task(initialize_callback(code, state)) @@ -201,7 +206,9 @@ class AutomaticData: self.client.on_app_event( lambda name, event: self.hass.async_create_task( - self.handle_event(name, event))) + self.handle_event(name, event) + ) + ) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.ws_close()) @@ -225,9 +232,11 @@ class AutomaticData: if event.created_at < self.vehicle_seen[event.vehicle.id]: # Skip events received out of order - _LOGGER.debug("Skipping out of order event. Event Created %s. " - "Last seen event: %s", event.created_at, - self.vehicle_seen[event.vehicle.id]) + _LOGGER.debug( + "Skipping out of order event. Event Created %s. " "Last seen event: %s", + event.created_at, + self.vehicle_seen[event.vehicle.id], + ) return self.vehicle_seen[event.vehicle.id] = event.created_at @@ -253,6 +262,7 @@ class AutomaticData: def ws_connect(self, now=None): """Open the websocket connection.""" import aioautomatic + self.ws_close_requested = False if self.ws_reconnect_handle is not None: @@ -260,16 +270,19 @@ class AutomaticData: try: ws_loop_future = yield from self.client.ws_connect() except aioautomatic.exceptions.UnauthorizedClientError: - _LOGGER.error("Client unauthorized for websocket connection. " - "Ensure Websocket is selected in the Automatic " - "developer application event delivery preferences") + _LOGGER.error( + "Client unauthorized for websocket connection. " + "Ensure Websocket is selected in the Automatic " + "developer application event delivery preferences" + ) return except aioautomatic.exceptions.AutomaticError as err: if self.ws_reconnect_handle is None: # Show log error and retry connection every 5 minutes _LOGGER.error("Error opening websocket connection: %s", err) self.ws_reconnect_handle = async_track_time_interval( - self.hass, self.ws_connect, timedelta(minutes=5)) + self.hass, self.ws_connect, timedelta(minutes=5) + ) return if self.ws_reconnect_handle is not None: @@ -312,8 +325,9 @@ class AutomaticData: name = vehicle.display_name if name is None: - name = ' '.join(filter(None, ( - str(vehicle.year), vehicle.make, vehicle.model))) + name = " ".join( + filter(None, (str(vehicle.year), vehicle.make, vehicle.model)) + ) if self.devices is not None and name not in self.devices: self.vehicle_info[vehicle.id] = None @@ -323,12 +337,9 @@ class AutomaticData: ATTR_DEV_ID: vehicle.id, ATTR_HOST_NAME: name, ATTR_MAC: vehicle.id, - ATTR_ATTRIBUTES: { - ATTR_FUEL_LEVEL: vehicle.fuel_level_percent, - } + ATTR_ATTRIBUTES: {ATTR_FUEL_LEVEL: vehicle.fuel_level_percent}, } - self.vehicle_seen[vehicle.id] = \ - vehicle.updated_at or vehicle.created_at + self.vehicle_seen[vehicle.id] = vehicle.updated_at or vehicle.created_at if vehicle.latest_location is not None: location = vehicle.latest_location @@ -339,8 +350,7 @@ class AutomaticData: trips = [] try: # Get the most recent trip for this vehicle - trips = yield from self.session.get_trips( - vehicle=vehicle.id, limit=1) + trips = yield from self.session.get_trips(vehicle=vehicle.id, limit=1) except aioautomatic.exceptions.AutomaticError as err: _LOGGER.error(str(err)) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 0e8bf30ae13..935f6ea0d02 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -7,44 +7,54 @@ import logging import voluptuous as vol from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_NAME, CONF_ID, CONF_PLATFORM, - EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_START, SERVICE_RELOAD, - SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) + ATTR_ENTITY_ID, + ATTR_NAME, + CONF_ID, + CONF_PLATFORM, + EVENT_AUTOMATION_TRIGGERED, + EVENT_HOMEASSISTANT_START, + SERVICE_RELOAD, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) from homeassistant.core import Context, CoreState from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, extract_domain_configs, script import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime, utcnow -DOMAIN = 'automation' -ENTITY_ID_FORMAT = DOMAIN + '.{}' +DOMAIN = "automation" +ENTITY_ID_FORMAT = DOMAIN + ".{}" -GROUP_NAME_ALL_AUTOMATIONS = 'all automations' +GROUP_NAME_ALL_AUTOMATIONS = "all automations" -CONF_ALIAS = 'alias' -CONF_HIDE_ENTITY = 'hide_entity' +CONF_ALIAS = "alias" +CONF_HIDE_ENTITY = "hide_entity" -CONF_CONDITION = 'condition' -CONF_ACTION = 'action' -CONF_TRIGGER = 'trigger' -CONF_CONDITION_TYPE = 'condition_type' -CONF_INITIAL_STATE = 'initial_state' +CONF_CONDITION = "condition" +CONF_ACTION = "action" +CONF_TRIGGER = "trigger" +CONF_CONDITION_TYPE = "condition_type" +CONF_INITIAL_STATE = "initial_state" -CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values' -CONDITION_TYPE_AND = 'and' -CONDITION_TYPE_OR = 'or' +CONDITION_USE_TRIGGER_VALUES = "use_trigger_values" +CONDITION_TYPE_AND = "and" +CONDITION_TYPE_OR = "or" DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND DEFAULT_HIDE_ENTITY = False DEFAULT_INITIAL_STATE = True -ATTR_LAST_TRIGGERED = 'last_triggered' -ATTR_VARIABLES = 'variables' -SERVICE_TRIGGER = 'trigger' +ATTR_LAST_TRIGGERED = "last_triggered" +ATTR_VARIABLES = "variables" +SERVICE_TRIGGER = "trigger" _LOGGER = logging.getLogger(__name__) @@ -52,10 +62,11 @@ _LOGGER = logging.getLogger(__name__) def _platform_validator(config): """Validate it is a valid platform.""" try: - platform = importlib.import_module('.{}'.format(config[CONF_PLATFORM]), - __name__) + platform = importlib.import_module( + ".{}".format(config[CONF_PLATFORM]), __name__ + ) except ImportError: - raise vol.Invalid('Invalid platform specified') from None + raise vol.Invalid("Invalid platform specified") from None return platform.TRIGGER_SCHEMA(config) @@ -64,35 +75,30 @@ _TRIGGER_SCHEMA = vol.All( cv.ensure_list, [ vol.All( - vol.Schema({ - vol.Required(CONF_PLATFORM): str - }, extra=vol.ALLOW_EXTRA), - _platform_validator - ), - ] + vol.Schema({vol.Required(CONF_PLATFORM): str}, extra=vol.ALLOW_EXTRA), + _platform_validator, + ) + ], ) _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) -PLATFORM_SCHEMA = vol.Schema({ - # str on purpose - CONF_ID: str, - CONF_ALIAS: cv.string, - vol.Optional(CONF_INITIAL_STATE): cv.boolean, - vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, - vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, - vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, - vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, -}) +PLATFORM_SCHEMA = vol.Schema( + { + # str on purpose + CONF_ID: str, + CONF_ALIAS: cv.string, + vol.Optional(CONF_INITIAL_STATE): cv.boolean, + vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, + vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, + vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, + vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, + } +) -SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, -}) - -TRIGGER_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Optional(ATTR_VARIABLES, default={}): dict, -}) +TRIGGER_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Optional(ATTR_VARIABLES, default={}): dict} +) RELOAD_SERVICE_SCHEMA = vol.Schema({}) @@ -109,8 +115,9 @@ def is_on(hass, entity_id): async def async_setup(hass, config): """Set up the automation.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, - group_name=GROUP_NAME_ALL_AUTOMATIONS) + component = EntityComponent( + _LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_AUTOMATIONS + ) await _async_process_config(hass, config, component) @@ -118,10 +125,13 @@ async def async_setup(hass, config): """Handle automation triggers.""" tasks = [] for entity in await component.async_extract_from_service(service_call): - tasks.append(entity.async_trigger( - service_call.data.get(ATTR_VARIABLES), - skip_condition=True, - context=service_call.context)) + tasks.append( + entity.async_trigger( + service_call.data.get(ATTR_VARIABLES), + skip_condition=True, + context=service_call.context, + ) + ) if tasks: await asyncio.wait(tasks) @@ -129,7 +139,7 @@ async def async_setup(hass, config): async def turn_onoff_service_handler(service_call): """Handle automation turn on/off service calls.""" tasks = [] - method = 'async_{}'.format(service_call.service) + method = "async_{}".format(service_call.service) for entity in await component.async_extract_from_service(service_call): tasks.append(getattr(entity, method)()) @@ -156,21 +166,21 @@ async def async_setup(hass, config): await _async_process_config(hass, conf, component) hass.services.async_register( - DOMAIN, SERVICE_TRIGGER, trigger_service_handler, - schema=TRIGGER_SERVICE_SCHEMA) + DOMAIN, SERVICE_TRIGGER, trigger_service_handler, schema=TRIGGER_SERVICE_SCHEMA + ) hass.services.async_register( - DOMAIN, SERVICE_RELOAD, reload_service_handler, - schema=RELOAD_SERVICE_SCHEMA) + DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA + ) hass.services.async_register( - DOMAIN, SERVICE_TOGGLE, toggle_service_handler, - schema=SERVICE_SCHEMA) + DOMAIN, SERVICE_TOGGLE, toggle_service_handler, schema=ENTITY_SERVICE_SCHEMA + ) for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): hass.services.async_register( - DOMAIN, service, turn_onoff_service_handler, - schema=SERVICE_SCHEMA) + DOMAIN, service, turn_onoff_service_handler, schema=ENTITY_SERVICE_SCHEMA + ) return True @@ -178,8 +188,16 @@ async def async_setup(hass, config): class AutomationEntity(ToggleEntity, RestoreEntity): """Entity to show status of entity.""" - def __init__(self, automation_id, name, async_attach_triggers, cond_func, - async_action, hidden, initial_state): + def __init__( + self, + automation_id, + name, + async_attach_triggers, + cond_func, + async_action, + hidden, + initial_state, + ): """Initialize an automation entity.""" self._id = automation_id self._name = name @@ -205,9 +223,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): @property def state_attributes(self): """Return the entity state attributes.""" - return { - ATTR_LAST_TRIGGERED: self._last_triggered - } + return {ATTR_LAST_TRIGGERED: self._last_triggered} @property def hidden(self) -> bool: @@ -217,8 +233,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): @property def is_on(self) -> bool: """Return True if entity is on.""" - return (self._async_detach_triggers is not None or - self._is_enabled) + return self._async_detach_triggers is not None or self._is_enabled async def async_added_to_hass(self) -> None: """Startup with initial state or previous state.""" @@ -227,23 +242,32 @@ class AutomationEntity(ToggleEntity, RestoreEntity): state = await self.async_get_last_state() if state: enable_automation = state.state == STATE_ON - last_triggered = state.attributes.get('last_triggered') + last_triggered = state.attributes.get("last_triggered") if last_triggered is not None: self._last_triggered = parse_datetime(last_triggered) - _LOGGER.debug("Loaded automation %s with state %s from state " - " storage last state %s", self.entity_id, - enable_automation, state) + _LOGGER.debug( + "Loaded automation %s with state %s from state " + " storage last state %s", + self.entity_id, + enable_automation, + state, + ) else: enable_automation = DEFAULT_INITIAL_STATE - _LOGGER.debug("Automation %s not in state storage, state %s from " - "default is used.", self.entity_id, - enable_automation) + _LOGGER.debug( + "Automation %s not in state storage, state %s from " "default is used.", + self.entity_id, + enable_automation, + ) if self._initial_state is not None: enable_automation = self._initial_state - _LOGGER.debug("Automation %s initial state %s overridden from " - "config initial_state", self.entity_id, - enable_automation) + _LOGGER.debug( + "Automation %s initial state %s overridden from " + "config initial_state", + self.entity_id, + enable_automation, + ) if enable_automation: await self.async_enable() @@ -256,8 +280,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """Turn the entity off.""" await self.async_disable() - async def async_trigger(self, variables, skip_condition=False, - context=None): + async def async_trigger(self, variables, skip_condition=False, context=None): """Trigger automation. This method is a coroutine. @@ -270,10 +293,11 @@ class AutomationEntity(ToggleEntity, RestoreEntity): trigger_context = Context(parent_id=parent_id) self.async_set_context(trigger_context) - self.hass.bus.async_fire(EVENT_AUTOMATION_TRIGGERED, { - ATTR_NAME: self._name, - ATTR_ENTITY_ID: self.entity_id, - }, context=trigger_context) + self.hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: self._name, ATTR_ENTITY_ID: self.entity_id}, + context=trigger_context, + ) await self._async_action(self.entity_id, variables, trigger_context) self._last_triggered = utcnow() await self.async_update_ha_state() @@ -296,22 +320,24 @@ class AutomationEntity(ToggleEntity, RestoreEntity): # HomeAssistant is starting up if self.hass.state != CoreState.not_running: self._async_detach_triggers = await self._async_attach_triggers( - self.async_trigger) + self.async_trigger + ) self.async_write_ha_state() return async def async_enable_automation(event): """Start automation on startup.""" # Don't do anything if no longer enabled or already attached - if (not self._is_enabled or - self._async_detach_triggers is not None): + if not self._is_enabled or self._async_detach_triggers is not None: return self._async_detach_triggers = await self._async_attach_triggers( - self.async_trigger) + self.async_trigger + ) self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_enable_automation) + EVENT_HOMEASSISTANT_START, async_enable_automation + ) self.async_write_ha_state() async def async_disable(self): @@ -333,9 +359,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): if self._id is None: return None - return { - CONF_ID: self._id - } + return {CONF_ID: self._id} async def _async_process_config(hass, config, component): @@ -350,14 +374,12 @@ async def _async_process_config(hass, config, component): for list_no, config_block in enumerate(conf): automation_id = config_block.get(CONF_ID) - name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, - list_no) + name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, list_no) hidden = config_block[CONF_HIDE_ENTITY] initial_state = config_block.get(CONF_INITIAL_STATE) - action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), - name) + action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name) if CONF_CONDITION in config_block: cond_func = _async_process_if(hass, config, config_block) @@ -365,17 +387,27 @@ async def _async_process_config(hass, config, component): if cond_func is None: continue else: + def cond_func(variables): """Condition will always pass.""" return True async_attach_triggers = partial( - _async_process_trigger, hass, config, - config_block.get(CONF_TRIGGER, []), name + _async_process_trigger, + hass, + config, + config_block.get(CONF_TRIGGER, []), + name, ) entity = AutomationEntity( - automation_id, name, async_attach_triggers, cond_func, action, - hidden, initial_state) + automation_id, + name, + async_attach_triggers, + cond_func, + action, + hidden, + initial_state, + ) entities.append(entity) @@ -389,14 +421,14 @@ def _async_get_action(hass, config, name): async def action(entity_id, variables, context): """Execute an action.""" - _LOGGER.info('Executing %s', name) + _LOGGER.info("Executing %s", name) try: await script_obj.async_run(variables, context) except Exception as err: # pylint: disable=broad-except script_obj.async_log_exception( - _LOGGER, - 'Error while executing automation {}'.format(entity_id), err) + _LOGGER, "Error while executing automation {}".format(entity_id), err + ) return action @@ -410,7 +442,7 @@ def _async_process_if(hass, config, p_config): try: checks.append(condition.async_from_config(if_config, False)) except HomeAssistantError as ex: - _LOGGER.warning('Invalid condition: %s', ex) + _LOGGER.warning("Invalid condition: %s", ex) return None def if_action(variables=None): @@ -426,13 +458,10 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): This method is a coroutine. """ removes = [] - info = { - 'name': name - } + info = {"name": name} for conf in trigger_configs: - platform = importlib.import_module('.{}'.format(conf[CONF_PLATFORM]), - __name__) + platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) remove = await platform.async_trigger(hass, conf, action, info) diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index 4e59018b41c..2bb70fa1c96 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -5,14 +5,14 @@ from homeassistant.const import CONF_DOMAIN, CONF_PLATFORM from homeassistant.loader import async_get_integration -TRIGGER_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'device', - vol.Required(CONF_DOMAIN): str, -}, extra=vol.ALLOW_EXTRA) +TRIGGER_SCHEMA = vol.Schema( + {vol.Required(CONF_PLATFORM): "device", vol.Required(CONF_DOMAIN): str}, + extra=vol.ALLOW_EXTRA, +) async def async_trigger(hass, config, action, automation_info): """Listen for trigger.""" integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform('device_automation') + platform = integration.get_platform("device_automation") return await platform.async_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 6cc7e3dae7d..b353eb56196 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -7,24 +7,28 @@ from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM from homeassistant.helpers import config_validation as cv -CONF_EVENT_TYPE = 'event_type' -CONF_EVENT_DATA = 'event_data' +CONF_EVENT_TYPE = "event_type" +CONF_EVENT_DATA = "event_data" _LOGGER = logging.getLogger(__name__) -TRIGGER_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'event', - vol.Required(CONF_EVENT_TYPE): cv.string, - vol.Optional(CONF_EVENT_DATA): dict, -}) +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "event", + vol.Required(CONF_EVENT_TYPE): cv.string, + vol.Optional(CONF_EVENT_DATA): dict, + } +) async def async_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) - event_data_schema = vol.Schema( - config.get(CONF_EVENT_DATA), - extra=vol.ALLOW_EXTRA) if config.get(CONF_EVENT_DATA) else None + event_data_schema = ( + vol.Schema(config.get(CONF_EVENT_DATA), extra=vol.ALLOW_EXTRA) + if config.get(CONF_EVENT_DATA) + else None + ) @callback def handle_event(event): @@ -38,11 +42,11 @@ async def async_trigger(hass, config, action, automation_info): # If event data doesn't match requested schema, skip event return - hass.async_run_job(action({ - 'trigger': { - 'platform': 'event', - 'event': event, - }, - }, context=event.context)) + hass.async_run_job( + action( + {"trigger": {"platform": "event", "event": event}}, + context=event.context, + ) + ) return hass.bus.async_listen(event_type, handle_event) diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 8f838ea6d6b..7c0994c4b30 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -4,27 +4,34 @@ import voluptuous as vol from homeassistant.components.geo_location import DOMAIN from homeassistant.core import callback from homeassistant.const import ( - CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE, EVENT_STATE_CHANGED) -from homeassistant.helpers import ( - condition, config_validation as cv) + CONF_EVENT, + CONF_PLATFORM, + CONF_SOURCE, + CONF_ZONE, + EVENT_STATE_CHANGED, +) +from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain -EVENT_ENTER = 'enter' -EVENT_LEAVE = 'leave' +EVENT_ENTER = "enter" +EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER -TRIGGER_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'geo_location', - vol.Required(CONF_SOURCE): cv.string, - vol.Required(CONF_ZONE): entity_domain('zone'), - vol.Required(CONF_EVENT, default=DEFAULT_EVENT): - vol.Any(EVENT_ENTER, EVENT_LEAVE), -}) +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "geo_location", + vol.Required(CONF_SOURCE): cv.string, + vol.Required(CONF_ZONE): entity_domain("zone"), + vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Any( + EVENT_ENTER, EVENT_LEAVE + ), + } +) def source_match(state, source): """Check if the state matches the provided source.""" - return state and state.attributes.get('source') == source + return state and state.attributes.get("source") == source async def async_trigger(hass, config, action, automation_info): @@ -37,13 +44,12 @@ async def async_trigger(hass, config, action, automation_info): def state_change_listener(event): """Handle specific state changes.""" # Skip if the event is not a geo_location entity. - if not event.data.get('entity_id').startswith(DOMAIN): + if not event.data.get("entity_id").startswith(DOMAIN): return # Skip if the event's source does not match the trigger's source. - from_state = event.data.get('old_state') - to_state = event.data.get('new_state') - if not source_match(from_state, source) \ - and not source_match(to_state, source): + from_state = event.data.get("old_state") + to_state = event.data.get("new_state") + if not source_match(from_state, source) and not source_match(to_state, source): return zone_state = hass.states.get(zone_entity_id) @@ -51,18 +57,29 @@ async def async_trigger(hass, config, action, automation_info): to_match = condition.zone(hass, zone_state, to_state) # pylint: disable=too-many-boolean-expressions - if trigger_event == EVENT_ENTER and not from_match and to_match or \ - trigger_event == EVENT_LEAVE and from_match and not to_match: - hass.async_run_job(action({ - 'trigger': { - 'platform': 'geo_location', - 'source': source, - 'entity_id': event.data.get('entity_id'), - 'from_state': from_state, - 'to_state': to_state, - 'zone': zone_state, - 'event': trigger_event, - }, - }, context=event.context)) + if ( + trigger_event == EVENT_ENTER + and not from_match + and to_match + or trigger_event == EVENT_LEAVE + and from_match + and not to_match + ): + hass.async_run_job( + action( + { + "trigger": { + "platform": "geo_location", + "source": source, + "entity_id": event.data.get("entity_id"), + "from_state": from_state, + "to_state": to_state, + "zone": zone_state, + "event": trigger_event, + } + }, + context=event.context, + ) + ) return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener) diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index 1b022316676..96931e62766 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -4,17 +4,18 @@ import logging import voluptuous as vol from homeassistant.core import callback, CoreState -from homeassistant.const import ( - CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP -EVENT_START = 'start' -EVENT_SHUTDOWN = 'shutdown' +EVENT_START = "start" +EVENT_SHUTDOWN = "shutdown" _LOGGER = logging.getLogger(__name__) -TRIGGER_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'homeassistant', - vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN), -}) +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "homeassistant", + vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN), + } +) async def async_trigger(hass, config, action, automation_info): @@ -22,27 +23,24 @@ async def async_trigger(hass, config, action, automation_info): event = config.get(CONF_EVENT) if event == EVENT_SHUTDOWN: + @callback def hass_shutdown(event): """Execute when Home Assistant is shutting down.""" - hass.async_run_job(action({ - 'trigger': { - 'platform': 'homeassistant', - 'event': event, - }, - }, context=event.context)) + hass.async_run_job( + action( + {"trigger": {"platform": "homeassistant", "event": event}}, + context=event.context, + ) + ) - return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, - hass_shutdown) + return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown) # Automation are enabled while hass is starting up, fire right away # Check state because a config reload shouldn't trigger it. if hass.state == CoreState.starting: - hass.async_run_job(action({ - 'trigger': { - 'platform': 'homeassistant', - 'event': event, - }, - })) + hass.async_run_job( + action({"trigger": {"platform": "homeassistant", "event": event}}) + ) return lambda: None diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 51ec5baccfd..c642781ca66 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -11,18 +11,22 @@ from homeassistant.helpers.event import track_point_in_utc_time _LOGGER = logging.getLogger(__name__) -CONF_NUMBER = 'number' -CONF_HELD_MORE_THAN = 'held_more_than' -CONF_HELD_LESS_THAN = 'held_less_than' +CONF_NUMBER = "number" +CONF_HELD_MORE_THAN = "held_more_than" +CONF_HELD_LESS_THAN = "held_less_than" -TRIGGER_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'litejet', - vol.Required(CONF_NUMBER): cv.positive_int, - vol.Optional(CONF_HELD_MORE_THAN): - vol.All(cv.time_period, cv.positive_timedelta), - vol.Optional(CONF_HELD_LESS_THAN): - vol.All(cv.time_period, cv.positive_timedelta) -}) +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "litejet", + vol.Required(CONF_NUMBER): cv.positive_int, + vol.Optional(CONF_HELD_MORE_THAN): vol.All( + cv.time_period, cv.positive_timedelta + ), + vol.Optional(CONF_HELD_LESS_THAN): vol.All( + cv.time_period, cv.positive_timedelta + ), + } +) async def async_trigger(hass, config, action, automation_info): @@ -36,14 +40,17 @@ async def async_trigger(hass, config, action, automation_info): @callback def call_action(): """Call action with right context.""" - hass.async_run_job(action, { - 'trigger': { - CONF_PLATFORM: 'litejet', - CONF_NUMBER: number, - CONF_HELD_MORE_THAN: held_more_than, - CONF_HELD_LESS_THAN: held_less_than + hass.async_run_job( + action, + { + "trigger": { + CONF_PLATFORM: "litejet", + CONF_NUMBER: number, + CONF_HELD_MORE_THAN: held_more_than, + CONF_HELD_LESS_THAN: held_less_than, + } }, - }) + ) # held_more_than and held_less_than: trigger on released (if in time range) # held_more_than: trigger after pressed with calculation @@ -64,9 +71,8 @@ async def async_trigger(hass, config, action, automation_info): hass.add_job(call_action) if held_more_than is not None and held_less_than is None: cancel_pressed_more_than = track_point_in_utc_time( - hass, - pressed_more_than_satisfied, - dt_util.utcnow() + held_more_than) + hass, pressed_more_than_satisfied, dt_util.utcnow() + held_more_than + ) def released(): """Handle the release of the LiteJet switch's button.""" @@ -81,8 +87,8 @@ async def async_trigger(hass, config, action, automation_info): if held_more_than is None or held_time > held_more_than: hass.add_job(call_action) - hass.data['litejet_system'].on_switch_pressed(number, pressed) - hass.data['litejet_system'].on_switch_released(number, released) + hass.data["litejet_system"].on_switch_pressed(number, pressed) + hass.data["litejet_system"].on_switch_released(number, released) def async_remove(): """Remove all subscriptions used for this trigger.""" diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 837a22362b5..26c1ea5683d 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -5,19 +5,21 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components import mqtt -from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD) +from homeassistant.const import CONF_PLATFORM, CONF_PAYLOAD import homeassistant.helpers.config_validation as cv -CONF_ENCODING = 'encoding' -CONF_TOPIC = 'topic' -DEFAULT_ENCODING = 'utf-8' +CONF_ENCODING = "encoding" +CONF_TOPIC = "topic" +DEFAULT_ENCODING = "utf-8" -TRIGGER_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): mqtt.DOMAIN, - vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_PAYLOAD): cv.string, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, -}) +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): mqtt.DOMAIN, + vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PAYLOAD): cv.string, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, + } +) async def async_trigger(hass, config, action, automation_info): @@ -31,21 +33,20 @@ async def async_trigger(hass, config, action, automation_info): """Listen for MQTT messages.""" if payload is None or payload == mqttmsg.payload: data = { - 'platform': 'mqtt', - 'topic': mqttmsg.topic, - 'payload': mqttmsg.payload, - 'qos': mqttmsg.qos, + "platform": "mqtt", + "topic": mqttmsg.topic, + "payload": mqttmsg.payload, + "qos": mqttmsg.qos, } try: - data['payload_json'] = json.loads(mqttmsg.payload) + data["payload_json"] = json.loads(mqttmsg.payload) except ValueError: pass - hass.async_run_job(action, { - 'trigger': data - }) + hass.async_run_job(action, {"trigger": data}) remove = await mqtt.async_subscribe( - hass, topic, mqtt_automation_listener, encoding=encoding) + hass, topic, mqtt_automation_listener, encoding=encoding + ) return remove diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 7254914b72b..f990e599552 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -6,22 +6,33 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.core import callback from homeassistant.const import ( - CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID, - CONF_BELOW, CONF_ABOVE, CONF_FOR) -from homeassistant.helpers.event import ( - async_track_state_change, async_track_same_state) + CONF_VALUE_TEMPLATE, + CONF_PLATFORM, + CONF_ENTITY_ID, + CONF_BELOW, + CONF_ABOVE, + CONF_FOR, +) +from homeassistant.helpers.event import async_track_state_change, async_track_same_state from homeassistant.helpers import condition, config_validation as cv, template -TRIGGER_SCHEMA = vol.All(vol.Schema({ - vol.Required(CONF_PLATFORM): 'numeric_state', - vol.Required(CONF_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_BELOW): vol.Coerce(float), - vol.Optional(CONF_ABOVE): vol.Coerce(float), - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_FOR): vol.Any( - vol.All(cv.time_period, cv.positive_timedelta), - cv.template, cv.template_complex), -}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE)) +TRIGGER_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_PLATFORM): "numeric_state", + vol.Required(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_BELOW): vol.Coerce(float), + vol.Optional(CONF_ABOVE): vol.Coerce(float), + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_FOR): vol.Any( + vol.All(cv.time_period, cv.positive_timedelta), + cv.template, + cv.template_complex, + ), + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) _LOGGER = logging.getLogger(__name__) @@ -48,33 +59,40 @@ async def async_trigger(hass, config, action, automation_info): return False variables = { - 'trigger': { - 'platform': 'numeric_state', - 'entity_id': entity, - 'below': below, - 'above': above, + "trigger": { + "platform": "numeric_state", + "entity_id": entity, + "below": below, + "above": above, } } return condition.async_numeric_state( - hass, to_s, below, above, value_template, variables) + hass, to_s, below, above, value_template, variables + ) @callback def state_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" + @callback def call_action(): """Call action with right context.""" - hass.async_run_job(action({ - 'trigger': { - 'platform': 'numeric_state', - 'entity_id': entity, - 'below': below, - 'above': above, - 'from_state': from_s, - 'to_state': to_s, - 'for': time_delta if not time_delta else period[entity], - } - }, context=to_s.context)) + hass.async_run_job( + action( + { + "trigger": { + "platform": "numeric_state", + "entity_id": entity, + "below": below, + "above": above, + "from_state": from_s, + "to_state": to_s, + "for": time_delta if not time_delta else period[entity], + } + }, + context=to_s.context, + ) + ) matching = check_numeric_state(entity, from_s, to_s) @@ -85,44 +103,49 @@ async def async_trigger(hass, config, action, automation_info): if time_delta: variables = { - 'trigger': { - 'platform': 'numeric_state', - 'entity_id': entity, - 'below': below, - 'above': above, + "trigger": { + "platform": "numeric_state", + "entity_id": entity, + "below": below, + "above": above, } } try: if isinstance(time_delta, template.Template): - period[entity] = vol.All( - cv.time_period, - cv.positive_timedelta)( - time_delta.async_render(variables)) + period[entity] = vol.All(cv.time_period, cv.positive_timedelta)( + time_delta.async_render(variables) + ) elif isinstance(time_delta, dict): time_delta_data = {} time_delta_data.update( - template.render_complex(time_delta, variables)) - period[entity] = vol.All( - cv.time_period, - cv.positive_timedelta)( - time_delta_data) + template.render_complex(time_delta, variables) + ) + period[entity] = vol.All(cv.time_period, cv.positive_timedelta)( + time_delta_data + ) else: period[entity] = time_delta except (exceptions.TemplateError, vol.Invalid) as ex: - _LOGGER.error("Error rendering '%s' for template: %s", - automation_info['name'], ex) + _LOGGER.error( + "Error rendering '%s' for template: %s", + automation_info["name"], + ex, + ) entities_triggered.discard(entity) return unsub_track_same[entity] = async_track_same_state( - hass, period[entity], call_action, entity_ids=entity, - async_check_same_func=check_numeric_state) + hass, + period[entity], + call_action, + entity_ids=entity, + async_check_same_func=check_numeric_state, + ) else: call_action() - unsub = async_track_state_change( - hass, entity_id, state_automation_listener) + unsub = async_track_state_change(hass, entity_id, state_automation_listener) @callback def async_remove(): diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 9ee4ad5ac68..ccea3d9ec5a 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -7,25 +7,31 @@ from homeassistant import exceptions from homeassistant.core import callback from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.event import ( - async_track_state_change, async_track_same_state) +from homeassistant.helpers.event import async_track_state_change, async_track_same_state _LOGGER = logging.getLogger(__name__) -CONF_ENTITY_ID = 'entity_id' -CONF_FROM = 'from' -CONF_TO = 'to' +CONF_ENTITY_ID = "entity_id" +CONF_FROM = "from" +CONF_TO = "to" -TRIGGER_SCHEMA = vol.All(vol.Schema({ - vol.Required(CONF_PLATFORM): 'state', - vol.Required(CONF_ENTITY_ID): cv.entity_ids, - # These are str on purpose. Want to catch YAML conversions - vol.Optional(CONF_FROM): str, - vol.Optional(CONF_TO): str, - vol.Optional(CONF_FOR): vol.Any( - vol.All(cv.time_period, cv.positive_timedelta), - cv.template, cv.template_complex), -}), cv.key_dependency(CONF_FOR, CONF_TO)) +TRIGGER_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_PLATFORM): "state", + vol.Required(CONF_ENTITY_ID): cv.entity_ids, + # These are str on purpose. Want to catch YAML conversions + vol.Optional(CONF_FROM): str, + vol.Optional(CONF_TO): str, + vol.Optional(CONF_FOR): vol.Any( + vol.All(cv.time_period, cv.positive_timedelta), + cv.template, + cv.template_complex, + ), + } + ), + cv.key_dependency(CONF_FOR, CONF_TO), +) async def async_trigger(hass, config, action, automation_info): @@ -35,29 +41,39 @@ async def async_trigger(hass, config, action, automation_info): to_state = config.get(CONF_TO, MATCH_ALL) time_delta = config.get(CONF_FOR) template.attach(hass, time_delta) - match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL) + match_all = from_state == MATCH_ALL and to_state == MATCH_ALL unsub_track_same = {} period = {} @callback def state_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" + @callback def call_action(): """Call action with right context.""" - hass.async_run_job(action({ - 'trigger': { - 'platform': 'state', - 'entity_id': entity, - 'from_state': from_s, - 'to_state': to_s, - 'for': time_delta if not time_delta else period[entity] - } - }, context=to_s.context)) + hass.async_run_job( + action( + { + "trigger": { + "platform": "state", + "entity_id": entity, + "from_state": from_s, + "to_state": to_s, + "for": time_delta if not time_delta else period[entity], + } + }, + context=to_s.context, + ) + ) # Ignore changes to state attributes if from/to is in use - if (not match_all and from_s is not None and to_s is not None and - from_s.state == to_s.state): + if ( + not match_all + and from_s is not None + and to_s is not None + and from_s.state == to_s.state + ): return if not time_delta: @@ -65,42 +81,44 @@ async def async_trigger(hass, config, action, automation_info): return variables = { - 'trigger': { - 'platform': 'state', - 'entity_id': entity, - 'from_state': from_s, - 'to_state': to_s, + "trigger": { + "platform": "state", + "entity_id": entity, + "from_state": from_s, + "to_state": to_s, } } try: if isinstance(time_delta, template.Template): - period[entity] = vol.All( - cv.time_period, - cv.positive_timedelta)( - time_delta.async_render(variables)) + period[entity] = vol.All(cv.time_period, cv.positive_timedelta)( + time_delta.async_render(variables) + ) elif isinstance(time_delta, dict): time_delta_data = {} - time_delta_data.update( - template.render_complex(time_delta, variables)) - period[entity] = vol.All( - cv.time_period, - cv.positive_timedelta)( - time_delta_data) + time_delta_data.update(template.render_complex(time_delta, variables)) + period[entity] = vol.All(cv.time_period, cv.positive_timedelta)( + time_delta_data + ) else: period[entity] = time_delta except (exceptions.TemplateError, vol.Invalid) as ex: - _LOGGER.error("Error rendering '%s' for template: %s", - automation_info['name'], ex) + _LOGGER.error( + "Error rendering '%s' for template: %s", automation_info["name"], ex + ) return unsub_track_same[entity] = async_track_same_state( - hass, period[entity], call_action, + hass, + period[entity], + call_action, lambda _, _2, to_state: to_state.state == to_s.state, - entity_ids=entity) + entity_ids=entity, + ) unsub = async_track_state_change( - hass, entity_id, state_automation_listener, from_state, to_state) + hass, entity_id, state_automation_listener, from_state, to_state + ) @callback def async_remove(): diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 07fbf716e1c..e4d41830e0f 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -6,17 +6,23 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.const import ( - CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE) + CONF_EVENT, + CONF_OFFSET, + CONF_PLATFORM, + SUN_EVENT_SUNRISE, +) from homeassistant.helpers.event import async_track_sunrise, async_track_sunset import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -TRIGGER_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'sun', - vol.Required(CONF_EVENT): cv.sun_event, - vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period, -}) +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "sun", + vol.Required(CONF_EVENT): cv.sun_event, + vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period, + } +) async def async_trigger(hass, config, action, automation_info): @@ -27,13 +33,9 @@ async def async_trigger(hass, config, action, automation_info): @callback def call_action(): """Call action with right context.""" - hass.async_run_job(action, { - 'trigger': { - 'platform': 'sun', - 'event': event, - 'offset': offset, - }, - }) + hass.async_run_job( + action, {"trigger": {"platform": "sun", "event": event, "offset": offset}} + ) if event == SUN_EVENT_SUNRISE: return async_track_sunrise(hass, call_action, offset) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 6a60c855781..a48f252312b 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -7,19 +7,22 @@ from homeassistant.core import callback from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_FOR from homeassistant import exceptions from homeassistant.helpers import condition -from homeassistant.helpers.event import ( - async_track_same_state, async_track_template) +from homeassistant.helpers.event import async_track_same_state, async_track_template from homeassistant.helpers import config_validation as cv, template _LOGGER = logging.getLogger(__name__) -TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'template', - vol.Required(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_FOR): vol.Any( - vol.All(cv.time_period, cv.positive_timedelta), - cv.template, cv.template_complex), -}) +TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "template", + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_FOR): vol.Any( + vol.All(cv.time_period, cv.positive_timedelta), + cv.template, + cv.template_complex, + ), + } +) async def async_trigger(hass, config, action, automation_info): @@ -38,57 +41,60 @@ async def async_trigger(hass, config, action, automation_info): @callback def call_action(): """Call action with right context.""" - hass.async_run_job(action({ - 'trigger': { - 'platform': 'template', - 'entity_id': entity_id, - 'from_state': from_s, - 'to_state': to_s, - 'for': time_delta if not time_delta else period - }, - }, context=(to_s.context if to_s else None))) + hass.async_run_job( + action( + { + "trigger": { + "platform": "template", + "entity_id": entity_id, + "from_state": from_s, + "to_state": to_s, + "for": time_delta if not time_delta else period, + } + }, + context=(to_s.context if to_s else None), + ) + ) if not time_delta: call_action() return variables = { - 'trigger': { - 'platform': 'template', - 'entity_id': entity_id, - 'from_state': from_s, - 'to_state': to_s, - }, + "trigger": { + "platform": "template", + "entity_id": entity_id, + "from_state": from_s, + "to_state": to_s, + } } try: if isinstance(time_delta, template.Template): - period = vol.All( - cv.time_period, - cv.positive_timedelta)( - time_delta.async_render(variables)) + period = vol.All(cv.time_period, cv.positive_timedelta)( + time_delta.async_render(variables) + ) elif isinstance(time_delta, dict): time_delta_data = {} - time_delta_data.update( - template.render_complex(time_delta, variables)) - period = vol.All( - cv.time_period, - cv.positive_timedelta)( - time_delta_data) + time_delta_data.update(template.render_complex(time_delta, variables)) + period = vol.All(cv.time_period, cv.positive_timedelta)(time_delta_data) else: period = time_delta except (exceptions.TemplateError, vol.Invalid) as ex: - _LOGGER.error("Error rendering '%s' for template: %s", - automation_info['name'], ex) + _LOGGER.error( + "Error rendering '%s' for template: %s", automation_info["name"], ex + ) return unsub_track_same = async_track_same_state( - hass, period, call_action, + hass, + period, + call_action, lambda _, _2, _3: condition.async_template(hass, value_template), - value_template.extract_entities()) + value_template.extract_entities(), + ) - unsub = async_track_template( - hass, value_template, template_listener) + unsub = async_track_template(hass, value_template, template_listener) @callback def async_remove(): diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index ce6d6eb4446..958c1f007bc 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -10,10 +10,9 @@ from homeassistant.helpers.event import async_track_time_change _LOGGER = logging.getLogger(__name__) -TRIGGER_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'time', - vol.Required(CONF_AT): cv.time, -}) +TRIGGER_SCHEMA = vol.Schema( + {vol.Required(CONF_PLATFORM): "time", vol.Required(CONF_AT): cv.time} +) async def async_trigger(hass, config, action, automation_info): @@ -24,12 +23,8 @@ async def async_trigger(hass, config, action, automation_info): @callback def time_automation_listener(now): """Listen for time changes and calls action.""" - hass.async_run_job(action, { - 'trigger': { - 'platform': 'time', - 'now': now, - }, - }) + hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}}) - return async_track_time_change(hass, time_automation_listener, - hour=hours, minute=minutes, second=seconds) + return async_track_time_change( + hass, time_automation_listener, hour=hours, minute=minutes, second=seconds + ) diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py index da8bc9f8629..15180e07094 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -8,18 +8,23 @@ from homeassistant.const import CONF_PLATFORM from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change -CONF_HOURS = 'hours' -CONF_MINUTES = 'minutes' -CONF_SECONDS = 'seconds' +CONF_HOURS = "hours" +CONF_MINUTES = "minutes" +CONF_SECONDS = "seconds" _LOGGER = logging.getLogger(__name__) -TRIGGER_SCHEMA = vol.All(vol.Schema({ - vol.Required(CONF_PLATFORM): 'time_pattern', - CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)), - CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)), - CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)), -}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS)) +TRIGGER_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_PLATFORM): "time_pattern", + CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)), + CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)), + CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)), + } + ), + cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS), +) async def async_trigger(hass, config, action, automation_info): @@ -37,12 +42,10 @@ async def async_trigger(hass, config, action, automation_info): @callback def time_automation_listener(now): """Listen for time changes and calls action.""" - hass.async_run_job(action, { - 'trigger': { - 'platform': 'time_pattern', - 'now': now, - }, - }) + hass.async_run_job( + action, {"trigger": {"platform": "time_pattern", "now": now}} + ) - return async_track_time_change(hass, time_automation_listener, - hour=hours, minute=minutes, second=seconds) + return async_track_time_change( + hass, time_automation_listener, hour=hours, minute=minutes, second=seconds + ) diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index 37cab3cb8c0..ceb764cea96 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -11,38 +11,37 @@ import homeassistant.helpers.config_validation as cv from . import DOMAIN as AUTOMATION_DOMAIN -DEPENDENCIES = ('webhook',) +DEPENDENCIES = ("webhook",) _LOGGER = logging.getLogger(__name__) -TRIGGER_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'webhook', - vol.Required(CONF_WEBHOOK_ID): cv.string, -}) +TRIGGER_SCHEMA = vol.Schema( + {vol.Required(CONF_PLATFORM): "webhook", vol.Required(CONF_WEBHOOK_ID): cv.string} +) async def _handle_webhook(action, hass, webhook_id, request): """Handle incoming webhook.""" - result = { - 'platform': 'webhook', - 'webhook_id': webhook_id, - } + result = {"platform": "webhook", "webhook_id": webhook_id} - if 'json' in request.headers.get(hdrs.CONTENT_TYPE, ''): - result['json'] = await request.json() + if "json" in request.headers.get(hdrs.CONTENT_TYPE, ""): + result["json"] = await request.json() else: - result['data'] = await request.post() + result["data"] = await request.post() - result['query'] = request.query - hass.async_run_job(action, {'trigger': result}) + result["query"] = request.query + hass.async_run_job(action, {"trigger": result}) async def async_trigger(hass, config, action, automation_info): """Trigger based on incoming webhooks.""" webhook_id = config.get(CONF_WEBHOOK_ID) hass.components.webhook.async_register( - AUTOMATION_DOMAIN, automation_info['name'], - webhook_id, partial(_handle_webhook, action)) + AUTOMATION_DOMAIN, + automation_info["name"], + webhook_id, + partial(_handle_webhook, action), + ) @callback def unregister(): diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index e2d79eede8d..1f0f558f0de 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -3,22 +3,29 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.const import ( - CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, MATCH_ALL, CONF_PLATFORM) + CONF_EVENT, + CONF_ENTITY_ID, + CONF_ZONE, + MATCH_ALL, + CONF_PLATFORM, +) from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers import ( - condition, config_validation as cv, location) +from homeassistant.helpers import condition, config_validation as cv, location -EVENT_ENTER = 'enter' -EVENT_LEAVE = 'leave' +EVENT_ENTER = "enter" +EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER -TRIGGER_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'zone', - vol.Required(CONF_ENTITY_ID): cv.entity_ids, - vol.Required(CONF_ZONE): cv.entity_id, - vol.Required(CONF_EVENT, default=DEFAULT_EVENT): - vol.Any(EVENT_ENTER, EVENT_LEAVE), -}) +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "zone", + vol.Required(CONF_ENTITY_ID): cv.entity_ids, + vol.Required(CONF_ZONE): cv.entity_id, + vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Any( + EVENT_ENTER, EVENT_LEAVE + ), + } +) async def async_trigger(hass, config, action, automation_info): @@ -30,8 +37,11 @@ async def async_trigger(hass, config, action, automation_info): @callback def zone_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" - if from_s and not location.has_location(from_s) or \ - not location.has_location(to_s): + if ( + from_s + and not location.has_location(from_s) + or not location.has_location(to_s) + ): return zone_state = hass.states.get(zone_entity_id) @@ -42,18 +52,30 @@ async def async_trigger(hass, config, action, automation_info): to_match = condition.zone(hass, zone_state, to_s) # pylint: disable=too-many-boolean-expressions - if event == EVENT_ENTER and not from_match and to_match or \ - event == EVENT_LEAVE and from_match and not to_match: - hass.async_run_job(action({ - 'trigger': { - 'platform': 'zone', - 'entity_id': entity, - 'from_state': from_s, - 'to_state': to_s, - 'zone': zone_state, - 'event': event, - }, - }, context=to_s.context)) + if ( + event == EVENT_ENTER + and not from_match + and to_match + or event == EVENT_LEAVE + and from_match + and not to_match + ): + hass.async_run_job( + action( + { + "trigger": { + "platform": "zone", + "entity_id": entity, + "from_state": from_s, + "to_state": to_s, + "zone": zone_state, + "event": event, + } + }, + context=to_s.context, + ) + ) - return async_track_state_change(hass, entity_id, zone_automation_listener, - MATCH_ALL, MATCH_ALL) + return async_track_state_change( + hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL + ) diff --git a/homeassistant/components/avea/__init__.py b/homeassistant/components/avea/__init__.py new file mode 100644 index 00000000000..861c4f655a1 --- /dev/null +++ b/homeassistant/components/avea/__init__.py @@ -0,0 +1 @@ +"""The avea component.""" diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py new file mode 100644 index 00000000000..e6ceedcf96d --- /dev/null +++ b/homeassistant/components/avea/light.py @@ -0,0 +1,91 @@ +"""Support for the Elgato Avea lights.""" +import logging +import avea + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + Light, +) +from homeassistant.exceptions import PlatformNotReady +import homeassistant.util.color as color_util + + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_AVEA = SUPPORT_BRIGHTNESS | SUPPORT_COLOR + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Avea platform.""" + try: + nearby_bulbs = avea.discover_avea_bulbs() + for bulb in nearby_bulbs: + bulb.get_name() + bulb.get_brightness() + except OSError as err: + raise PlatformNotReady from err + + add_entities(AveaLight(bulb) for bulb in nearby_bulbs) + + +class AveaLight(Light): + """Representation of an Avea.""" + + def __init__(self, light): + """Initialize an AveaLight.""" + self._light = light + self._name = light.name + self._state = None + self._brightness = light.brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_AVEA + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def is_on(self): + """Return true if light is on.""" + return self._state + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + if not kwargs: + self._light.set_brightness(4095) + else: + if ATTR_BRIGHTNESS in kwargs: + bright = round((kwargs[ATTR_BRIGHTNESS] / 255) * 4095) + self._light.set_brightness(bright) + if ATTR_HS_COLOR in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + self._light.set_rgb(rgb[0], rgb[1], rgb[2]) + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.set_brightness(0) + + def update(self): + """Fetch new state data for this light. + + This is the only method that should fetch new data for Home Assistant. + """ + brightness = self._light.get_brightness() + if brightness is not None: + if brightness == 0: + self._state = False + else: + self._state = True + self._brightness = round(255 * (brightness / 4095)) diff --git a/homeassistant/components/avea/manifest.json b/homeassistant/components/avea/manifest.json new file mode 100644 index 00000000000..273cefcbcfa --- /dev/null +++ b/homeassistant/components/avea/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "avea", + "name": "Elgato Avea", + "documentation": "https://www.home-assistant.io/components/avea", + "dependencies": [], + "codeowners": ["@pattyland"], + "requirements": ["avea==1.2.8"] +} \ No newline at end of file diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index b138b8bf61f..0c95b2bf736 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -6,38 +6,50 @@ import time import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light) + ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + Light, +) from homeassistant.const import ( - CONF_API_KEY, CONF_DEVICES, CONF_ID, CONF_NAME, CONF_PASSWORD, - CONF_USERNAME) + CONF_API_KEY, + CONF_DEVICES, + CONF_ID, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) SUPPORT_AVION_LED = SUPPORT_BRIGHTNESS -DEVICE_SCHEMA = vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_ID): cv.positive_int, - vol.Optional(CONF_NAME): cv.string, -}) +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_ID): cv.positive_int, + vol.Optional(CONF_NAME): cv.string, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up an Avion switch.""" # pylint: disable=no-member - avion = importlib.import_module('avion') + avion = importlib.import_module("avion") lights = [] if CONF_USERNAME in config and CONF_PASSWORD in config: - devices = avion.get_devices( - config[CONF_USERNAME], config[CONF_PASSWORD]) + devices = avion.get_devices(config[CONF_USERNAME], config[CONF_PASSWORD]) for device in devices: lights.append(AvionLight(device)) @@ -47,7 +59,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): passphrase=device_config[CONF_API_KEY], name=device_config.get(CONF_NAME), object_id=device_config.get(CONF_ID), - connect=False) + connect=False, + ) lights.append(AvionLight(device)) add_entities(lights) @@ -102,7 +115,7 @@ class AvionLight(Light): def set_state(self, brightness): """Set the state of this lamp to the provided brightness.""" # pylint: disable=no-member - avion = importlib.import_module('avion') + avion = importlib.import_module("avion") # Bluetooth LE is unreliable, and the connection may drop at any # time. Make an effort to re-establish the link. diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 71b74c7971e..85b5a0be191 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -7,8 +7,12 @@ import math import voluptuous as vol from homeassistant.const import ( - CONF_ACCESS_TOKEN, CONF_DEVICES, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) + CONF_ACCESS_TOKEN, + CONF_DEVICES, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, +) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -17,48 +21,64 @@ from homeassistant.util import Throttle, dt _LOGGER = logging.getLogger(__name__) -ATTR_SCORE = 'score' -ATTR_TIMESTAMP = 'timestamp' -ATTR_LAST_API_UPDATE = 'last_api_update' -ATTR_COMPONENT = 'component' -ATTR_VALUE = 'value' -ATTR_SENSORS = 'sensors' +ATTR_SCORE = "score" +ATTR_TIMESTAMP = "timestamp" +ATTR_LAST_API_UPDATE = "last_api_update" +ATTR_COMPONENT = "component" +ATTR_VALUE = "value" +ATTR_SENSORS = "sensors" -CONF_UUID = 'uuid' +CONF_UUID = "uuid" -DEVICE_CLASS_PM2_5 = 'PM2.5' -DEVICE_CLASS_PM10 = 'PM10' -DEVICE_CLASS_CARBON_DIOXIDE = 'CO2' -DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = 'VOC' -DEVICE_CLASS_SCORE = 'score' +DEVICE_CLASS_PM2_5 = "PM2.5" +DEVICE_CLASS_PM10 = "PM10" +DEVICE_CLASS_CARBON_DIOXIDE = "CO2" +DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "VOC" +DEVICE_CLASS_SCORE = "score" SENSOR_TYPES = { - 'TEMP': {'device_class': DEVICE_CLASS_TEMPERATURE, - 'unit_of_measurement': TEMP_CELSIUS, - 'icon': 'mdi:thermometer'}, - 'HUMID': {'device_class': DEVICE_CLASS_HUMIDITY, - 'unit_of_measurement': '%', - 'icon': 'mdi:water-percent'}, - 'CO2': {'device_class': DEVICE_CLASS_CARBON_DIOXIDE, - 'unit_of_measurement': 'ppm', - 'icon': 'mdi:periodic-table-co2'}, - 'VOC': {'device_class': DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - 'unit_of_measurement': 'ppb', - 'icon': 'mdi:cloud'}, + "TEMP": { + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": TEMP_CELSIUS, + "icon": "mdi:thermometer", + }, + "HUMID": { + "device_class": DEVICE_CLASS_HUMIDITY, + "unit_of_measurement": "%", + "icon": "mdi:water-percent", + }, + "CO2": { + "device_class": DEVICE_CLASS_CARBON_DIOXIDE, + "unit_of_measurement": "ppm", + "icon": "mdi:periodic-table-co2", + }, + "VOC": { + "device_class": DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + "unit_of_measurement": "ppb", + "icon": "mdi:cloud", + }, # Awair docs don't actually specify the size they measure for 'dust', # but 2.5 allows the sensor to show up in HomeKit - 'DUST': {'device_class': DEVICE_CLASS_PM2_5, - 'unit_of_measurement': 'µg/m3', - 'icon': 'mdi:cloud'}, - 'PM25': {'device_class': DEVICE_CLASS_PM2_5, - 'unit_of_measurement': 'µg/m3', - 'icon': 'mdi:cloud'}, - 'PM10': {'device_class': DEVICE_CLASS_PM10, - 'unit_of_measurement': 'µg/m3', - 'icon': 'mdi:cloud'}, - 'score': {'device_class': DEVICE_CLASS_SCORE, - 'unit_of_measurement': '%', - 'icon': 'mdi:percent'}, + "DUST": { + "device_class": DEVICE_CLASS_PM2_5, + "unit_of_measurement": "µg/m3", + "icon": "mdi:cloud", + }, + "PM25": { + "device_class": DEVICE_CLASS_PM2_5, + "unit_of_measurement": "µg/m3", + "icon": "mdi:cloud", + }, + "PM10": { + "device_class": DEVICE_CLASS_PM10, + "unit_of_measurement": "µg/m3", + "icon": "mdi:cloud", + }, + "score": { + "device_class": DEVICE_CLASS_SCORE, + "unit_of_measurement": "%", + "icon": "mdi:percent", + }, } AWAIR_QUOTA = 300 @@ -67,15 +87,14 @@ AWAIR_QUOTA = 300 # Don't bother asking us for state more often than that. SCAN_INTERVAL = timedelta(minutes=5) -AWAIR_DEVICE_SCHEMA = vol.Schema({ - vol.Required(CONF_UUID): cv.string, -}) +AWAIR_DEVICE_SCHEMA = vol.Schema({vol.Required(CONF_UUID): cv.string}) -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_DEVICES): vol.All( - cv.ensure_list, [AWAIR_DEVICE_SCHEMA]), -}) +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [AWAIR_DEVICE_SCHEMA]), + } +) # Awair *heavily* throttles calls that get user information, @@ -84,8 +103,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ # list of devices, and they may provide the same set of information # that the devices() call would return. However, the only thing # used at this time is the `uuid` value. -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Connect to the Awair API and find devices.""" from python_awair import AwairClient @@ -106,8 +124,7 @@ async def async_setup_platform(hass, config, async_add_entities, await awair_data.async_update() for sensor in SENSOR_TYPES: if sensor in awair_data.data: - awair_sensor = AwairSensor(awair_data, device, - sensor, throttle) + awair_sensor = AwairSensor(awair_data, device, sensor, throttle) all_devices.append(awair_sensor) async_add_entities(all_devices, True) @@ -116,8 +133,11 @@ async def async_setup_platform(hass, config, async_add_entities, _LOGGER.error("Awair API access_token invalid") except AwairClient.RatelimitError: _LOGGER.error("Awair API ratelimit exceeded.") - except (AwairClient.QueryError, AwairClient.NotFoundError, - AwairClient.GenericError) as error: + except ( + AwairClient.QueryError, + AwairClient.NotFoundError, + AwairClient.GenericError, + ) as error: _LOGGER.error("Unexpected Awair API error: %s", error) raise PlatformNotReady @@ -129,9 +149,9 @@ class AwairSensor(Entity): def __init__(self, data, device, sensor_type, throttle): """Initialize the sensor.""" self._uuid = device[CONF_UUID] - self._device_class = SENSOR_TYPES[sensor_type]['device_class'] - self._name = 'Awair {}'.format(self._device_class) - unit = SENSOR_TYPES[sensor_type]['unit_of_measurement'] + self._device_class = SENSOR_TYPES[sensor_type]["device_class"] + self._name = "Awair {}".format(self._device_class) + unit = SENSOR_TYPES[sensor_type]["unit_of_measurement"] self._unit_of_measurement = unit self._data = data self._type = sensor_type @@ -150,7 +170,7 @@ class AwairSensor(Entity): @property def icon(self): """Icon to use in the frontend.""" - return SENSOR_TYPES[self._type]['icon'] + return SENSOR_TYPES[self._type]["icon"] @property def state(self): diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index 5b9978fb3e6..1959cc05e80 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -39,11 +39,9 @@ AWS_CREDENTIAL_SCHEMA = vol.Schema( } ) -DEFAULT_CREDENTIAL = [{ - CONF_NAME: "default", - CONF_PROFILE_NAME: "default", - CONF_VALIDATE: False, -}] +DEFAULT_CREDENTIAL = [ + {CONF_NAME: "default", CONF_PROFILE_NAME: "default", CONF_VALIDATE: False} +] SUPPORTED_SERVICES = ["lambda", "sns", "sqs"] @@ -66,9 +64,9 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Optional( - CONF_CREDENTIALS, default=DEFAULT_CREDENTIAL - ): vol.All(cv.ensure_list, [AWS_CREDENTIAL_SCHEMA]), + vol.Optional(CONF_CREDENTIALS, default=DEFAULT_CREDENTIAL): vol.All( + cv.ensure_list, [AWS_CREDENTIAL_SCHEMA] + ), vol.Optional(CONF_NOTIFY, default=[]): vol.All( cv.ensure_list, [NOTIFY_PLATFORM_SCHEMA] ), @@ -111,9 +109,7 @@ async def async_setup_entry(hass, entry): if entry.source == config_entries.SOURCE_IMPORT: if conf is None: # user removed config from configuration.yaml, abort setup - hass.async_create_task( - hass.config_entries.async_remove(entry.entry_id) - ) + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) return False if conf != entry.data: @@ -147,9 +143,7 @@ async def async_setup_entry(hass, entry): # have to use discovery to load platform. for notify_config in conf[CONF_NOTIFY]: hass.async_create_task( - discovery.async_load_platform( - hass, "notify", DOMAIN, notify_config, config - ) + discovery.async_load_platform(hass, "notify", DOMAIN, notify_config, config) ) return validation diff --git a/homeassistant/components/aws/config_flow.py b/homeassistant/components/aws/config_flow.py index c21f2a94137..6ac332b251c 100644 --- a/homeassistant/components/aws/config_flow.py +++ b/homeassistant/components/aws/config_flow.py @@ -17,6 +17,4 @@ class AWSFlowHandler(config_entries.ConfigFlow): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - return self.async_create_entry( - title="configuration.yaml", data=user_input - ) + return self.async_create_entry(title="configuration.yaml", data=user_input) diff --git a/homeassistant/components/aws/const.py b/homeassistant/components/aws/const.py index 4738547bdec..499f4413596 100644 --- a/homeassistant/components/aws/const.py +++ b/homeassistant/components/aws/const.py @@ -8,7 +8,7 @@ DATA_SESSIONS = "aws_sessions" CONF_ACCESS_KEY_ID = "aws_access_key_id" CONF_CONTEXT = "context" CONF_CREDENTIAL_NAME = "credential_name" -CONF_CREDENTIALS = 'credentials' +CONF_CREDENTIALS = "credentials" CONF_NOTIFY = "notify" CONF_PROFILE_NAME = "profile_name" CONF_REGION = "region_name" diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index 4b71ae425cb..fa1cf3fa363 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -32,15 +32,13 @@ async def get_available_regions(hass, service): # get_available_regions is not a coroutine since it does not perform # network I/O. But it still perform file I/O heavily, so put it into # an executor thread to unblock event loop - return await hass.async_add_executor_job( - session.get_available_regions, service - ) + return await hass.async_add_executor_job(session.get_available_regions, service) async def async_get_service(hass, config, discovery_info=None): """Get the AWS notification service.""" if discovery_info is None: - _LOGGER.error('Please config aws notify platform in aws component') + _LOGGER.error("Please config aws notify platform in aws component") return None import aiobotocore @@ -56,7 +54,9 @@ async def async_get_service(hass, config, discovery_info=None): if region_name not in available_regions: _LOGGER.error( "Region %s is not available for %s service, must in %s", - region_name, service, available_regions + region_name, + service, + available_regions, ) return None @@ -76,9 +76,7 @@ async def async_get_service(hass, config, discovery_info=None): if hass.data[DATA_SESSIONS]: session = next(iter(hass.data[DATA_SESSIONS].values())) else: - _LOGGER.error( - "Missing aws credential for %s", config[CONF_NAME] - ) + _LOGGER.error("Missing aws credential for %s", config[CONF_NAME]) return None if session is None: @@ -86,9 +84,7 @@ async def async_get_service(hass, config, discovery_info=None): if credential_name is not None: session = hass.data[DATA_SESSIONS].get(credential_name) if session is None: - _LOGGER.warning( - "No available aws session for %s", credential_name - ) + _LOGGER.warning("No available aws session for %s", credential_name) del aws_config[CONF_CREDENTIAL_NAME] if session is None: @@ -150,7 +146,7 @@ class AWSLambda(AWSNotify): json_payload = json.dumps(payload) async with self.session.create_client( - self.service, **self.aws_config + self.service, **self.aws_config ) as client: tasks = [] for target in kwargs.get(ATTR_TARGET, []): @@ -185,7 +181,7 @@ class AWSSNS(AWSNotify): subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) async with self.session.create_client( - self.service, **self.aws_config + self.service, **self.aws_config ) as client: tasks = [] for target in kwargs.get(ATTR_TARGET, []): @@ -225,7 +221,7 @@ class AWSSQS(AWSNotify): } async with self.session.create_client( - self.service, **self.aws_config + self.service, **self.aws_config ) as client: tasks = [] for target in kwargs.get(ATTR_TARGET, []): diff --git a/homeassistant/components/axis/.translations/bg.json b/homeassistant/components/axis/.translations/bg.json new file mode 100644 index 00000000000..b8c5bf7609f --- /dev/null +++ b/homeassistant/components/axis/.translations/bg.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "bad_config_file": "\u041b\u043e\u0448\u0438 \u0434\u0430\u043d\u043d\u0438 \u043e\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u044f \u0444\u0430\u0439\u043b", + "link_local_address": "\u041b\u043e\u043a\u0430\u043b\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u0442", + "not_axis_device": "\u041e\u0442\u043a\u0440\u0438\u0442\u043e\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0435 Axis" + }, + "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "already_in_progress": "\u0412 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u0442\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", + "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u043d\u0430\u043b\u0438\u0447\u043d\u043e", + "faulty_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438" + }, + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442 Axis" + } + }, + "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/da.json b/homeassistant/components/axis/.translations/da.json index 4657d2fb355..2d728468fc7 100644 --- a/homeassistant/components/axis/.translations/da.json +++ b/homeassistant/components/axis/.translations/da.json @@ -1,10 +1,14 @@ { "config": { "abort": { - "already_configured": "Enheden er allerede konfigureret" + "already_configured": "Enheden er allerede konfigureret", + "bad_config_file": "Forkerte data fra konfigurationsfilen", + "link_local_address": "Link lokale adresser underst\u00f8ttes ikke", + "not_axis_device": "Fundet enhed ikke en Axis enhed" }, "error": { "already_configured": "Enheden er allerede konfigureret", + "already_in_progress": "Enheds konfiguration er allerede i gang.", "device_unavailable": "Enheden er ikke tilg\u00e6ngelig", "faulty_credentials": "Ugyldige legitimationsoplysninger" }, diff --git a/homeassistant/components/axis/.translations/es.json b/homeassistant/components/axis/.translations/es.json index 9229b90866f..817737eee04 100644 --- a/homeassistant/components/axis/.translations/es.json +++ b/homeassistant/components/axis/.translations/es.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "bad_config_file": "Datos err\u00f3neos en el archivo de configuraci\u00f3n", - "link_local_address": "Las direcciones de enlace locales no son compatibles" + "link_local_address": "Las direcciones de enlace locales no son compatibles", + "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", diff --git a/homeassistant/components/axis/.translations/pt-BR.json b/homeassistant/components/axis/.translations/pt-BR.json index 4126d99e2e2..0b8fe8541da 100644 --- a/homeassistant/components/axis/.translations/pt-BR.json +++ b/homeassistant/components/axis/.translations/pt-BR.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "bad_config_file": "Dados incorretos do arquivo de configura\u00e7\u00e3o", + "link_local_address": "Link de endere\u00e7os locais n\u00e3o s\u00e3o suportados", "not_axis_device": "Dispositivo descoberto n\u00e3o \u00e9 um dispositivo Axis" }, "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o para o dispositivo j\u00e1 est\u00e1 em andamento.", + "device_unavailable": "O dispositivo n\u00e3o est\u00e1 dispon\u00edvel", "faulty_credentials": "Credenciais do usu\u00e1rio inv\u00e1lidas" }, "step": { @@ -14,7 +19,8 @@ "password": "Senha", "port": "Porta", "username": "Nome de usu\u00e1rio" - } + }, + "title": "Configurar o dispositivo Axis" } }, "title": "Dispositivo Axis" diff --git a/homeassistant/components/axis/.translations/zh-Hans.json b/homeassistant/components/axis/.translations/zh-Hans.json new file mode 100644 index 00000000000..f7f6c8259ce --- /dev/null +++ b/homeassistant/components/axis/.translations/zh-Hans.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", + "username": "\u7528\u6237\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index e9e8a158a3b..bdda82b2145 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -4,17 +4,21 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( - CONF_DEVICE, CONF_MAC, CONF_NAME, CONF_TRIGGER_TIME, - EVENT_HOMEASSISTANT_STOP) + CONF_DEVICE, + CONF_MAC, + CONF_NAME, + CONF_TRIGGER_TIME, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers import config_validation as cv from .config_flow import DEVICE_SCHEMA from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN from .device import AxisNetworkDevice, get_device -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys(DEVICE_SCHEMA), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: cv.schema_with_slug_keys(DEVICE_SCHEMA)}, extra=vol.ALLOW_EXTRA +) async def async_setup(hass, config): @@ -26,10 +30,13 @@ async def async_setup(hass, config): if CONF_NAME not in device_config: device_config[CONF_NAME] = device_name - hass.async_create_task(hass.config_entries.flow.async_init( - DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, - data=device_config - )) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=device_config, + ) + ) return True @@ -72,7 +79,7 @@ async def async_populate_options(hass, config_entry): options = { CONF_CAMERA: camera, CONF_EVENTS: True, - CONF_TRIGGER_TIME: DEFAULT_TRIGGER_TIME + CONF_TRIGGER_TIME: DEFAULT_TRIGGER_TIME, } hass.config_entries.async_update_entry(config_entry, options=options) diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 9a8f53c8bde..3864ac344e1 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -17,8 +17,11 @@ class AxisEntityBase(Entity): async def async_added_to_hass(self): """Subscribe device events.""" - self.unsub_dispatcher.append(async_dispatcher_connect( - self.hass, self.device.event_reachable, self.update_callback)) + self.unsub_dispatcher.append( + async_dispatcher_connect( + self.hass, self.device.event_reachable, self.update_callback + ) + ) async def async_will_remove_from_hass(self) -> None: """Unsubscribe device events when removed.""" @@ -33,9 +36,7 @@ class AxisEntityBase(Entity): @property def device_info(self): """Return a device description for device registry.""" - return { - 'identifiers': {(AXIS_DOMAIN, self.device.serial)} - } + return {"identifiers": {(AXIS_DOMAIN, self.device.serial)}} @callback def update_callback(self, no_delay=None): @@ -71,8 +72,7 @@ class AxisEventBase(AxisEntityBase): @property def name(self): """Return the name of the event.""" - return '{} {} {}'.format( - self.device.name, self.event.TYPE, self.event.id) + return "{} {} {}".format(self.device.name, self.event.TYPE, self.event.id) @property def should_poll(self): @@ -82,5 +82,4 @@ class AxisEventBase(AxisEntityBase): @property def unique_id(self): """Return a unique identifier for this device.""" - return '{}-{}-{}'.format( - self.device.serial, self.event.topic, self.event.id) + return "{}-{}-{}".format(self.device.serial, self.event.topic, self.event.id) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 86a2a738b70..1d12e0b8d61 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -28,8 +28,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if event.CLASS != CLASS_OUTPUT: async_add_entities([AxisBinarySensor(event, device)], True) - device.listeners.append(async_dispatcher_connect( - hass, device.event_new_sensor, async_add_sensor)) + device.listeners.append( + async_dispatcher_connect(hass, device.event_new_sensor, async_add_sensor) + ) class AxisBinarySensor(AxisEventBase, BinarySensorDevice): @@ -63,8 +64,8 @@ class AxisBinarySensor(AxisEventBase, BinarySensorDevice): self.remove_timer = None self.remove_timer = async_track_point_in_utc_time( - self.hass, _delay_update, - utcnow() + timedelta(seconds=delay)) + self.hass, _delay_update, utcnow() + timedelta(seconds=delay) + ) @property def is_on(self): @@ -74,10 +75,13 @@ class AxisBinarySensor(AxisEventBase, BinarySensorDevice): @property def name(self): """Return the name of the event.""" - if self.event.CLASS == CLASS_INPUT and self.event.id and \ - self.device.api.vapix.ports[self.event.id].name: - return '{} {}'.format( - self.device.name, - self.device.api.vapix.ports[self.event.id].name) + if ( + self.event.CLASS == CLASS_INPUT + and self.event.id + and self.device.api.vapix.ports[self.event.id].name + ): + return "{} {}".format( + self.device.name, self.device.api.vapix.ports[self.event.id].name + ) return super().name diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index c993e9d9f64..e7e0f7459f3 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -2,18 +2,30 @@ from homeassistant.components.camera import SUPPORT_STREAM from homeassistant.components.mjpeg.camera import ( - CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera, filter_urllib3_logging) + CONF_MJPEG_URL, + CONF_STILL_IMAGE_URL, + MjpegCamera, + filter_urllib3_logging, +) from homeassistant.const import ( - CONF_AUTHENTICATION, CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME, - CONF_PASSWORD, CONF_PORT, CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION) + CONF_AUTHENTICATION, + CONF_DEVICE, + CONF_HOST, + CONF_MAC, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + HTTP_DIGEST_AUTHENTICATION, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from .axis_base import AxisEntityBase from .const import DOMAIN as AXIS_DOMAIN -AXIS_IMAGE = 'http://{}:{}/axis-cgi/jpg/image.cgi' -AXIS_VIDEO = 'http://{}:{}/axis-cgi/mjpg/video.cgi' -AXIS_STREAM = 'rtsp://{}:{}@{}/axis-media/media.amp?videocodec=h264' +AXIS_IMAGE = "http://{}:{}/axis-cgi/jpg/image.cgi" +AXIS_VIDEO = "http://{}:{}/axis-cgi/mjpg/video.cgi" +AXIS_STREAM = "rtsp://{}:{}@{}/axis-media/media.amp?videocodec=h264" async def async_setup_entry(hass, config_entry, async_add_entities): @@ -29,10 +41,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): CONF_PASSWORD: config_entry.data[CONF_DEVICE][CONF_PASSWORD], CONF_MJPEG_URL: AXIS_VIDEO.format( config_entry.data[CONF_DEVICE][CONF_HOST], - config_entry.data[CONF_DEVICE][CONF_PORT]), + config_entry.data[CONF_DEVICE][CONF_PORT], + ), CONF_STILL_IMAGE_URL: AXIS_IMAGE.format( config_entry.data[CONF_DEVICE][CONF_HOST], - config_entry.data[CONF_DEVICE][CONF_PORT]), + config_entry.data[CONF_DEVICE][CONF_PORT], + ), CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, } async_add_entities([AxisCamera(config, device)]) @@ -48,8 +62,11 @@ class AxisCamera(AxisEntityBase, MjpegCamera): async def async_added_to_hass(self): """Subscribe camera events.""" - self.unsub_dispatcher.append(async_dispatcher_connect( - self.hass, self.device.event_new_address, self._new_address)) + self.unsub_dispatcher.append( + async_dispatcher_connect( + self.hass, self.device.event_new_address, self._new_address + ) + ) await super().async_added_to_hass() @@ -63,7 +80,8 @@ class AxisCamera(AxisEntityBase, MjpegCamera): return AXIS_STREAM.format( self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME], self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD], - self.device.host) + self.device.host, + ) def _new_address(self): """Set new device address for video stream.""" @@ -74,4 +92,4 @@ class AxisCamera(AxisEntityBase, MjpegCamera): @property def unique_id(self): """Return a unique identifier for this device.""" - return '{}-camera'.format(self.device.serial) + return "{}-camera".format(self.device.serial) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 410fb62c139..4b54982244b 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -4,8 +4,14 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( - CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_USERNAME) + CONF_DEVICE, + CONF_HOST, + CONF_MAC, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.util.json import load_json @@ -14,36 +20,40 @@ from .const import CONF_MODEL, DOMAIN from .device import get_device from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect -AXIS_OUI = {'00408C', 'ACCC8E', 'B8A44F'} +AXIS_OUI = {"00408C", "ACCC8E", "B8A44F"} -CONFIG_FILE = 'axis.conf' +CONFIG_FILE = "axis.conf" -EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound', - 'daynight', 'tampering', 'input'] +EVENT_TYPES = ["motion", "vmd3", "pir", "sound", "daynight", "tampering", "input"] -PLATFORMS = ['camera'] +PLATFORMS = ["camera"] AXIS_INCLUDE = EVENT_TYPES + PLATFORMS -AXIS_DEFAULT_HOST = '192.168.0.90' -AXIS_DEFAULT_USERNAME = 'root' -AXIS_DEFAULT_PASSWORD = 'pass' +AXIS_DEFAULT_HOST = "192.168.0.90" +AXIS_DEFAULT_USERNAME = "root" +AXIS_DEFAULT_PASSWORD = "pass" DEFAULT_PORT = 80 -DEVICE_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string, - vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}, extra=vol.ALLOW_EXTRA) +DEVICE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string, + vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + }, + extra=vol.ALLOW_EXTRA, +) @callback def configured_devices(hass): """Return a set of the configured devices.""" - return {entry.data[CONF_MAC]: entry for entry - in hass.config_entries.async_entries(DOMAIN)} + return { + entry.data[CONF_MAC]: entry + for entry in hass.config_entries.async_entries(DOMAIN) + } @config_entries.HANDLERS.register(DOMAIN) @@ -76,7 +86,7 @@ class AxisFlowHandler(config_entries.ConfigFlow): CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT], CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD] + CONF_PASSWORD: user_input[CONF_PASSWORD], } device = await get_device(self.hass, self.device_config) @@ -90,26 +100,30 @@ class AxisFlowHandler(config_entries.ConfigFlow): return await self._create_entry() except AlreadyConfigured: - errors['base'] = 'already_configured' + errors["base"] = "already_configured" except AuthenticationRequired: - errors['base'] = 'faulty_credentials' + errors["base"] = "faulty_credentials" except CannotConnect: - errors['base'] = 'device_unavailable' + errors["base"] = "device_unavailable" - data = self.import_schema or self.discovery_schema or { - vol.Required(CONF_HOST): str, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_PORT, default=DEFAULT_PORT): int - } + data = ( + self.import_schema + or self.discovery_schema + or { + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + ) return self.async_show_form( - step_id='user', + step_id="user", description_placeholders=self.device_config, data_schema=vol.Schema(data), - errors=errors + errors=errors, ) async def _create_entry(self): @@ -119,8 +133,8 @@ class AxisFlowHandler(config_entries.ConfigFlow): """ if self.name is None: same_model = [ - entry.data[CONF_NAME] for entry - in self.hass.config_entries.async_entries(DOMAIN) + entry.data[CONF_NAME] + for entry in self.hass.config_entries.async_entries(DOMAIN) if entry.data[CONF_MODEL] == self.model ] @@ -138,10 +152,7 @@ class AxisFlowHandler(config_entries.ConfigFlow): } title = "{} - {}".format(self.model, self.serial_number) - return self.async_create_entry( - title=title, - data=data - ) + return self.async_create_entry(title=title, data=data) async def _update_entry(self, entry, host): """Update existing entry if it is the same device.""" @@ -153,38 +164,40 @@ class AxisFlowHandler(config_entries.ConfigFlow): This flow is triggered by the discovery component. """ - serialnumber = discovery_info['properties']['macaddress'] + serialnumber = discovery_info["properties"]["macaddress"] if serialnumber[:6] not in AXIS_OUI: - return self.async_abort(reason='not_axis_device') + return self.async_abort(reason="not_axis_device") - if discovery_info[CONF_HOST].startswith('169.254'): - return self.async_abort(reason='link_local_address') + if discovery_info[CONF_HOST].startswith("169.254"): + return self.async_abort(reason="link_local_address") # pylint: disable=unsupported-assignment-operation - self.context['macaddress'] = serialnumber + self.context["macaddress"] = serialnumber - if any(serialnumber == flow['context']['macaddress'] - for flow in self._async_in_progress()): - return self.async_abort(reason='already_in_progress') + if any( + serialnumber == flow["context"]["macaddress"] + for flow in self._async_in_progress() + ): + return self.async_abort(reason="already_in_progress") device_entries = configured_devices(self.hass) if serialnumber in device_entries: entry = device_entries[serialnumber] await self._update_entry(entry, discovery_info[CONF_HOST]) - return self.async_abort(reason='already_configured') + return self.async_abort(reason="already_configured") config_file = await self.hass.async_add_executor_job( - load_json, self.hass.config.path(CONFIG_FILE)) + load_json, self.hass.config.path(CONFIG_FILE) + ) if serialnumber not in config_file: self.discovery_schema = { - vol.Required( - CONF_HOST, default=discovery_info[CONF_HOST]): str, + vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_PORT, default=discovery_info[CONF_PORT]): int + vol.Required(CONF_PORT, default=discovery_info[CONF_PORT]): int, } return await self.async_step_user() @@ -193,10 +206,10 @@ class AxisFlowHandler(config_entries.ConfigFlow): device_config[CONF_HOST] = discovery_info[CONF_HOST] if CONF_NAME not in device_config: - device_config[CONF_NAME] = discovery_info['hostname'] + device_config[CONF_NAME] = discovery_info["hostname"] except vol.Invalid: - return self.async_abort(reason='bad_config_file') + return self.async_abort(reason="bad_config_file") return await self.async_step_import(device_config) @@ -213,10 +226,8 @@ class AxisFlowHandler(config_entries.ConfigFlow): self.import_schema = { vol.Required(CONF_HOST, default=import_config[CONF_HOST]): str, - vol.Required( - CONF_USERNAME, default=import_config[CONF_USERNAME]): str, - vol.Required( - CONF_PASSWORD, default=import_config[CONF_PASSWORD]): str, - vol.Required(CONF_PORT, default=import_config[CONF_PORT]): int + vol.Required(CONF_USERNAME, default=import_config[CONF_USERNAME]): str, + vol.Required(CONF_PASSWORD, default=import_config[CONF_PASSWORD]): str, + vol.Required(CONF_PORT, default=import_config[CONF_PORT]): int, } return await self.async_step_user(user_input=import_config) diff --git a/homeassistant/components/axis/const.py b/homeassistant/components/axis/const.py index 5e730708591..7f0fd9c8947 100644 --- a/homeassistant/components/axis/const.py +++ b/homeassistant/components/axis/const.py @@ -3,10 +3,10 @@ import logging LOGGER = logging.getLogger(__package__) -DOMAIN = 'axis' +DOMAIN = "axis" -CONF_CAMERA = 'camera' -CONF_EVENTS = 'events' -CONF_MODEL = 'model' +CONF_CAMERA = "camera" +CONF_EVENTS = "events" +CONF_MODEL = "model" DEFAULT_TRIGGER_TIME = 0 diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 32c5ac090e9..465d8c73b74 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -4,8 +4,14 @@ import asyncio import async_timeout from homeassistant.const import ( - CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_USERNAME) + CONF_DEVICE, + CONF_HOST, + CONF_MAC, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -53,30 +59,27 @@ class AxisNetworkDevice: async def async_update_device_registry(self): """Update device registry.""" - device_registry = await \ - self.hass.helpers.device_registry.async_get_registry() + device_registry = await self.hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, connections={(CONNECTION_NETWORK_MAC, self.serial)}, identifiers={(DOMAIN, self.serial)}, - manufacturer='Axis Communications AB', + manufacturer="Axis Communications AB", model="{} {}".format(self.model, self.product_type), name=self.name, - sw_version=self.fw_version + sw_version=self.fw_version, ) async def async_setup(self): """Set up the device.""" try: - self.api = await get_device( - self.hass, self.config_entry.data[CONF_DEVICE]) + self.api = await get_device(self.hass, self.config_entry.data[CONF_DEVICE]) except CannotConnect: raise ConfigEntryNotReady except Exception: # pylint: disable=broad-except - LOGGER.error( - 'Unknown error connecting with Axis device on %s', self.host) + LOGGER.error("Unknown error connecting with Axis device on %s", self.host) return False self.fw_version = self.api.vapix.params.firmware_version @@ -86,18 +89,22 @@ class AxisNetworkDevice: self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( - self.config_entry, 'camera')) + self.config_entry, "camera" + ) + ) if self.config_entry.options[CONF_EVENTS]: - self.api.stream.connection_status_callback = \ + self.api.stream.connection_status_callback = ( self.async_connection_status_callback + ) self.api.enable_events(event_callback=self.async_event_callback) platform_tasks = [ self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform) - for platform in ['binary_sensor', 'switch'] + self.config_entry, platform + ) + for platform in ["binary_sensor", "switch"] ] self.hass.async_create_task(self.start(platform_tasks)) @@ -108,7 +115,7 @@ class AxisNetworkDevice: @property def event_new_address(self): """Device specific event to signal new device address.""" - return 'axis_new_address_{}'.format(self.serial) + return "axis_new_address_{}".format(self.serial) @staticmethod async def async_new_address_callback(hass, entry): @@ -124,7 +131,7 @@ class AxisNetworkDevice: @property def event_reachable(self): """Device specific event to signal a change in connection status.""" - return 'axis_reachable_{}'.format(self.serial) + return "axis_reachable_{}".format(self.serial) @callback def async_connection_status_callback(self, status): @@ -134,6 +141,7 @@ class AxisNetworkDevice: Only signal state change if state change is true. """ from axis.streammanager import SIGNAL_PLAYING + if self.available != (status == SIGNAL_PLAYING): self.available = not self.available async_dispatcher_send(self.hass, self.event_reachable, True) @@ -141,12 +149,12 @@ class AxisNetworkDevice: @property def event_new_sensor(self): """Device specific event to signal new sensor available.""" - return 'axis_add_sensor_{}'.format(self.serial) + return "axis_add_sensor_{}".format(self.serial) @callback def async_event_callback(self, action, event_id): """Call to configure events when initialized on event stream.""" - if action == 'add': + if action == "add": async_dispatcher_send(self.hass, self.event_new_sensor, event_id) async def start(self, platform_tasks): @@ -166,14 +174,17 @@ class AxisNetworkDevice: if self.config_entry.options[CONF_CAMERA]: platform_tasks.append( self.hass.config_entries.async_forward_entry_unload( - self.config_entry, 'camera')) + self.config_entry, "camera" + ) + ) if self.config_entry.options[CONF_EVENTS]: self.api.stop() platform_tasks += [ self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform) - for platform in ['binary_sensor', 'switch'] + self.config_entry, platform + ) + for platform in ["binary_sensor", "switch"] ] await asyncio.gather(*platform_tasks) @@ -190,10 +201,13 @@ async def get_device(hass, config): import axis device = axis.AxisDevice( - loop=hass.loop, host=config[CONF_HOST], + loop=hass.loop, + host=config[CONF_HOST], username=config[CONF_USERNAME], password=config[CONF_PASSWORD], - port=config[CONF_PORT], web_proto='http') + port=config[CONF_PORT], + web_proto="http", + ) device.vapix.initialize_params(preload_data=False) device.vapix.initialize_ports() @@ -202,28 +216,23 @@ async def get_device(hass, config): with async_timeout.timeout(15): await asyncio.gather( - hass.async_add_executor_job( - device.vapix.params.update_brand), - - hass.async_add_executor_job( - device.vapix.params.update_properties), - - hass.async_add_executor_job( - device.vapix.ports.update) + hass.async_add_executor_job(device.vapix.params.update_brand), + hass.async_add_executor_job(device.vapix.params.update_properties), + hass.async_add_executor_job(device.vapix.ports.update), ) return device except axis.Unauthorized: - LOGGER.warning("Connected to device at %s but not registered.", - config[CONF_HOST]) + LOGGER.warning( + "Connected to device at %s but not registered.", config[CONF_HOST] + ) raise AuthenticationRequired except (asyncio.TimeoutError, axis.RequestError): - LOGGER.error("Error connecting to the Axis device at %s", - config[CONF_HOST]) + LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST]) raise CannotConnect except axis.AxisException: - LOGGER.exception('Unknown Axis communication error occurred') + LOGGER.exception("Unknown Axis communication error occurred") raise AuthenticationRequired diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index 852528120a5..a64ffc3fa85 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -24,8 +24,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if event.CLASS == CLASS_OUTPUT: async_add_entities([AxisSwitch(event, device)], True) - device.listeners.append(async_dispatcher_connect( - hass, device.event_new_sensor, async_add_switch)) + device.listeners.append( + async_dispatcher_connect(hass, device.event_new_sensor, async_add_switch) + ) class AxisSwitch(AxisEventBase, SwitchDevice): @@ -38,22 +39,24 @@ class AxisSwitch(AxisEventBase, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn on switch.""" - action = '/' + action = "/" await self.hass.async_add_executor_job( - self.device.api.vapix.ports[self.event.id].action, action) + self.device.api.vapix.ports[self.event.id].action, action + ) async def async_turn_off(self, **kwargs): """Turn off switch.""" - action = '\\' + action = "\\" await self.hass.async_add_executor_job( - self.device.api.vapix.ports[self.event.id].action, action) + self.device.api.vapix.ports[self.event.id].action, action + ) @property def name(self): """Return the name of the event.""" if self.event.id and self.device.api.vapix.ports[self.event.id].name: - return '{} {}'.format( - self.device.name, - self.device.api.vapix.ports[self.event.id].name) + return "{} {}".format( + self.device.name, self.device.api.vapix.ports[self.event.id].name + ) return super().name diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index c5362fe1821..371b8d1ea8d 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -7,8 +7,11 @@ import voluptuous as vol from azure.eventhub import EventData, EventHubClientAsync from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_UNAVAILABLE, - STATE_UNKNOWN) + EVENT_HOMEASSISTANT_STOP, + EVENT_STATE_CHANGED, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA @@ -16,23 +19,28 @@ from homeassistant.helpers.json import JSONEncoder _LOGGER = logging.getLogger(__name__) -DOMAIN = 'azure_event_hub' +DOMAIN = "azure_event_hub" -CONF_EVENT_HUB_NAMESPACE = 'event_hub_namespace' -CONF_EVENT_HUB_INSTANCE_NAME = 'event_hub_instance_name' -CONF_EVENT_HUB_SAS_POLICY = 'event_hub_sas_policy' -CONF_EVENT_HUB_SAS_KEY = 'event_hub_sas_key' -CONF_FILTER = 'filter' +CONF_EVENT_HUB_NAMESPACE = "event_hub_namespace" +CONF_EVENT_HUB_INSTANCE_NAME = "event_hub_instance_name" +CONF_EVENT_HUB_SAS_POLICY = "event_hub_sas_policy" +CONF_EVENT_HUB_SAS_KEY = "event_hub_sas_key" +CONF_FILTER = "filter" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_EVENT_HUB_NAMESPACE): cv.string, - vol.Required(CONF_EVENT_HUB_INSTANCE_NAME): cv.string, - vol.Required(CONF_EVENT_HUB_SAS_POLICY): cv.string, - vol.Required(CONF_EVENT_HUB_SAS_KEY): cv.string, - vol.Required(CONF_FILTER): FILTER_SCHEMA, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_EVENT_HUB_NAMESPACE): cv.string, + vol.Required(CONF_EVENT_HUB_INSTANCE_NAME): cv.string, + vol.Required(CONF_EVENT_HUB_SAS_POLICY): cv.string, + vol.Required(CONF_EVENT_HUB_SAS_KEY): cv.string, + vol.Required(CONF_FILTER): FILTER_SCHEMA, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): @@ -40,15 +48,16 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): config = yaml_config[DOMAIN] event_hub_address = "amqps://{}.servicebus.windows.net/{}".format( - config[CONF_EVENT_HUB_NAMESPACE], - config[CONF_EVENT_HUB_INSTANCE_NAME]) + config[CONF_EVENT_HUB_NAMESPACE], config[CONF_EVENT_HUB_INSTANCE_NAME] + ) entities_filter = config[CONF_FILTER] client = EventHubClientAsync( event_hub_address, debug=True, username=config[CONF_EVENT_HUB_SAS_POLICY], - password=config[CONF_EVENT_HUB_SAS_KEY]) + password=config[CONF_EVENT_HUB_SAS_KEY], + ) async_sender = client.add_async_sender() await client.run_async() @@ -56,17 +65,16 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): async def async_send_to_event_hub(event: Event): """Send states to Event Hub.""" - state = event.data.get('new_state') - if (state is None - or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE) - or not entities_filter(state.entity_id)): + state = event.data.get("new_state") + if ( + state is None + or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) + or not entities_filter(state.entity_id) + ): return event_data = EventData( - json.dumps( - obj=state.as_dict(), - default=encoder.encode - ).encode('utf-8') + json.dumps(obj=state.as_dict(), default=encoder.encode).encode("utf-8") ) await async_sender.send(event_data) diff --git a/homeassistant/components/baidu/tts.py b/homeassistant/components/baidu/tts.py index faf62e92651..85737d1affd 100644 --- a/homeassistant/components/baidu/tts.py +++ b/homeassistant/components/baidu/tts.py @@ -9,49 +9,46 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -SUPPORTED_LANGUAGES = ['zh'] -DEFAULT_LANG = 'zh' +SUPPORTED_LANGUAGES = ["zh"] +DEFAULT_LANG = "zh" -CONF_APP_ID = 'app_id' -CONF_SECRET_KEY = 'secret_key' -CONF_SPEED = 'speed' -CONF_PITCH = 'pitch' -CONF_VOLUME = 'volume' -CONF_PERSON = 'person' +CONF_APP_ID = "app_id" +CONF_SECRET_KEY = "secret_key" +CONF_SPEED = "speed" +CONF_PITCH = "pitch" +CONF_VOLUME = "volume" +CONF_PERSON = "person" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), - vol.Required(CONF_APP_ID): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_SECRET_KEY): cv.string, - vol.Optional(CONF_SPEED, default=5): vol.All( - vol.Coerce(int), vol.Range(min=0, max=9) - ), - vol.Optional(CONF_PITCH, default=5): vol.All( - vol.Coerce(int), vol.Range(min=0, max=9) - ), - vol.Optional(CONF_VOLUME, default=5): vol.All( - vol.Coerce(int), vol.Range(min=0, max=15) - ), - vol.Optional(CONF_PERSON, default=0): vol.All( - vol.Coerce(int), vol.Range(min=0, max=4) - ), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), + vol.Required(CONF_APP_ID): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_SECRET_KEY): cv.string, + vol.Optional(CONF_SPEED, default=5): vol.All( + vol.Coerce(int), vol.Range(min=0, max=9) + ), + vol.Optional(CONF_PITCH, default=5): vol.All( + vol.Coerce(int), vol.Range(min=0, max=9) + ), + vol.Optional(CONF_VOLUME, default=5): vol.All( + vol.Coerce(int), vol.Range(min=0, max=15) + ), + vol.Optional(CONF_PERSON, default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=4) + ), + } +) # Keys are options in the config file, and Values are options # required by Baidu TTS API. _OPTIONS = { - CONF_PERSON: 'per', - CONF_PITCH: 'pit', - CONF_SPEED: 'spd', - CONF_VOLUME: 'vol', + CONF_PERSON: "per", + CONF_PITCH: "pit", + CONF_SPEED: "spd", + CONF_VOLUME: "vol", } -SUPPORTED_OPTIONS = [ - CONF_PERSON, - CONF_PITCH, - CONF_SPEED, - CONF_VOLUME, -] +SUPPORTED_OPTIONS = [CONF_PERSON, CONF_PITCH, CONF_SPEED, CONF_VOLUME] def get_engine(hass, config): @@ -66,13 +63,13 @@ class BaiduTTSProvider(Provider): """Init Baidu TTS service.""" self.hass = hass self._lang = conf.get(CONF_LANG) - self._codec = 'mp3' - self.name = 'BaiduTTS' + self._codec = "mp3" + self.name = "BaiduTTS" self._app_data = { - 'appid': conf.get(CONF_APP_ID), - 'apikey': conf.get(CONF_API_KEY), - 'secretkey': conf.get(CONF_SECRET_KEY), + "appid": conf.get(CONF_APP_ID), + "apikey": conf.get(CONF_API_KEY), + "secretkey": conf.get(CONF_SECRET_KEY), } self._speech_conf_data = { @@ -110,31 +107,28 @@ class BaiduTTSProvider(Provider): def get_tts_audio(self, message, language, options=None): """Load TTS from BaiduTTS.""" from aip import AipSpeech + aip_speech = AipSpeech( - self._app_data['appid'], - self._app_data['apikey'], - self._app_data['secretkey'] + self._app_data["appid"], + self._app_data["apikey"], + self._app_data["secretkey"], ) if options is None: - result = aip_speech.synthesis( - message, language, 1, self._speech_conf_data - ) + result = aip_speech.synthesis(message, language, 1, self._speech_conf_data) else: speech_data = self._speech_conf_data.copy() for key, value in options.items(): speech_data[_OPTIONS[key]] = value - result = aip_speech.synthesis( - message, language, 1, speech_data - ) + result = aip_speech.synthesis(message, language, 1, speech_data) if isinstance(result, dict): _LOGGER.error( "Baidu TTS error-- err_no:%d; err_msg:%s; err_detail:%s", - result['err_no'], - result['err_msg'], - result['err_detail'] + result["err_no"], + result["err_msg"], + result["err_detail"], ) return None, None diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 6b2395ef6d2..acefc5a3b26 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -3,66 +3,87 @@ from collections import OrderedDict import voluptuous as vol -from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( - CONF_ABOVE, CONF_BELOW, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME, - CONF_PLATFORM, CONF_STATE, CONF_VALUE_TEMPLATE, STATE_UNKNOWN) + CONF_ABOVE, + CONF_BELOW, + CONF_DEVICE_CLASS, + CONF_ENTITY_ID, + CONF_NAME, + CONF_PLATFORM, + CONF_STATE, + CONF_VALUE_TEMPLATE, + STATE_UNKNOWN, +) from homeassistant.core import callback from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change -ATTR_OBSERVATIONS = 'observations' -ATTR_PROBABILITY = 'probability' -ATTR_PROBABILITY_THRESHOLD = 'probability_threshold' +ATTR_OBSERVATIONS = "observations" +ATTR_PROBABILITY = "probability" +ATTR_PROBABILITY_THRESHOLD = "probability_threshold" -CONF_OBSERVATIONS = 'observations' -CONF_PRIOR = 'prior' +CONF_OBSERVATIONS = "observations" +CONF_PRIOR = "prior" CONF_TEMPLATE = "template" -CONF_PROBABILITY_THRESHOLD = 'probability_threshold' -CONF_P_GIVEN_F = 'prob_given_false' -CONF_P_GIVEN_T = 'prob_given_true' -CONF_TO_STATE = 'to_state' +CONF_PROBABILITY_THRESHOLD = "probability_threshold" +CONF_P_GIVEN_F = "prob_given_false" +CONF_P_GIVEN_T = "prob_given_true" +CONF_TO_STATE = "to_state" DEFAULT_NAME = "Bayesian Binary Sensor" DEFAULT_PROBABILITY_THRESHOLD = 0.5 -NUMERIC_STATE_SCHEMA = vol.Schema({ - CONF_PLATFORM: 'numeric_state', - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Optional(CONF_ABOVE): vol.Coerce(float), - vol.Optional(CONF_BELOW): vol.Coerce(float), - vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float) -}, required=True) +NUMERIC_STATE_SCHEMA = vol.Schema( + { + CONF_PLATFORM: "numeric_state", + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_ABOVE): vol.Coerce(float), + vol.Optional(CONF_BELOW): vol.Coerce(float), + vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), + }, + required=True, +) -STATE_SCHEMA = vol.Schema({ - CONF_PLATFORM: CONF_STATE, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TO_STATE): cv.string, - vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float) -}, required=True) +STATE_SCHEMA = vol.Schema( + { + CONF_PLATFORM: CONF_STATE, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TO_STATE): cv.string, + vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), + }, + required=True, +) -TEMPLATE_SCHEMA = vol.Schema({ - CONF_PLATFORM: CONF_TEMPLATE, - vol.Required(CONF_VALUE_TEMPLATE): cv.template, - vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float) -}, required=True) +TEMPLATE_SCHEMA = vol.Schema( + { + CONF_PLATFORM: CONF_TEMPLATE, + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), + }, + required=True, +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): cv.string, - vol.Required(CONF_OBSERVATIONS): - vol.Schema(vol.All(cv.ensure_list, - [vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA, - TEMPLATE_SCHEMA)])), - vol.Required(CONF_PRIOR): vol.Coerce(float), - vol.Optional(CONF_PROBABILITY_THRESHOLD, - default=DEFAULT_PROBABILITY_THRESHOLD): vol.Coerce(float), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): cv.string, + vol.Required(CONF_OBSERVATIONS): vol.Schema( + vol.All( + cv.ensure_list, + [vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA, TEMPLATE_SCHEMA)], + ) + ), + vol.Required(CONF_PRIOR): vol.Coerce(float), + vol.Optional( + CONF_PROBABILITY_THRESHOLD, default=DEFAULT_PROBABILITY_THRESHOLD + ): vol.Coerce(float), + } +) def update_probability(prior, prob_true, prob_false): @@ -73,8 +94,7 @@ def update_probability(prior, prob_true, prob_false): return probability -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Bayesian Binary sensor.""" name = config.get(CONF_NAME) observations = config.get(CONF_OBSERVATIONS) @@ -82,17 +102,20 @@ async def async_setup_platform(hass, config, async_add_entities, probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD) device_class = config.get(CONF_DEVICE_CLASS) - async_add_entities([ - BayesianBinarySensor( - name, prior, observations, probability_threshold, device_class) - ], True) + async_add_entities( + [ + BayesianBinarySensor( + name, prior, observations, probability_threshold, device_class + ) + ], + True, + ) class BayesianBinarySensor(BinarySensorDevice): """Representation of a Bayesian sensor.""" - def __init__(self, name, prior, observations, probability_threshold, - device_class): + def __init__(self, name, prior, observations, probability_threshold, device_class): """Initialize the Bayesian sensor.""" self._name = name self._observations = observations @@ -106,32 +129,31 @@ class BayesianBinarySensor(BinarySensorDevice): to_observe = set() for obs in self._observations: - if 'entity_id' in obs: - to_observe.update(set([obs.get('entity_id')])) - if 'value_template' in obs: - to_observe.update( - set(obs.get(CONF_VALUE_TEMPLATE).extract_entities())) + if "entity_id" in obs: + to_observe.update(set([obs.get("entity_id")])) + if "value_template" in obs: + to_observe.update(set(obs.get(CONF_VALUE_TEMPLATE).extract_entities())) self.entity_obs = dict.fromkeys(to_observe, []) for ind, obs in enumerate(self._observations): - obs['id'] = ind - if 'entity_id' in obs: - self.entity_obs[obs['entity_id']].append(obs) - if 'value_template' in obs: + obs["id"] = ind + if "entity_id" in obs: + self.entity_obs[obs["entity_id"]].append(obs) + if "value_template" in obs: for ent in obs.get(CONF_VALUE_TEMPLATE).extract_entities(): self.entity_obs[ent].append(obs) self.watchers = { - 'numeric_state': self._process_numeric_state, - 'state': self._process_state, - 'template': self._process_template + "numeric_state": self._process_numeric_state, + "state": self._process_state, + "template": self._process_template, } async def async_added_to_hass(self): """Call when entity about to be added.""" + @callback - def async_threshold_sensor_state_listener(entity, old_state, - new_state): + def async_threshold_sensor_state_listener(entity, old_state, new_state): """Handle sensor state changes.""" if new_state.state == STATE_UNKNOWN: return @@ -139,33 +161,32 @@ class BayesianBinarySensor(BinarySensorDevice): entity_obs_list = self.entity_obs[entity] for entity_obs in entity_obs_list: - platform = entity_obs['platform'] + platform = entity_obs["platform"] self.watchers[platform](entity_obs) prior = self.prior for obs in self.current_obs.values(): - prior = update_probability( - prior, obs['prob_true'], obs['prob_false']) + prior = update_probability(prior, obs["prob_true"], obs["prob_false"]) self.probability = prior self.hass.async_add_job(self.async_update_ha_state, True) async_track_state_change( - self.hass, self.entity_obs, async_threshold_sensor_state_listener) + self.hass, self.entity_obs, async_threshold_sensor_state_listener + ) def _update_current_obs(self, entity_observation, should_trigger): """Update current observation.""" - obs_id = entity_observation['id'] + obs_id = entity_observation["id"] if should_trigger: - prob_true = entity_observation['prob_given_true'] - prob_false = entity_observation.get( - 'prob_given_false', 1 - prob_true) + prob_true = entity_observation["prob_given_true"] + prob_false = entity_observation.get("prob_given_false", 1 - prob_true) self.current_obs[obs_id] = { - 'prob_true': prob_true, - 'prob_false': prob_false + "prob_true": prob_true, + "prob_false": prob_false, } else: @@ -173,21 +194,26 @@ class BayesianBinarySensor(BinarySensorDevice): def _process_numeric_state(self, entity_observation): """Add entity to current_obs if numeric state conditions are met.""" - entity = entity_observation['entity_id'] + entity = entity_observation["entity_id"] should_trigger = condition.async_numeric_state( - self.hass, entity, - entity_observation.get('below'), - entity_observation.get('above'), None, entity_observation) + self.hass, + entity, + entity_observation.get("below"), + entity_observation.get("above"), + None, + entity_observation, + ) self._update_current_obs(entity_observation, should_trigger) def _process_state(self, entity_observation): """Add entity to current observations if state conditions are met.""" - entity = entity_observation['entity_id'] + entity = entity_observation["entity_id"] should_trigger = condition.state( - self.hass, entity, entity_observation.get('to_state')) + self.hass, entity, entity_observation.get("to_state") + ) self._update_current_obs(entity_observation, should_trigger) @@ -196,7 +222,8 @@ class BayesianBinarySensor(BinarySensorDevice): template = entity_observation.get(CONF_VALUE_TEMPLATE) template.hass = self.hass should_trigger = condition.async_template( - self.hass, template, entity_observation) + self.hass, template, entity_observation + ) self._update_current_obs(entity_observation, should_trigger) @property diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py index 85ea5753739..bfaa2a7c50d 100644 --- a/homeassistant/components/bbb_gpio/__init__.py +++ b/homeassistant/components/bbb_gpio/__init__.py @@ -1,12 +1,11 @@ """Support for controlling GPIO pins of a Beaglebone Black.""" import logging -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) -DOMAIN = 'bbb_gpio' +DOMAIN = "bbb_gpio" def setup(hass, config): @@ -30,6 +29,7 @@ def setup_output(pin): """Set up a GPIO as output.""" # pylint: disable=import-error from Adafruit_BBIO import GPIO + GPIO.setup(pin, GPIO.OUT) @@ -37,15 +37,15 @@ def setup_input(pin, pull_mode): """Set up a GPIO as input.""" # pylint: disable=import-error from Adafruit_BBIO import GPIO - GPIO.setup(pin, GPIO.IN, - GPIO.PUD_DOWN if pull_mode == 'DOWN' - else GPIO.PUD_UP) + + GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP) def write_output(pin, value): """Write a value to a GPIO.""" # pylint: disable=import-error from Adafruit_BBIO import GPIO + GPIO.output(pin, value) @@ -53,6 +53,7 @@ def read_input(pin): """Read a value from a GPIO.""" # pylint: disable=import-error from Adafruit_BBIO import GPIO + return GPIO.input(pin) is GPIO.HIGH @@ -60,5 +61,5 @@ def edge_detect(pin, event_callback, bounce): """Add detection for RISING and FALLING events.""" # pylint: disable=import-error from Adafruit_BBIO import GPIO - GPIO.add_event_detect( - pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) + + GPIO.add_event_detect(pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/bbb_gpio/binary_sensor.py b/homeassistant/components/bbb_gpio/binary_sensor.py index bcc45a4af32..105015da720 100644 --- a/homeassistant/components/bbb_gpio/binary_sensor.py +++ b/homeassistant/components/bbb_gpio/binary_sensor.py @@ -4,34 +4,33 @@ import logging import voluptuous as vol from homeassistant.components import bbb_gpio -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME) +from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_PINS = 'pins' -CONF_BOUNCETIME = 'bouncetime' -CONF_INVERT_LOGIC = 'invert_logic' -CONF_PULL_MODE = 'pull_mode' +CONF_PINS = "pins" +CONF_BOUNCETIME = "bouncetime" +CONF_INVERT_LOGIC = "invert_logic" +CONF_PULL_MODE = "pull_mode" DEFAULT_BOUNCETIME = 50 DEFAULT_INVERT_LOGIC = False -DEFAULT_PULL_MODE = 'UP' +DEFAULT_PULL_MODE = "UP" -PIN_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): - vol.In(['UP', 'DOWN']) -}) +PIN_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.In(["UP", "DOWN"]), + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PINS, default={}): - vol.Schema({cv.string: PIN_SCHEMA}), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA})} +) def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/bbb_gpio/switch.py b/homeassistant/components/bbb_gpio/switch.py index 49b4c5de19c..45f95609758 100644 --- a/homeassistant/components/bbb_gpio/switch.py +++ b/homeassistant/components/bbb_gpio/switch.py @@ -5,25 +5,27 @@ import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.components import bbb_gpio -from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME) +from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_PINS = 'pins' -CONF_INITIAL = 'initial' -CONF_INVERT_LOGIC = 'invert_logic' +CONF_PINS = "pins" +CONF_INITIAL = "initial" +CONF_INVERT_LOGIC = "invert_logic" -PIN_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_INITIAL, default=False): cv.boolean, - vol.Optional(CONF_INVERT_LOGIC, default=False): cv.boolean, -}) +PIN_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL, default=False): cv.boolean, + vol.Optional(CONF_INVERT_LOGIC, default=False): cv.boolean, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA}), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA})} +) def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index f70969aa61b..b565d05685f 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -6,7 +6,10 @@ import logging import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -14,13 +17,13 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = '192.168.1.254' +DEFAULT_HOST = "192.168.1.254" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string} +) def get_scanner(hass, config): @@ -30,7 +33,7 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update']) +Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) class BboxDeviceScanner(DeviceScanner): @@ -56,8 +59,9 @@ class BboxDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - filter_named = [result.name for result in self.last_results if - result.mac == device] + filter_named = [ + result.name for result in self.last_results if result.mac == device + ] if filter_named: return filter_named[0] @@ -79,11 +83,13 @@ class BboxDeviceScanner(DeviceScanner): now = dt_util.now() last_results = [] for device in result: - if device['active'] != 1: + if device["active"] != 1: continue last_results.append( - Device(device['macaddress'], device['hostname'], - device['ipaddress'], now)) + Device( + device["macaddress"], device["hostname"], device["ipaddress"], now + ) + ) self.last_results = last_results diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 80fa82b30fc..76621b7792b 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -7,38 +7,52 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, CONF_MONITORED_VARIABLES, ATTR_ATTRIBUTION) +from homeassistant.const import CONF_NAME, CONF_MONITORED_VARIABLES, ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -BANDWIDTH_MEGABITS_SECONDS = 'Mb/s' # type: str +BANDWIDTH_MEGABITS_SECONDS = "Mb/s" # type: str ATTRIBUTION = "Powered by Bouygues Telecom" -DEFAULT_NAME = 'Bbox' +DEFAULT_NAME = "Bbox" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) # Sensor types are defined like so: Name, unit, icon SENSOR_TYPES = { - 'down_max_bandwidth': ['Maximum Download Bandwidth', - BANDWIDTH_MEGABITS_SECONDS, 'mdi:download'], - 'up_max_bandwidth': ['Maximum Upload Bandwidth', - BANDWIDTH_MEGABITS_SECONDS, 'mdi:upload'], - 'current_down_bandwidth': ['Currently Used Download Bandwidth', - BANDWIDTH_MEGABITS_SECONDS, 'mdi:download'], - 'current_up_bandwidth': ['Currently Used Upload Bandwidth', - BANDWIDTH_MEGABITS_SECONDS, 'mdi:upload'], + "down_max_bandwidth": [ + "Maximum Download Bandwidth", + BANDWIDTH_MEGABITS_SECONDS, + "mdi:download", + ], + "up_max_bandwidth": [ + "Maximum Upload Bandwidth", + BANDWIDTH_MEGABITS_SECONDS, + "mdi:upload", + ], + "current_down_bandwidth": [ + "Currently Used Download Bandwidth", + BANDWIDTH_MEGABITS_SECONDS, + "mdi:download", + ], + "current_up_bandwidth": [ + "Currently Used Upload Bandwidth", + BANDWIDTH_MEGABITS_SECONDS, + "mdi:upload", + ], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_VARIABLES): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MONITORED_VARIABLES): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -77,7 +91,7 @@ class BboxSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self.client_name, self._name) + return "{} {}".format(self.client_name, self._name) @property def state(self): @@ -97,25 +111,19 @@ class BboxSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - } + return {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Get the latest data from Bbox and update the state.""" self.bbox_data.update() - if self.type == 'down_max_bandwidth': - self._state = round( - self.bbox_data.data['rx']['maxBandwidth'] / 1000, 2) - elif self.type == 'up_max_bandwidth': - self._state = round( - self.bbox_data.data['tx']['maxBandwidth'] / 1000, 2) - elif self.type == 'current_down_bandwidth': - self._state = round(self.bbox_data.data['rx']['bandwidth'] / 1000, - 2) - elif self.type == 'current_up_bandwidth': - self._state = round(self.bbox_data.data['tx']['bandwidth'] / 1000, - 2) + if self.type == "down_max_bandwidth": + self._state = round(self.bbox_data.data["rx"]["maxBandwidth"] / 1000, 2) + elif self.type == "up_max_bandwidth": + self._state = round(self.bbox_data.data["tx"]["maxBandwidth"] / 1000, 2) + elif self.type == "current_down_bandwidth": + self._state = round(self.bbox_data.data["rx"]["bandwidth"] / 1000, 2) + elif self.type == "current_up_bandwidth": + self._state = round(self.bbox_data.data["tx"]["bandwidth"] / 1000, 2) class BboxData: diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index eaee023ce86..0a305c21adb 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -11,12 +11,12 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_I2C_ADDRESS = 'i2c_address' -CONF_I2C_BUS = 'i2c_bus' -CONF_OPERATION_MODE = 'operation_mode' -CONF_SENSITIVITY = 'sensitivity' -CONF_DELAY = 'measurement_delay_ms' -CONF_MULTIPLIER = 'multiplier' +CONF_I2C_ADDRESS = "i2c_address" +CONF_I2C_BUS = "i2c_bus" +CONF_OPERATION_MODE = "operation_mode" +CONF_SENSITIVITY = "sensitivity" +CONF_DELAY = "measurement_delay_ms" +CONF_MULTIPLIER = "multiplier" # Operation modes for BH1750 sensor (from the datasheet). Time typically 120ms # In one time measurements, device is set to Power Down after each sample. @@ -29,35 +29,36 @@ ONE_TIME_HIGH_RES_MODE_2 = "one_time_high_res_mode_2" OPERATION_MODES = { CONTINUOUS_LOW_RES_MODE: (0x13, True), # 4lx resolution CONTINUOUS_HIGH_RES_MODE_1: (0x10, True), # 1lx resolution. - CONTINUOUS_HIGH_RES_MODE_2: (0X11, True), # 0.5lx resolution. + CONTINUOUS_HIGH_RES_MODE_2: (0x11, True), # 0.5lx resolution. ONE_TIME_LOW_RES_MODE: (0x23, False), # 4lx resolution. ONE_TIME_HIGH_RES_MODE_1: (0x20, False), # 1lx resolution. ONE_TIME_HIGH_RES_MODE_2: (0x21, False), # 0.5lx resolution. } -SENSOR_UNIT = 'lx' -DEFAULT_NAME = 'BH1750 Light Sensor' -DEFAULT_I2C_ADDRESS = '0x23' +SENSOR_UNIT = "lx" +DEFAULT_NAME = "BH1750 Light Sensor" +DEFAULT_I2C_ADDRESS = "0x23" DEFAULT_I2C_BUS = 1 DEFAULT_MODE = CONTINUOUS_HIGH_RES_MODE_1 DEFAULT_DELAY_MS = 120 DEFAULT_SENSITIVITY = 69 # from 31 to 254 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.string, - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), - vol.Optional(CONF_OPERATION_MODE, default=DEFAULT_MODE): - vol.In(OPERATION_MODES), - vol.Optional(CONF_SENSITIVITY, default=DEFAULT_SENSITIVITY): - cv.positive_int, - vol.Optional(CONF_DELAY, default=DEFAULT_DELAY_MS): cv.positive_int, - vol.Optional(CONF_MULTIPLIER, default=1.): vol.Range(min=0.1, max=10), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.string, + vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), + vol.Optional(CONF_OPERATION_MODE, default=DEFAULT_MODE): vol.In( + OPERATION_MODES + ), + vol.Optional(CONF_SENSITIVITY, default=DEFAULT_SENSITIVITY): cv.positive_int, + vol.Optional(CONF_DELAY, default=DEFAULT_DELAY_MS): cv.positive_int, + vol.Optional(CONF_MULTIPLIER, default=1.0): vol.Range(min=0.1, max=10), + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BH1750 sensor.""" import smbus # pylint: disable=import-error from i2csense.bh1750 import BH1750 # pylint: disable=import-error @@ -70,20 +71,26 @@ async def async_setup_platform(hass, config, async_add_entities, bus = smbus.SMBus(bus_number) sensor = await hass.async_add_job( - partial(BH1750, bus, i2c_address, - operation_mode=operation_mode, - measurement_delay=config.get(CONF_DELAY), - sensitivity=config.get(CONF_SENSITIVITY), - logger=_LOGGER) + partial( + BH1750, + bus, + i2c_address, + operation_mode=operation_mode, + measurement_delay=config.get(CONF_DELAY), + sensitivity=config.get(CONF_SENSITIVITY), + logger=_LOGGER, + ) ) if not sensor.sample_ok: _LOGGER.error("BH1750 sensor not detected at %s", i2c_address) return False - dev = [BH1750Sensor(sensor, name, SENSOR_UNIT, - config.get(CONF_MULTIPLIER))] - _LOGGER.info("Setup of BH1750 light sensor at %s in mode %s is complete", - i2c_address, operation_mode) + dev = [BH1750Sensor(sensor, name, SENSOR_UNIT, config.get(CONF_MULTIPLIER))] + _LOGGER.info( + "Setup of BH1750 light sensor at %s in mode %s is complete", + i2c_address, + operation_mode, + ) async_add_entities(dev, True) @@ -91,7 +98,7 @@ async def async_setup_platform(hass, config, async_add_entities, class BH1750Sensor(Entity): """Implementation of the BH1750 sensor.""" - def __init__(self, bh1750_sensor, name, unit, multiplier=1.): + def __init__(self, bh1750_sensor, name, unit, multiplier=1.0): """Initialize the sensor.""" self._name = name self._unit_of_measurement = unit @@ -125,10 +132,9 @@ class BH1750Sensor(Entity): async def async_update(self): """Get the latest data from the BH1750 and update the states.""" await self.hass.async_add_job(self.bh1750_sensor.update) - if self.bh1750_sensor.sample_ok \ - and self.bh1750_sensor.light_level >= 0: - self._state = int(round(self.bh1750_sensor.light_level - * self._multiplier)) + if self.bh1750_sensor.sample_ok and self.bh1750_sensor.light_level >= 0: + self._state = int(round(self.bh1750_sensor.light_level * self._multiplier)) else: - _LOGGER.warning("Bad Update of sensor.%s: %s", - self.name, self.bh1750_sensor.light_level) + _LOGGER.warning( + "Bad Update of sensor.%s: %s", self.name, self.bh1750_sensor.light_level + ) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 19054588ee7..951f4a423e5 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -7,83 +7,85 @@ import voluptuous as vol from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity -from homeassistant.const import (STATE_ON, STATE_OFF) +from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.helpers.config_validation import ( # noqa - PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) -DOMAIN = 'binary_sensor' +DOMAIN = "binary_sensor" SCAN_INTERVAL = timedelta(seconds=30) -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" # On means low, Off means normal -DEVICE_CLASS_BATTERY = 'battery' +DEVICE_CLASS_BATTERY = "battery" # On means cold, Off means normal -DEVICE_CLASS_COLD = 'cold' +DEVICE_CLASS_COLD = "cold" # On means connected, Off means disconnected -DEVICE_CLASS_CONNECTIVITY = 'connectivity' +DEVICE_CLASS_CONNECTIVITY = "connectivity" # On means open, Off means closed -DEVICE_CLASS_DOOR = 'door' +DEVICE_CLASS_DOOR = "door" # On means open, Off means closed -DEVICE_CLASS_GARAGE_DOOR = 'garage_door' +DEVICE_CLASS_GARAGE_DOOR = "garage_door" # On means gas detected, Off means no gas (clear) -DEVICE_CLASS_GAS = 'gas' +DEVICE_CLASS_GAS = "gas" # On means hot, Off means normal -DEVICE_CLASS_HEAT = 'heat' +DEVICE_CLASS_HEAT = "heat" # On means light detected, Off means no light -DEVICE_CLASS_LIGHT = 'light' +DEVICE_CLASS_LIGHT = "light" # On means open (unlocked), Off means closed (locked) -DEVICE_CLASS_LOCK = 'lock' +DEVICE_CLASS_LOCK = "lock" # On means wet, Off means dry -DEVICE_CLASS_MOISTURE = 'moisture' +DEVICE_CLASS_MOISTURE = "moisture" # On means motion detected, Off means no motion (clear) -DEVICE_CLASS_MOTION = 'motion' +DEVICE_CLASS_MOTION = "motion" # On means moving, Off means not moving (stopped) -DEVICE_CLASS_MOVING = 'moving' +DEVICE_CLASS_MOVING = "moving" # On means occupied, Off means not occupied (clear) -DEVICE_CLASS_OCCUPANCY = 'occupancy' +DEVICE_CLASS_OCCUPANCY = "occupancy" # On means open, Off means closed -DEVICE_CLASS_OPENING = 'opening' +DEVICE_CLASS_OPENING = "opening" # On means plugged in, Off means unplugged -DEVICE_CLASS_PLUG = 'plug' +DEVICE_CLASS_PLUG = "plug" # On means power detected, Off means no power -DEVICE_CLASS_POWER = 'power' +DEVICE_CLASS_POWER = "power" # On means home, Off means away -DEVICE_CLASS_PRESENCE = 'presence' +DEVICE_CLASS_PRESENCE = "presence" # On means problem detected, Off means no problem (OK) -DEVICE_CLASS_PROBLEM = 'problem' +DEVICE_CLASS_PROBLEM = "problem" # On means unsafe, Off means safe -DEVICE_CLASS_SAFETY = 'safety' +DEVICE_CLASS_SAFETY = "safety" # On means smoke detected, Off means no smoke (clear) -DEVICE_CLASS_SMOKE = 'smoke' +DEVICE_CLASS_SMOKE = "smoke" # On means sound detected, Off means no sound (clear) -DEVICE_CLASS_SOUND = 'sound' +DEVICE_CLASS_SOUND = "sound" # On means vibration detected, Off means no vibration -DEVICE_CLASS_VIBRATION = 'vibration' +DEVICE_CLASS_VIBRATION = "vibration" # On means open, Off means closed -DEVICE_CLASS_WINDOW = 'window' +DEVICE_CLASS_WINDOW = "window" DEVICE_CLASSES = [ DEVICE_CLASS_BATTERY, @@ -117,7 +119,8 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) async def async_setup(hass, config): """Track states and offer events for binary sensors.""" component = hass.data[DOMAIN] = EntityComponent( - logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) + logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL + ) await component.async_setup(config) return True diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index 6ccb10f50e6..4d8d5643826 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -5,8 +5,7 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_DISPLAY_OPTIONS, ATTR_ATTRIBUTION, CONF_CURRENCY) +from homeassistant.const import CONF_DISPLAY_OPTIONS, ATTR_ATTRIBUTION, CONF_CURRENCY import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -14,41 +13,44 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Data provided by blockchain.info" -DEFAULT_CURRENCY = 'USD' +DEFAULT_CURRENCY = "USD" -ICON = 'mdi:currency-btc' +ICON = "mdi:currency-btc" SCAN_INTERVAL = timedelta(minutes=5) OPTION_TYPES = { - 'exchangerate': ['Exchange rate (1 BTC)', None], - 'trade_volume_btc': ['Trade volume', 'BTC'], - 'miners_revenue_usd': ['Miners revenue', 'USD'], - 'btc_mined': ['Mined', 'BTC'], - 'trade_volume_usd': ['Trade volume', 'USD'], - 'difficulty': ['Difficulty', None], - 'minutes_between_blocks': ['Time between Blocks', 'min'], - 'number_of_transactions': ['No. of Transactions', None], - 'hash_rate': ['Hash rate', 'PH/s'], - 'timestamp': ['Timestamp', None], - 'mined_blocks': ['Mined Blocks', None], - 'blocks_size': ['Block size', None], - 'total_fees_btc': ['Total fees', 'BTC'], - 'total_btc_sent': ['Total sent', 'BTC'], - 'estimated_btc_sent': ['Estimated sent', 'BTC'], - 'total_btc': ['Total', 'BTC'], - 'total_blocks': ['Total Blocks', None], - 'next_retarget': ['Next retarget', None], - 'estimated_transaction_volume_usd': ['Est. Transaction volume', 'USD'], - 'miners_revenue_btc': ['Miners revenue', 'BTC'], - 'market_price_usd': ['Market price', 'USD'] + "exchangerate": ["Exchange rate (1 BTC)", None], + "trade_volume_btc": ["Trade volume", "BTC"], + "miners_revenue_usd": ["Miners revenue", "USD"], + "btc_mined": ["Mined", "BTC"], + "trade_volume_usd": ["Trade volume", "USD"], + "difficulty": ["Difficulty", None], + "minutes_between_blocks": ["Time between Blocks", "min"], + "number_of_transactions": ["No. of Transactions", None], + "hash_rate": ["Hash rate", "PH/s"], + "timestamp": ["Timestamp", None], + "mined_blocks": ["Mined Blocks", None], + "blocks_size": ["Block size", None], + "total_fees_btc": ["Total fees", "BTC"], + "total_btc_sent": ["Total sent", "BTC"], + "estimated_btc_sent": ["Estimated sent", "BTC"], + "total_btc": ["Total", "BTC"], + "total_blocks": ["Total Blocks", None], + "next_retarget": ["Next retarget", None], + "estimated_transaction_volume_usd": ["Est. Transaction volume", "USD"], + "miners_revenue_btc": ["Miners revenue", "BTC"], + "market_price_usd": ["Market price", "USD"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DISPLAY_OPTIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(OPTION_TYPES)]), - vol.Optional(CONF_CURRENCY, default=DEFAULT_CURRENCY): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_DISPLAY_OPTIONS, default=[]): vol.All( + cv.ensure_list, [vol.In(OPTION_TYPES)] + ), + vol.Optional(CONF_CURRENCY, default=DEFAULT_CURRENCY): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -104,9 +106,7 @@ class BitcoinSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - } + return {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Get the latest data and updates the states.""" @@ -114,52 +114,49 @@ class BitcoinSensor(Entity): stats = self.data.stats ticker = self.data.ticker - if self.type == 'exchangerate': + if self.type == "exchangerate": self._state = ticker[self._currency].p15min self._unit_of_measurement = self._currency - elif self.type == 'trade_volume_btc': - self._state = '{0:.1f}'.format(stats.trade_volume_btc) - elif self.type == 'miners_revenue_usd': - self._state = '{0:.0f}'.format(stats.miners_revenue_usd) - elif self.type == 'btc_mined': - self._state = '{}'.format(stats.btc_mined * 0.00000001) - elif self.type == 'trade_volume_usd': - self._state = '{0:.1f}'.format(stats.trade_volume_usd) - elif self.type == 'difficulty': - self._state = '{0:.0f}'.format(stats.difficulty) - elif self.type == 'minutes_between_blocks': - self._state = '{0:.2f}'.format(stats.minutes_between_blocks) - elif self.type == 'number_of_transactions': - self._state = '{}'.format(stats.number_of_transactions) - elif self.type == 'hash_rate': - self._state = '{0:.1f}'.format(stats.hash_rate * 0.000001) - elif self.type == 'timestamp': + elif self.type == "trade_volume_btc": + self._state = "{0:.1f}".format(stats.trade_volume_btc) + elif self.type == "miners_revenue_usd": + self._state = "{0:.0f}".format(stats.miners_revenue_usd) + elif self.type == "btc_mined": + self._state = "{}".format(stats.btc_mined * 0.00000001) + elif self.type == "trade_volume_usd": + self._state = "{0:.1f}".format(stats.trade_volume_usd) + elif self.type == "difficulty": + self._state = "{0:.0f}".format(stats.difficulty) + elif self.type == "minutes_between_blocks": + self._state = "{0:.2f}".format(stats.minutes_between_blocks) + elif self.type == "number_of_transactions": + self._state = "{}".format(stats.number_of_transactions) + elif self.type == "hash_rate": + self._state = "{0:.1f}".format(stats.hash_rate * 0.000001) + elif self.type == "timestamp": self._state = stats.timestamp - elif self.type == 'mined_blocks': - self._state = '{}'.format(stats.mined_blocks) - elif self.type == 'blocks_size': - self._state = '{0:.1f}'.format(stats.blocks_size) - elif self.type == 'total_fees_btc': - self._state = '{0:.2f}'.format(stats.total_fees_btc * 0.00000001) - elif self.type == 'total_btc_sent': - self._state = '{0:.2f}'.format(stats.total_btc_sent * 0.00000001) - elif self.type == 'estimated_btc_sent': - self._state = '{0:.2f}'.format( - stats.estimated_btc_sent * 0.00000001) - elif self.type == 'total_btc': - self._state = '{0:.2f}'.format(stats.total_btc * 0.00000001) - elif self.type == 'total_blocks': - self._state = '{0:.2f}'.format(stats.total_blocks) - elif self.type == 'next_retarget': - self._state = '{0:.2f}'.format(stats.next_retarget) - elif self.type == 'estimated_transaction_volume_usd': - self._state = '{0:.2f}'.format( - stats.estimated_transaction_volume_usd) - elif self.type == 'miners_revenue_btc': - self._state = '{0:.1f}'.format( - stats.miners_revenue_btc * 0.00000001) - elif self.type == 'market_price_usd': - self._state = '{0:.2f}'.format(stats.market_price_usd) + elif self.type == "mined_blocks": + self._state = "{}".format(stats.mined_blocks) + elif self.type == "blocks_size": + self._state = "{0:.1f}".format(stats.blocks_size) + elif self.type == "total_fees_btc": + self._state = "{0:.2f}".format(stats.total_fees_btc * 0.00000001) + elif self.type == "total_btc_sent": + self._state = "{0:.2f}".format(stats.total_btc_sent * 0.00000001) + elif self.type == "estimated_btc_sent": + self._state = "{0:.2f}".format(stats.estimated_btc_sent * 0.00000001) + elif self.type == "total_btc": + self._state = "{0:.2f}".format(stats.total_btc * 0.00000001) + elif self.type == "total_blocks": + self._state = "{0:.2f}".format(stats.total_blocks) + elif self.type == "next_retarget": + self._state = "{0:.2f}".format(stats.next_retarget) + elif self.type == "estimated_transaction_volume_usd": + self._state = "{0:.2f}".format(stats.estimated_transaction_volume_usd) + elif self.type == "miners_revenue_btc": + self._state = "{0:.1f}".format(stats.miners_revenue_btc * 0.00000001) + elif self.type == "market_price_usd": + self._state = "{0:.2f}".format(stats.market_price_usd) class BitcoinData: diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index 96e6ee5d56f..c54a61c66b1 100755 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -13,18 +13,20 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_DUE_IN = 'Due in' +ATTR_DUE_IN = "Due in" -CONF_STOP_ID = 'stopid' -CONF_ROUTE = 'route' +CONF_STOP_ID = "stopid" +CONF_ROUTE = "route" -DEFAULT_NAME = 'Next bus' +DEFAULT_NAME = "Next bus" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_STOP_ID): cv.string, - vol.Required(CONF_ROUTE): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STOP_ID): cv.string, + vol.Required(CONF_ROUTE): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -61,7 +63,7 @@ class BizkaibusSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of the sensor.""" - return 'minutes' + return "minutes" def update(self): """Get the latest data from the webservice.""" diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index be0538a89e9..a77fad69663 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -4,39 +4,45 @@ import socket import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON) + DOMAIN, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE, STATE_OFF, - STATE_ON) + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_TYPE, + STATE_OFF, + STATE_ON, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) SUPPORT_BLACKBIRD = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE -ZONE_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, -}) +MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids}) -SOURCE_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, -}) +ZONE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) -CONF_ZONES = 'zones' -CONF_SOURCES = 'sources' +SOURCE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) -DATA_BLACKBIRD = 'blackbird' +CONF_ZONES = "zones" +CONF_SOURCES = "sources" -SERVICE_SETALLZONES = 'blackbird_set_all_zones' -ATTR_SOURCE = 'source' +DATA_BLACKBIRD = "blackbird" -BLACKBIRD_SETALLZONES_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ - vol.Required(ATTR_SOURCE): cv.string -}) +SERVICE_SETALLZONES = "blackbird_set_all_zones" +ATTR_SOURCE = "source" + +BLACKBIRD_SETALLZONES_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( + {vol.Required(ATTR_SOURCE): cv.string} +) # Valid zone ids: 1-8 @@ -47,12 +53,15 @@ SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_PORT, CONF_HOST), - PLATFORM_SCHEMA.extend({ - vol.Exclusive(CONF_PORT, CONF_TYPE): cv.string, - vol.Exclusive(CONF_HOST, CONF_TYPE): cv.string, - vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), - vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), - })) + PLATFORM_SCHEMA.extend( + { + vol.Exclusive(CONF_PORT, CONF_TYPE): cv.string, + vol.Exclusive(CONF_HOST, CONF_TYPE): cv.string, + vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), + vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), + } + ), +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -83,8 +92,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Error connecting to the Blackbird controller") return - sources = {source_id: extra[CONF_NAME] for source_id, extra - in config[CONF_SOURCES].items()} + sources = { + source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items() + } devices = [] for zone_id, extra in config[CONF_ZONES].items(): @@ -101,8 +111,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entity_ids = service.data.get(ATTR_ENTITY_ID) source = service.data.get(ATTR_SOURCE) if entity_ids: - devices = [device for device in hass.data[DATA_BLACKBIRD].values() - if device.entity_id in entity_ids] + devices = [ + device + for device in hass.data[DATA_BLACKBIRD].values() + if device.entity_id in entity_ids + ] else: devices = hass.data[DATA_BLACKBIRD].values() @@ -111,8 +124,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if service.service == SERVICE_SETALLZONES: device.set_all_zones(source) - hass.services.register(DOMAIN, SERVICE_SETALLZONES, service_handle, - schema=BLACKBIRD_SETALLZONES_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_SETALLZONES, service_handle, schema=BLACKBIRD_SETALLZONES_SCHEMA + ) class BlackbirdZone(MediaPlayerDevice): @@ -126,8 +140,9 @@ class BlackbirdZone(MediaPlayerDevice): # dict source name -> source_id self._source_name_id = {v: k for k, v in sources.items()} # ordered list of all source names - self._source_names = sorted(self._source_name_id.keys(), - key=lambda v: self._source_name_id[v]) + self._source_names = sorted( + self._source_name_id.keys(), key=lambda v: self._source_name_id[v] + ) self._zone_id = zone_id self._name = zone_name self._state = None diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 74057c7b6bc..bd11572ba1c 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -3,106 +3,122 @@ import logging from datetime import timedelta import voluptuous as vol -from homeassistant.helpers import ( - config_validation as cv, discovery) +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, - CONF_MONITORED_CONDITIONS, CONF_MODE, CONF_OFFSET, TEMP_FAHRENHEIT) + CONF_USERNAME, + CONF_PASSWORD, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_BINARY_SENSORS, + CONF_SENSORS, + CONF_FILENAME, + CONF_MONITORED_CONDITIONS, + CONF_MODE, + CONF_OFFSET, + TEMP_FAHRENHEIT, +) _LOGGER = logging.getLogger(__name__) -DOMAIN = 'blink' -BLINK_DATA = 'blink' +DOMAIN = "blink" +BLINK_DATA = "blink" -CONF_CAMERA = 'camera' -CONF_ALARM_CONTROL_PANEL = 'alarm_control_panel' +CONF_CAMERA = "camera" +CONF_ALARM_CONTROL_PANEL = "alarm_control_panel" -DEFAULT_BRAND = 'Blink' +DEFAULT_BRAND = "Blink" DEFAULT_ATTRIBUTION = "Data provided by immedia-semi.com" SIGNAL_UPDATE_BLINK = "blink_update" DEFAULT_SCAN_INTERVAL = timedelta(seconds=300) -TYPE_CAMERA_ARMED = 'motion_enabled' -TYPE_MOTION_DETECTED = 'motion_detected' -TYPE_TEMPERATURE = 'temperature' -TYPE_BATTERY = 'battery' -TYPE_WIFI_STRENGTH = 'wifi_strength' +TYPE_CAMERA_ARMED = "motion_enabled" +TYPE_MOTION_DETECTED = "motion_detected" +TYPE_TEMPERATURE = "temperature" +TYPE_BATTERY = "battery" +TYPE_WIFI_STRENGTH = "wifi_strength" -SERVICE_REFRESH = 'blink_update' -SERVICE_TRIGGER = 'trigger_camera' -SERVICE_SAVE_VIDEO = 'save_video' +SERVICE_REFRESH = "blink_update" +SERVICE_TRIGGER = "trigger_camera" +SERVICE_SAVE_VIDEO = "save_video" BINARY_SENSORS = { - TYPE_CAMERA_ARMED: ['Camera Armed', 'mdi:verified'], - TYPE_MOTION_DETECTED: ['Motion Detected', 'mdi:run-fast'], + TYPE_CAMERA_ARMED: ["Camera Armed", "mdi:verified"], + TYPE_MOTION_DETECTED: ["Motion Detected", "mdi:run-fast"], } SENSORS = { - TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'], - TYPE_BATTERY: ['Battery', '', 'mdi:battery-80'], - TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'], + TYPE_TEMPERATURE: ["Temperature", TEMP_FAHRENHEIT, "mdi:thermometer"], + TYPE_BATTERY: ["Battery", "", "mdi:battery-80"], + TYPE_WIFI_STRENGTH: ["Wifi Signal", "dBm", "mdi:wifi-strength-2"], } -BINARY_SENSOR_SCHEMA = vol.Schema({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): - vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]) -}) +BINARY_SENSOR_SCHEMA = vol.Schema( + { + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): vol.All( + cv.ensure_list, [vol.In(BINARY_SENSORS)] + ) + } +) -SENSOR_SCHEMA = vol.Schema({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): - vol.All(cv.ensure_list, [vol.In(SENSORS)]) -}) +SENSOR_SCHEMA = vol.Schema( + { + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( + cv.ensure_list, [vol.In(SENSORS)] + ) + } +) -SERVICE_TRIGGER_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string -}) +SERVICE_TRIGGER_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) -SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FILENAME): cv.string, -}) +SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema( + {vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILENAME): cv.string} +) CONFIG_SCHEMA = vol.Schema( { - DOMAIN: - vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): - cv.time_period, - vol.Optional(CONF_BINARY_SENSORS, default={}): - BINARY_SENSOR_SCHEMA, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional(CONF_OFFSET, default=1): int, - vol.Optional(CONF_MODE, default=''): cv.string, - }) + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_OFFSET, default=1): int, + vol.Optional(CONF_MODE, default=""): cv.string, + } + ) }, - extra=vol.ALLOW_EXTRA) + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): """Set up Blink System.""" from blinkpy import blinkpy + conf = config[BLINK_DATA] username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] scan_interval = conf[CONF_SCAN_INTERVAL] - is_legacy = bool(conf[CONF_MODE] == 'legacy') + is_legacy = bool(conf[CONF_MODE] == "legacy") motion_interval = conf[CONF_OFFSET] - hass.data[BLINK_DATA] = blinkpy.Blink(username=username, - password=password, - motion_interval=motion_interval, - legacy_subdomain=is_legacy) + hass.data[BLINK_DATA] = blinkpy.Blink( + username=username, + password=password, + motion_interval=motion_interval, + legacy_subdomain=is_legacy, + ) hass.data[BLINK_DATA].refresh_rate = scan_interval.total_seconds() hass.data[BLINK_DATA].start() platforms = [ - ('alarm_control_panel', {}), - ('binary_sensor', conf[CONF_BINARY_SENSORS]), - ('camera', {}), - ('sensor', conf[CONF_SENSORS]), + ("alarm_control_panel", {}), + ("binary_sensor", conf[CONF_BINARY_SENSORS]), + ("camera", {}), + ("sensor", conf[CONF_SENSORS]), ] for component, schema in platforms: @@ -125,14 +141,12 @@ def setup(hass, config): await async_handle_save_video_service(hass, call) hass.services.register(DOMAIN, SERVICE_REFRESH, blink_refresh) - hass.services.register(DOMAIN, - SERVICE_TRIGGER, - trigger_camera, - schema=SERVICE_TRIGGER_SCHEMA) - hass.services.register(DOMAIN, - SERVICE_SAVE_VIDEO, - async_save_video, - schema=SERVICE_SAVE_VIDEO_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_TRIGGER, trigger_camera, schema=SERVICE_TRIGGER_SCHEMA + ) + hass.services.register( + DOMAIN, SERVICE_SAVE_VIDEO, async_save_video, schema=SERVICE_SAVE_VIDEO_SCHEMA + ) return True @@ -141,8 +155,7 @@ async def async_handle_save_video_service(hass, call): camera_name = call.data[CONF_NAME] video_path = call.data[CONF_FILENAME] if not hass.config.is_allowed_path(video_path): - _LOGGER.error( - "Can't write %s, no access to path!", video_path) + _LOGGER.error("Can't write %s, no access to path!", video_path) return def _write_video(camera_name, video_path): @@ -152,7 +165,6 @@ async def async_handle_save_video_service(hass, call): all_cameras[camera_name].video_to_file(video_path) try: - await hass.async_add_executor_job( - _write_video, camera_name, video_path) + await hass.async_add_executor_job(_write_video, camera_name, video_path) except OSError as err: _LOGGER.error("Can't write image to file: %s", err) diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index 8cc89d90b2f..adcefeddf23 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -3,13 +3,16 @@ import logging from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.const import ( - ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED) + ATTR_ATTRIBUTION, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_DISARMED, +) from . import BLINK_DATA, DEFAULT_ATTRIBUTION _LOGGER = logging.getLogger(__name__) -ICON = 'mdi:security' +ICON = "mdi:security" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -58,8 +61,8 @@ class BlinkSyncModule(AlarmControlPanel): def device_state_attributes(self): """Return the state attributes.""" attr = self.sync.attributes - attr['network_info'] = self.data.networks - attr['associated_cameras'] = list(self.sync.cameras.keys()) + attr["network_info"] = self.data.networks + attr["associated_cameras"] = list(self.sync.cameras.keys()) attr[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION return attr diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index d1301319a81..5e8b5323f89 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -7,8 +7,8 @@ from . import BLINK_DATA, DEFAULT_BRAND _LOGGER = logging.getLogger(__name__) -ATTR_VIDEO_CLIP = 'video' -ATTR_IMAGE = 'image' +ATTR_VIDEO_CLIP = "video" +ATTR_IMAGE = "image" def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index 6fb8be8e4ea..fba2d0bd493 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -28,8 +28,7 @@ class BlinkSensor(Entity): def __init__(self, data, camera, sensor_type): """Initialize sensors from Blink camera.""" name, units, icon = SENSORS[sensor_type] - self._name = "{} {} {}".format( - BLINK_DATA, camera, name) + self._name = "{} {} {}".format(BLINK_DATA, camera, name) self._camera_name = name self._type = sensor_type self.data = data @@ -39,8 +38,8 @@ class BlinkSensor(Entity): self._icon = icon self._unique_id = "{}-{}".format(self._camera.serial, self._type) self._sensor_key = self._type - if self._type == 'temperature': - self._sensor_key = 'temperature_calibrated' + if self._type == "temperature": + self._sensor_key = "temperature_calibrated" @property def name(self): @@ -75,5 +74,5 @@ class BlinkSensor(Entity): except KeyError: self._state = None _LOGGER.error( - "%s not a valid camera attribute. Did the API change?", - self._sensor_key) + "%s not a valid camera attribute. Did the API change?", self._sensor_key + ) diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py index 8eab6afaeb7..5f3cb7ebfd1 100644 --- a/homeassistant/components/blinksticklight/light.py +++ b/homeassistant/components/blinksticklight/light.py @@ -4,24 +4,31 @@ import logging import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + Light, + PLATFORM_SCHEMA, +) from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -CONF_SERIAL = 'serial' +CONF_SERIAL = "serial" -DEFAULT_NAME = 'Blinkstick' +DEFAULT_NAME = "Blinkstick" SUPPORT_BLINKSTICK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SERIAL): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_SERIAL): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -94,9 +101,9 @@ class BlinkStickLight(Light): self._brightness = 255 rgb_color = color_util.color_hsv_to_RGB( - self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100) - self._stick.set_color( - red=rgb_color[0], green=rgb_color[1], blue=rgb_color[2]) + self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 + ) + self._stick.set_color(red=rgb_color[0], green=rgb_color[1], blue=rgb_color[2]) def turn_off(self, **kwargs): """Turn the device off.""" diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py index cb3e854b388..9fee72662c6 100644 --- a/homeassistant/components/blinkt/light.py +++ b/homeassistant/components/blinkt/light.py @@ -6,35 +6,40 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, - Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + ATTR_HS_COLOR, + SUPPORT_COLOR, + Light, + PLATFORM_SCHEMA, +) from homeassistant.const import CONF_NAME import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -SUPPORT_BLINKT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) +SUPPORT_BLINKT = SUPPORT_BRIGHTNESS | SUPPORT_COLOR -DEFAULT_NAME = 'blinkt' +DEFAULT_NAME = "blinkt" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Blinkt Light platform.""" # pylint: disable=no-member - blinkt = importlib.import_module('blinkt') + blinkt = importlib.import_module("blinkt") # ensure that the lights are off when exiting blinkt.set_clear_on_exit() name = config.get(CONF_NAME) - add_entities([ - BlinktLight(blinkt, name, index) for index in range(blinkt.NUM_PIXELS) - ]) + add_entities( + [BlinktLight(blinkt, name, index) for index in range(blinkt.NUM_PIXELS)] + ) class BlinktLight(Light): @@ -97,13 +102,11 @@ class BlinktLight(Light): if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - percent_bright = (self._brightness / 255) + percent_bright = self._brightness / 255 rgb_color = color_util.color_hs_to_RGB(*self._hs_color) - self._blinkt.set_pixel(self._index, - rgb_color[0], - rgb_color[1], - rgb_color[2], - percent_bright) + self._blinkt.set_pixel( + self._index, rgb_color[0], rgb_color[1], rgb_color[2], percent_bright + ) self._blinkt.show() diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index 436e2979a6e..c95ccb3fed3 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -6,25 +6,27 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) +from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Data provided by blockchain.info" -CONF_ADDRESSES = 'addresses' +CONF_ADDRESSES = "addresses" -DEFAULT_NAME = 'Bitcoin Balance' +DEFAULT_NAME = "Bitcoin Balance" -ICON = 'mdi:currency-btc' +ICON = "mdi:currency-btc" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ADDRESSES): [cv.string], - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ADDRESSES): [cv.string], + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -50,7 +52,7 @@ class BlockchainSensor(Entity): self._name = name self.addresses = addresses self._state = None - self._unit_of_measurement = 'BTC' + self._unit_of_measurement = "BTC" @property def name(self): @@ -75,11 +77,10 @@ class BlockchainSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - } + return {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Get the latest state of the sensor.""" from pyblockchain import get_balance + self._state = get_balance(self.addresses) diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index 7f924929662..dc0723730c4 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -14,19 +14,17 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) BLOOMSKY = None -BLOOMSKY_TYPE = ['camera', 'binary_sensor', 'sensor'] +BLOOMSKY_TYPE = ["camera", "binary_sensor", "sensor"] -DOMAIN = 'bloomsky' +DOMAIN = "bloomsky" # The BloomSky only updates every 5-8 minutes as per the API spec so there's # no point in polling the API more frequently MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_API_KEY): cv.string})}, extra=vol.ALLOW_EXTRA +) def setup(hass, config): @@ -35,7 +33,7 @@ def setup(hass, config): global BLOOMSKY try: - BLOOMSKY = BloomSky(api_key) + BLOOMSKY = BloomSky(api_key, hass.config.units.is_metric) except RuntimeError: return False @@ -49,12 +47,14 @@ class BloomSky: """Handle all communication with the BloomSky API.""" # API documentation at http://weatherlution.com/bloomsky-api/ - API_URL = 'http://api.bloomsky.com/api/skydata' + API_URL = "http://api.bloomsky.com/api/skydata" - def __init__(self, api_key): + def __init__(self, api_key, is_metric): """Initialize the BookSky.""" self._api_key = api_key + self._endpoint_argument = "unit=intl" if is_metric else "" self.devices = {} + self.is_metric = is_metric _LOGGER.debug("Initial BloomSky device load...") self.refresh_devices() @@ -63,13 +63,17 @@ class BloomSky: """Use the API to retrieve a list of devices.""" _LOGGER.debug("Fetching BloomSky update") response = requests.get( - self.API_URL, headers={AUTHORIZATION: self._api_key}, timeout=10) + "{}?{}".format(self.API_URL, self._endpoint_argument), + headers={AUTHORIZATION: self._api_key}, + timeout=10, + ) if response.status_code == 401: raise RuntimeError("Invalid API_KEY") + if response.status_code == 405: + _LOGGER.error("You have no bloomsky devices configured") + return if response.status_code != 200: _LOGGER.error("Invalid HTTP response: %s", response.status_code) return # Create dictionary keyed off of the device unique id - self.devices.update({ - device['DeviceID']: device for device in response.json() - }) + self.devices.update({device["DeviceID"]: device for device in response.json()}) diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index b17c4e4c257..3a8242929c5 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -3,8 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -12,15 +11,15 @@ from . import BLOOMSKY _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES = { - 'Rain': 'moisture', - 'Night': None, -} +SENSOR_TYPES = {"Rain": "moisture", "Night": None} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ) + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -30,8 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for device in BLOOMSKY.devices.values(): for variable in sensors: - add_entities( - [BloomSkySensor(BLOOMSKY, device, variable)], True) + add_entities([BloomSkySensor(BLOOMSKY, device, variable)], True) class BloomSkySensor(BinarySensorDevice): @@ -40,11 +38,11 @@ class BloomSkySensor(BinarySensorDevice): def __init__(self, bs, device, sensor_name): """Initialize a BloomSky binary sensor.""" self._bloomsky = bs - self._device_id = device['DeviceID'] + self._device_id = device["DeviceID"] self._sensor_name = sensor_name - self._name = '{} {}'.format(device['DeviceName'], sensor_name) + self._name = "{} {}".format(device["DeviceName"], sensor_name) self._state = None - self._unique_id = '{}-{}'.format(self._device_id, self._sensor_name) + self._unique_id = "{}-{}".format(self._device_id, self._sensor_name) @property def unique_id(self): @@ -70,5 +68,4 @@ class BloomSkySensor(BinarySensorDevice): """Request an update from the BloomSky API.""" self._bloomsky.refresh_devices() - self._state = \ - self._bloomsky.devices[self._device_id]['Data'][self._sensor_name] + self._state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] diff --git a/homeassistant/components/bloomsky/camera.py b/homeassistant/components/bloomsky/camera.py index a748ff2b5b8..9b8c4ab283f 100644 --- a/homeassistant/components/bloomsky/camera.py +++ b/homeassistant/components/bloomsky/camera.py @@ -20,8 +20,8 @@ class BloomSkyCamera(Camera): def __init__(self, bs, device): """Initialize access to the BloomSky camera images.""" super(BloomSkyCamera, self).__init__() - self._name = device['DeviceName'] - self._id = device['DeviceID'] + self._name = device["DeviceName"] + self._id = device["DeviceID"] self._bloomsky = bs self._url = "" self._last_url = "" @@ -34,7 +34,7 @@ class BloomSkyCamera(Camera): def camera_image(self): """Update the camera's image if it has changed.""" try: - self._url = self._bloomsky.devices[self._id]['Data']['ImageURL'] + self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"] self._bloomsky.refresh_devices() # If the URL hasn't changed then the image hasn't changed. if self._url != self._last_url: diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index e7d4bc5c8eb..cca57bcae82 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS) +from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -13,27 +13,43 @@ from . import BLOOMSKY LOGGER = logging.getLogger(__name__) # These are the available sensors -SENSOR_TYPES = ['Temperature', - 'Humidity', - 'Pressure', - 'Luminance', - 'UVIndex', - 'Voltage'] +SENSOR_TYPES = [ + "Temperature", + "Humidity", + "Pressure", + "Luminance", + "UVIndex", + "Voltage", +] # Sensor units - these do not currently align with the API documentation -SENSOR_UNITS = {'Temperature': TEMP_FAHRENHEIT, - 'Humidity': '%', - 'Pressure': 'inHg', - 'Luminance': 'cd/m²', - 'Voltage': 'mV'} +SENSOR_UNITS_IMPERIAL = { + "Temperature": TEMP_FAHRENHEIT, + "Humidity": "%", + "Pressure": "inHg", + "Luminance": "cd/m²", + "Voltage": "mV", +} + +# Metric units +SENSOR_UNITS_METRIC = { + "Temperature": TEMP_CELSIUS, + "Humidity": "%", + "Pressure": "mbar", + "Luminance": "cd/m²", + "Voltage": "mV", +} # Which sensors to format numerically -FORMAT_NUMBERS = ['Temperature', 'Pressure', 'Voltage'] +FORMAT_NUMBERS = ["Temperature", "Pressure", "Voltage"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ) + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -43,8 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for device in BLOOMSKY.devices.values(): for variable in sensors: - add_entities( - [BloomSkySensor(BLOOMSKY, device, variable)], True) + add_entities([BloomSkySensor(BLOOMSKY, device, variable)], True) class BloomSkySensor(Entity): @@ -53,11 +68,11 @@ class BloomSkySensor(Entity): def __init__(self, bs, device, sensor_name): """Initialize a BloomSky sensor.""" self._bloomsky = bs - self._device_id = device['DeviceID'] + self._device_id = device["DeviceID"] self._sensor_name = sensor_name - self._name = '{} {}'.format(device['DeviceName'], sensor_name) + self._name = "{} {}".format(device["DeviceName"], sensor_name) self._state = None - self._unique_id = '{}-{}'.format(self._device_id, self._sensor_name) + self._unique_id = "{}-{}".format(self._device_id, self._sensor_name) @property def unique_id(self): @@ -77,16 +92,17 @@ class BloomSkySensor(Entity): @property def unit_of_measurement(self): """Return the sensor units.""" - return SENSOR_UNITS.get(self._sensor_name, None) + if self._bloomsky.is_metric: + return SENSOR_UNITS_METRIC.get(self._sensor_name, None) + return SENSOR_UNITS_IMPERIAL.get(self._sensor_name, None) def update(self): """Request an update from the BloomSky API.""" self._bloomsky.refresh_devices() - state = \ - self._bloomsky.devices[self._device_id]['Data'][self._sensor_name] + state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] if self._sensor_name in FORMAT_NUMBERS: - self._state = '{0:.2f}'.format(state) + self._state = "{0:.2f}".format(state) else: self._state = state diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 2a3b3e35125..e5f264b5f73 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -10,18 +10,38 @@ from aiohttp.hdrs import CONNECTION, KEEP_ALIVE import async_timeout import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, - SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, - SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) + ATTR_MEDIA_ENQUEUE, + DOMAIN, + MEDIA_TYPE_MUSIC, + SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_SELECT_SOURCE, + SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_HOSTS, CONF_NAME, CONF_PORT, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, - STATE_PAUSED, STATE_PLAYING) + ATTR_ENTITY_ID, + CONF_HOST, + CONF_HOSTS, + CONF_NAME, + CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -31,54 +51,49 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -ATTR_MASTER = 'master' +ATTR_MASTER = "master" -DATA_BLUESOUND = 'bluesound' +DATA_BLUESOUND = "bluesound" DEFAULT_PORT = 11000 NODE_OFFLINE_CHECK_TIMEOUT = 180 NODE_RETRY_INITIATION = timedelta(minutes=3) -SERVICE_CLEAR_TIMER = 'bluesound_clear_sleep_timer' -SERVICE_JOIN = 'bluesound_join' -SERVICE_SET_TIMER = 'bluesound_set_sleep_timer' -SERVICE_UNJOIN = 'bluesound_unjoin' -STATE_GROUPED = 'grouped' +SERVICE_CLEAR_TIMER = "bluesound_clear_sleep_timer" +SERVICE_JOIN = "bluesound_join" +SERVICE_SET_TIMER = "bluesound_set_sleep_timer" +SERVICE_UNJOIN = "bluesound_unjoin" +STATE_GROUPED = "grouped" SYNC_STATUS_INTERVAL = timedelta(minutes=5) UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30) UPDATE_PRESETS_INTERVAL = timedelta(minutes=30) UPDATE_SERVICES_INTERVAL = timedelta(minutes=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [{ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - }]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOSTS): vol.All( + cv.ensure_list, + [ + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } + ], + ) + } +) -BS_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) +BS_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) -BS_JOIN_SCHEMA = BS_SCHEMA.extend({ - vol.Required(ATTR_MASTER): cv.entity_id, -}) +BS_JOIN_SCHEMA = BS_SCHEMA.extend({vol.Required(ATTR_MASTER): cv.entity_id}) SERVICE_TO_METHOD = { - SERVICE_JOIN: { - 'method': 'async_join', - 'schema': BS_JOIN_SCHEMA}, - SERVICE_UNJOIN: { - 'method': 'async_unjoin', - 'schema': BS_SCHEMA}, - SERVICE_SET_TIMER: { - 'method': 'async_increase_timer', - 'schema': BS_SCHEMA}, - SERVICE_CLEAR_TIMER: { - 'method': 'async_clear_timer', - 'schema': BS_SCHEMA} + SERVICE_JOIN: {"method": "async_join", "schema": BS_JOIN_SCHEMA}, + SERVICE_UNJOIN: {"method": "async_unjoin", "schema": BS_SCHEMA}, + SERVICE_SET_TIMER: {"method": "async_increase_timer", "schema": BS_SCHEMA}, + SERVICE_CLEAR_TIMER: {"method": "async_clear_timer", "schema": BS_SCHEMA}, } @@ -111,8 +126,7 @@ def _add_player(hass, async_add_entities, host, port=None, name=None): if hass.is_running: _start_polling() else: - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, _start_polling) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _start_polling) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_polling) @@ -125,23 +139,30 @@ def _add_player(hass, async_add_entities, host, port=None, name=None): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Bluesound platforms.""" if DATA_BLUESOUND not in hass.data: hass.data[DATA_BLUESOUND] = [] if discovery_info: - _add_player(hass, async_add_entities, discovery_info.get(CONF_HOST), - discovery_info.get(CONF_PORT, None)) + _add_player( + hass, + async_add_entities, + discovery_info.get(CONF_HOST), + discovery_info.get(CONF_PORT, None), + ) return hosts = config.get(CONF_HOSTS, None) if hosts: for host in hosts: _add_player( - hass, async_add_entities, host.get(CONF_HOST), - host.get(CONF_PORT), host.get(CONF_NAME)) + hass, + async_add_entities, + host.get(CONF_HOST), + host.get(CONF_PORT), + host.get(CONF_NAME), + ) async def async_service_handler(service): """Map services to method of Bluesound devices.""" @@ -149,22 +170,27 @@ async def async_setup_platform( if not method: return - params = {key: value for key, value in service.data.items() - if key != ATTR_ENTITY_ID} + params = { + key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID + } entity_ids = service.data.get(ATTR_ENTITY_ID) if entity_ids: - target_players = [player for player in hass.data[DATA_BLUESOUND] - if player.entity_id in entity_ids] + target_players = [ + player + for player in hass.data[DATA_BLUESOUND] + if player.entity_id in entity_ids + ] else: target_players = hass.data[DATA_BLUESOUND] for player in target_players: - await getattr(player, method['method'])(**params) + await getattr(player, method["method"])(**params) for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service]['schema'] + schema = SERVICE_TO_METHOD[service]["schema"] hass.services.async_register( - DOMAIN, service, async_service_handler, schema=schema) + DOMAIN, service, async_service_handler, schema=schema + ) class BluesoundPlayer(MediaPlayerDevice): @@ -207,28 +233,30 @@ class BluesoundPlayer(MediaPlayerDevice): except ValueError: return -1 - async def force_update_sync_status( - self, on_updated_cb=None, raise_timeout=False): + async def force_update_sync_status(self, on_updated_cb=None, raise_timeout=False): """Update the internal status.""" resp = await self.send_bluesound_command( - 'SyncStatus', raise_timeout, raise_timeout) + "SyncStatus", raise_timeout, raise_timeout + ) if not resp: return None - self._sync_status = resp['SyncStatus'].copy() + self._sync_status = resp["SyncStatus"].copy() if not self._name: - self._name = self._sync_status.get('@name', self.host) + self._name = self._sync_status.get("@name", self.host) if not self._icon: - self._icon = self._sync_status.get('@icon', self.host) + self._icon = self._sync_status.get("@icon", self.host) - master = self._sync_status.get('master', None) + master = self._sync_status.get("master", None) if master is not None: self._is_master = False - master_host = master.get('#text') - master_device = [device for device in - self._hass.data[DATA_BLUESOUND] - if device.host == master_host] + master_host = master.get("#text") + master_device = [ + device + for device in self._hass.data[DATA_BLUESOUND] + if device.host == master_host + ] if master_device and master_host != self.host: self._master = master_device[0] @@ -238,7 +266,7 @@ class BluesoundPlayer(MediaPlayerDevice): else: if self._master is not None: self._master = None - slaves = self._sync_status.get('slave', None) + slaves = self._sync_status.get("slave", None) self._is_master = slaves is not None if on_updated_cb: @@ -251,11 +279,9 @@ class BluesoundPlayer(MediaPlayerDevice): while True: await self.async_update_status() - except (asyncio.TimeoutError, ClientError, - BluesoundPlayer._TimeoutException): + except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException): _LOGGER.info("Node %s is offline, retrying later", self._name) - await asyncio.sleep( - NODE_OFFLINE_CHECK_TIMEOUT) + await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() except CancelledError: @@ -266,8 +292,7 @@ class BluesoundPlayer(MediaPlayerDevice): def start_polling(self): """Start the polling task.""" - self._polling_task = self._hass.async_create_task( - self._start_poll_command()) + self._polling_task = self._hass.async_create_task(self._start_poll_command()) def stop_polling(self): """Stop the polling task.""" @@ -280,15 +305,14 @@ class BluesoundPlayer(MediaPlayerDevice): self._retry_remove() self._retry_remove = None - await self.force_update_sync_status( - self._init_callback, True) + await self.force_update_sync_status(self._init_callback, True) except (asyncio.TimeoutError, ClientError): _LOGGER.info("Node %s is offline, retrying later", self.host) self._retry_remove = async_track_time_interval( - self._hass, self.async_init, NODE_RETRY_INITIATION) + self._hass, self.async_init, NODE_RETRY_INITIATION + ) except Exception: - _LOGGER.exception( - "Unexpected when initiating error in %s", self.host) + _LOGGER.exception("Unexpected when initiating error in %s", self.host) raise async def async_update(self): @@ -302,14 +326,15 @@ class BluesoundPlayer(MediaPlayerDevice): await self.async_update_services() async def send_bluesound_command( - self, method, raise_timeout=False, allow_offline=False): + self, method, raise_timeout=False, allow_offline=False + ): """Send command to the player.""" import xmltodict if not self._is_online and not allow_offline: return - if method[0] == '/': + if method[0] == "/": method = method[1:] url = "http://{}:{}/{}".format(self.host, self.port, method) @@ -346,15 +371,16 @@ class BluesoundPlayer(MediaPlayerDevice): async def async_update_status(self): """Use the poll session to always get the status of the player.""" import xmltodict + response = None - url = 'Status' - etag = '' + url = "Status" + etag = "" if self._status is not None: - etag = self._status.get('@etag', '') + etag = self._status.get("@etag", "") - if etag != '': - url = 'Status?etag={}&timeout=120.0'.format(etag) + if etag != "": + url = "Status?etag={}&timeout=120.0".format(etag) url = "http://{}:{}/{}".format(self.host, self.port, url) _LOGGER.debug("Calling URL: %s", url) @@ -363,18 +389,18 @@ class BluesoundPlayer(MediaPlayerDevice): with async_timeout.timeout(125): response = await self._polling_session.get( - url, headers={CONNECTION: KEEP_ALIVE}) + url, headers={CONNECTION: KEEP_ALIVE} + ) if response.status == 200: result = await response.text() self._is_online = True self._last_status_update = dt_util.utcnow() - self._status = xmltodict.parse(result)['status'].copy() + self._status = xmltodict.parse(result)["status"].copy() - group_name = self._status.get('groupName', None) + group_name = self._status.get("groupName", None) if group_name != self._group_name: - _LOGGER.debug( - "Group name change detected on device: %s", self.host) + _LOGGER.debug("Group name change detected on device: %s", self.host) self._group_name = group_name # the sleep is needed to make sure that the # devices is synced @@ -395,16 +421,16 @@ class BluesoundPlayer(MediaPlayerDevice): _LOGGER.info("Status 595 returned, treating as timeout") raise BluesoundPlayer._TimeoutException() else: - _LOGGER.error("Error %s on %s. Trying one more time", - response.status, url) + _LOGGER.error( + "Error %s on %s. Trying one more time", response.status, url + ) except (asyncio.TimeoutError, ClientError): self._is_online = False self._last_status_update = None self._status = None self.async_schedule_update_ha_state() - _LOGGER.info( - "Client connection error, marking %s as offline", self._name) + _LOGGER.info("Client connection error, marking %s as offline", self._name) raise async def async_trigger_sync_on_all(self): @@ -415,90 +441,93 @@ class BluesoundPlayer(MediaPlayerDevice): await player.force_update_sync_status() @Throttle(SYNC_STATUS_INTERVAL) - async def async_update_sync_status( - self, on_updated_cb=None, raise_timeout=False): + async def async_update_sync_status(self, on_updated_cb=None, raise_timeout=False): """Update sync status.""" - await self.force_update_sync_status( - on_updated_cb, raise_timeout=False) + await self.force_update_sync_status(on_updated_cb, raise_timeout=False) @Throttle(UPDATE_CAPTURE_INTERVAL) async def async_update_captures(self): """Update Capture sources.""" - resp = await self.send_bluesound_command( - 'RadioBrowse?service=Capture') + resp = await self.send_bluesound_command("RadioBrowse?service=Capture") if not resp: return self._capture_items = [] def _create_capture_item(item): - self._capture_items.append({ - 'title': item.get('@text', ''), - 'name': item.get('@text', ''), - 'type': item.get('@serviceType', 'Capture'), - 'image': item.get('@image', ''), - 'url': item.get('@URL', '') - }) + self._capture_items.append( + { + "title": item.get("@text", ""), + "name": item.get("@text", ""), + "type": item.get("@serviceType", "Capture"), + "image": item.get("@image", ""), + "url": item.get("@URL", ""), + } + ) - if 'radiotime' in resp and 'item' in resp['radiotime']: - if isinstance(resp['radiotime']['item'], list): - for item in resp['radiotime']['item']: + if "radiotime" in resp and "item" in resp["radiotime"]: + if isinstance(resp["radiotime"]["item"], list): + for item in resp["radiotime"]["item"]: _create_capture_item(item) else: - _create_capture_item(resp['radiotime']['item']) + _create_capture_item(resp["radiotime"]["item"]) return self._capture_items @Throttle(UPDATE_PRESETS_INTERVAL) async def async_update_presets(self): """Update Presets.""" - resp = await self.send_bluesound_command('Presets') + resp = await self.send_bluesound_command("Presets") if not resp: return self._preset_items = [] def _create_preset_item(item): - self._preset_items.append({ - 'title': item.get('@name', ''), - 'name': item.get('@name', ''), - 'type': 'preset', - 'image': item.get('@image', ''), - 'is_raw_url': True, - 'url2': item.get('@url', ''), - 'url': 'Preset?id={}'.format(item.get('@id', '')) - }) + self._preset_items.append( + { + "title": item.get("@name", ""), + "name": item.get("@name", ""), + "type": "preset", + "image": item.get("@image", ""), + "is_raw_url": True, + "url2": item.get("@url", ""), + "url": "Preset?id={}".format(item.get("@id", "")), + } + ) - if 'presets' in resp and 'preset' in resp['presets']: - if isinstance(resp['presets']['preset'], list): - for item in resp['presets']['preset']: + if "presets" in resp and "preset" in resp["presets"]: + if isinstance(resp["presets"]["preset"], list): + for item in resp["presets"]["preset"]: _create_preset_item(item) else: - _create_preset_item(resp['presets']['preset']) + _create_preset_item(resp["presets"]["preset"]) return self._preset_items @Throttle(UPDATE_SERVICES_INTERVAL) async def async_update_services(self): """Update Services.""" - resp = await self.send_bluesound_command('Services') + resp = await self.send_bluesound_command("Services") if not resp: return self._services_items = [] def _create_service_item(item): - self._services_items.append({ - 'title': item.get('@displayname', ''), - 'name': item.get('@name', ''), - 'type': item.get('@type', ''), - 'image': item.get('@icon', ''), - 'url': item.get('@name', '') - }) + self._services_items.append( + { + "title": item.get("@displayname", ""), + "name": item.get("@name", ""), + "type": item.get("@type", ""), + "image": item.get("@icon", ""), + "url": item.get("@name", ""), + } + ) - if 'services' in resp and 'service' in resp['services']: - if isinstance(resp['services']['service'], list): - for item in resp['services']['service']: + if "services" in resp and "service" in resp["services"]: + if isinstance(resp["services"]["service"], list): + for item in resp["services"]["service"]: _create_service_item(item) else: - _create_service_item(resp['services']['service']) + _create_service_item(resp["services"]["service"]) return self._services_items @@ -516,21 +545,20 @@ class BluesoundPlayer(MediaPlayerDevice): if self.is_grouped and not self.is_master: return STATE_GROUPED - status = self._status.get('state', None) - if status in ('pause', 'stop'): + status = self._status.get("state", None) + if status in ("pause", "stop"): return STATE_PAUSED - if status in ('stream', 'play'): + if status in ("stream", "play"): return STATE_PLAYING return STATE_IDLE @property def media_title(self): """Title of current playing media.""" - if (self._status is None or - (self.is_grouped and not self.is_master)): + if self._status is None or (self.is_grouped and not self.is_master): return None - return self._status.get('title1', None) + return self._status.get("title1", None) @property def media_artist(self): @@ -541,34 +569,32 @@ class BluesoundPlayer(MediaPlayerDevice): if self.is_grouped and not self.is_master: return self._group_name - artist = self._status.get('artist', None) + artist = self._status.get("artist", None) if not artist: - artist = self._status.get('title2', None) + artist = self._status.get("title2", None) return artist @property def media_album_name(self): """Artist of current playing media (Music track only).""" - if (self._status is None or - (self.is_grouped and not self.is_master)): + if self._status is None or (self.is_grouped and not self.is_master): return None - album = self._status.get('album', None) + album = self._status.get("album", None) if not album: - album = self._status.get('title3', None) + album = self._status.get("title3", None) return album @property def media_image_url(self): """Image url of current playing media.""" - if (self._status is None or - (self.is_grouped and not self.is_master)): + if self._status is None or (self.is_grouped and not self.is_master): return None - url = self._status.get('image', None) + url = self._status.get("image", None) if not url: return - if url[0] == '/': + if url[0] == "/": url = "http://{}:{}{}".format(self.host, self.port, url) return url @@ -576,33 +602,30 @@ class BluesoundPlayer(MediaPlayerDevice): @property def media_position(self): """Position of current playing media in seconds.""" - if (self._status is None or - (self.is_grouped and not self.is_master)): + if self._status is None or (self.is_grouped and not self.is_master): return None mediastate = self.state if self._last_status_update is None or mediastate == STATE_IDLE: return None - position = self._status.get('secs', None) + position = self._status.get("secs", None) if position is None: return None position = float(position) if mediastate == STATE_PLAYING: - position += (dt_util.utcnow() - - self._last_status_update).total_seconds() + position += (dt_util.utcnow() - self._last_status_update).total_seconds() return position @property def media_duration(self): """Duration of current playing media in seconds.""" - if (self._status is None or - (self.is_grouped and not self.is_master)): + if self._status is None or (self.is_grouped and not self.is_master): return None - duration = self._status.get('totlen', None) + duration = self._status.get("totlen", None) if duration is None: return None return float(duration) @@ -615,9 +638,9 @@ class BluesoundPlayer(MediaPlayerDevice): @property def volume_level(self): """Volume level of the media player (0..1).""" - volume = self._status.get('volume', None) + volume = self._status.get("volume", None) if self.is_grouped: - volume = self._sync_status.get('@volume', None) + volume = self._sync_status.get("@volume", None) if volume is not None: return int(volume) / 100 @@ -644,22 +667,23 @@ class BluesoundPlayer(MediaPlayerDevice): @property def source_list(self): """List of available input sources.""" - if (self._status is None or - (self.is_grouped and not self.is_master)): + if self._status is None or (self.is_grouped and not self.is_master): return None sources = [] for source in self._preset_items: - sources.append(source['title']) + sources.append(source["title"]) - for source in [x for x in self._services_items - if x['type'] == 'LocalMusic' or - x['type'] == 'RadioService']: - sources.append(source['title']) + for source in [ + x + for x in self._services_items + if x["type"] == "LocalMusic" or x["type"] == "RadioService" + ]: + sources.append(source["title"]) for source in self._capture_items: - sources.append(source['title']) + sources.append(source["title"]) return sources @@ -668,67 +692,72 @@ class BluesoundPlayer(MediaPlayerDevice): """Name of the current input source.""" from urllib import parse - if (self._status is None or - (self.is_grouped and not self.is_master)): + if self._status is None or (self.is_grouped and not self.is_master): return None - current_service = self._status.get('service', '') - if current_service == '': - return '' - stream_url = self._status.get('streamUrl', '') + current_service = self._status.get("service", "") + if current_service == "": + return "" + stream_url = self._status.get("streamUrl", "") - if self._status.get('is_preset', '') == '1' and stream_url != '': + if self._status.get("is_preset", "") == "1" and stream_url != "": # This check doesn't work with all presets, for example playlists. # But it works with radio service_items will catch playlists. - items = [x for x in self._preset_items if 'url2' in x and - parse.unquote(x['url2']) == stream_url] + items = [ + x + for x in self._preset_items + if "url2" in x and parse.unquote(x["url2"]) == stream_url + ] if items: - return items[0]['title'] + return items[0]["title"] # This could be a bit difficult to detect. Bluetooth could be named # different things and there is not any way to match chooses in # capture list to current playing. It's a bit of guesswork. # This method will be needing some tweaking over time. - title = self._status.get('title1', '').lower() - if title == 'bluetooth' or stream_url == 'Capture:hw:2,0/44100/16/2': - items = [x for x in self._capture_items - if x['url'] == "Capture%3Abluez%3Abluetooth"] + title = self._status.get("title1", "").lower() + if title == "bluetooth" or stream_url == "Capture:hw:2,0/44100/16/2": + items = [ + x + for x in self._capture_items + if x["url"] == "Capture%3Abluez%3Abluetooth" + ] if items: - return items[0]['title'] + return items[0]["title"] - items = [x for x in self._capture_items if x['url'] == stream_url] + items = [x for x in self._capture_items if x["url"] == stream_url] if items: - return items[0]['title'] + return items[0]["title"] - if stream_url[:8] == 'Capture:': + if stream_url[:8] == "Capture:": stream_url = stream_url[8:] - idx = BluesoundPlayer._try_get_index(stream_url, ':') + idx = BluesoundPlayer._try_get_index(stream_url, ":") if idx > 0: stream_url = stream_url[:idx] for item in self._capture_items: - url = parse.unquote(item['url']) - if url[:8] == 'Capture:': + url = parse.unquote(item["url"]) + if url[:8] == "Capture:": url = url[8:] - idx = BluesoundPlayer._try_get_index(url, ':') + idx = BluesoundPlayer._try_get_index(url, ":") if idx > 0: url = url[:idx] if url.lower() == stream_url.lower(): - return item['title'] + return item["title"] - items = [x for x in self._capture_items - if x['name'] == current_service] + items = [x for x in self._capture_items if x["name"] == current_service] if items: - return items[0]['title'] + return items[0]["title"] - items = [x for x in self._services_items - if x['name'] == current_service] + items = [x for x in self._services_items if x["name"] == current_service] if items: - return items[0]['title'] + return items[0]["title"] - if self._status.get('streamUrl', '') != '': - _LOGGER.debug("Couldn't find source of stream URL: %s", - self._status.get('streamUrl', '')) + if self._status.get("streamUrl", "") != "": + _LOGGER.debug( + "Couldn't find source of stream URL: %s", + self._status.get("streamUrl", ""), + ) return None @property @@ -738,23 +767,33 @@ class BluesoundPlayer(MediaPlayerDevice): return None if self.is_grouped and not self.is_master: - return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | \ - SUPPORT_VOLUME_MUTE + return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE supported = SUPPORT_CLEAR_PLAYLIST - if self._status.get('indexing', '0') == '0': - supported = supported | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | \ - SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_SELECT_SOURCE | \ - SUPPORT_SHUFFLE_SET + if self._status.get("indexing", "0") == "0": + supported = ( + supported + | SUPPORT_PAUSE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_PLAY + | SUPPORT_SELECT_SOURCE + | SUPPORT_SHUFFLE_SET + ) current_vol = self.volume_level if current_vol is not None and current_vol >= 0: - supported = supported | SUPPORT_VOLUME_STEP | \ - SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE + supported = ( + supported + | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + ) - if self._status.get('canSeek', '') == '1': + if self._status.get("canSeek", "") == "1": supported = supported | SUPPORT_SEEK return supported @@ -772,16 +811,22 @@ class BluesoundPlayer(MediaPlayerDevice): @property def shuffle(self): """Return true if shuffle is active.""" - return self._status.get('shuffle', '0') == '1' + return self._status.get("shuffle", "0") == "1" async def async_join(self, master): """Join the player to a group.""" - master_device = [device for device in self.hass.data[DATA_BLUESOUND] - if device.entity_id == master] + master_device = [ + device + for device in self.hass.data[DATA_BLUESOUND] + if device.entity_id == master + ] if master_device: - _LOGGER.debug("Trying to join player: %s to master: %s", - self.host, master_device[0].host) + _LOGGER.debug( + "Trying to join player: %s to master: %s", + self.host, + master_device[0].host, + ) await master_device[0].async_add_slave(self) else: @@ -798,24 +843,23 @@ class BluesoundPlayer(MediaPlayerDevice): async def async_add_slave(self, slave_device): """Add slave to master.""" return await self.send_bluesound_command( - '/AddSlave?slave={}&port={}'.format( - slave_device.host, slave_device.port)) + "/AddSlave?slave={}&port={}".format(slave_device.host, slave_device.port) + ) async def async_remove_slave(self, slave_device): """Remove slave to master.""" return await self.send_bluesound_command( - '/RemoveSlave?slave={}&port={}'.format( - slave_device.host, slave_device.port)) + "/RemoveSlave?slave={}&port={}".format(slave_device.host, slave_device.port) + ) async def async_increase_timer(self): """Increase sleep time on player.""" - sleep_time = await self.send_bluesound_command('/Sleep') + sleep_time = await self.send_bluesound_command("/Sleep") if sleep_time is None: - _LOGGER.error( - "Error while increasing sleep time on player: %s", self.host) + _LOGGER.error("Error while increasing sleep time on player: %s", self.host) return 0 - return int(sleep_time.get('sleep', '0')) + return int(sleep_time.get("sleep", "0")) async def async_clear_timer(self): """Clear sleep timer on player.""" @@ -825,31 +869,31 @@ class BluesoundPlayer(MediaPlayerDevice): async def async_set_shuffle(self, shuffle): """Enable or disable shuffle mode.""" - value = '1' if shuffle else '0' - return await self.send_bluesound_command( - '/Shuffle?state={}'.format(value)) + value = "1" if shuffle else "0" + return await self.send_bluesound_command("/Shuffle?state={}".format(value)) async def async_select_source(self, source): """Select input source.""" if self.is_grouped and not self.is_master: return - items = [x for x in self._preset_items if x['title'] == source] + items = [x for x in self._preset_items if x["title"] == source] if not items: - items = [x for x in self._services_items if x['title'] == source] + items = [x for x in self._services_items if x["title"] == source] if not items: - items = [x for x in self._capture_items if x['title'] == source] + items = [x for x in self._capture_items if x["title"] == source] if not items: return selected_source = items[0] - url = 'Play?url={}&preset_id&image={}'.format( - selected_source['url'], selected_source['image']) + url = "Play?url={}&preset_id&image={}".format( + selected_source["url"], selected_source["image"] + ) - if 'is_raw_url' in selected_source and selected_source['is_raw_url']: - url = selected_source['url'] + if "is_raw_url" in selected_source and selected_source["is_raw_url"]: + url = selected_source["url"] return await self.send_bluesound_command(url) @@ -858,19 +902,18 @@ class BluesoundPlayer(MediaPlayerDevice): if self.is_grouped and not self.is_master: return - return await self.send_bluesound_command('Clear') + return await self.send_bluesound_command("Clear") async def async_media_next_track(self): """Send media_next command to media player.""" if self.is_grouped and not self.is_master: return - cmd = 'Skip' - if self._status and 'actions' in self._status: - for action in self._status['actions']['action']: - if ('@name' in action and '@url' in action and - action['@name'] == 'skip'): - cmd = action['@url'] + cmd = "Skip" + if self._status and "actions" in self._status: + for action in self._status["actions"]["action"]: + if "@name" in action and "@url" in action and action["@name"] == "skip": + cmd = action["@url"] return await self.send_bluesound_command(cmd) @@ -879,12 +922,11 @@ class BluesoundPlayer(MediaPlayerDevice): if self.is_grouped and not self.is_master: return - cmd = 'Back' - if self._status and 'actions' in self._status: - for action in self._status['actions']['action']: - if ('@name' in action and '@url' in action and - action['@name'] == 'back'): - cmd = action['@url'] + cmd = "Back" + if self._status and "actions" in self._status: + for action in self._status["actions"]["action"]: + if "@name" in action and "@url" in action and action["@name"] == "back": + cmd = action["@url"] return await self.send_bluesound_command(cmd) @@ -893,29 +935,28 @@ class BluesoundPlayer(MediaPlayerDevice): if self.is_grouped and not self.is_master: return - return await self.send_bluesound_command('Play') + return await self.send_bluesound_command("Play") async def async_media_pause(self): """Send media_pause command to media player.""" if self.is_grouped and not self.is_master: return - return await self.send_bluesound_command('Pause') + return await self.send_bluesound_command("Pause") async def async_media_stop(self): """Send stop command.""" if self.is_grouped and not self.is_master: return - return await self.send_bluesound_command('Pause') + return await self.send_bluesound_command("Pause") async def async_media_seek(self, position): """Send media_seek command to media player.""" if self.is_grouped and not self.is_master: return - return await self.send_bluesound_command( - 'Play?seek={}'.format(float(position))) + return await self.send_bluesound_command("Play?seek={}".format(float(position))) async def async_play_media(self, media_type, media_id, **kwargs): """ @@ -926,7 +967,7 @@ class BluesoundPlayer(MediaPlayerDevice): if self.is_grouped and not self.is_master: return - url = 'Play?url={}'.format(media_id) + url = "Play?url={}".format(media_id) if kwargs.get(ATTR_MEDIA_ENQUEUE): return await self.send_bluesound_command(url) @@ -938,14 +979,14 @@ class BluesoundPlayer(MediaPlayerDevice): current_vol = self.volume_level if not current_vol or current_vol < 0: return - return self.async_set_volume_level(((current_vol*100)+1)/100) + return self.async_set_volume_level(((current_vol * 100) + 1) / 100) async def async_volume_down(self): """Volume down the media player.""" current_vol = self.volume_level if not current_vol or current_vol < 0: return - return self.async_set_volume_level(((current_vol*100)-1)/100) + return self.async_set_volume_level(((current_vol * 100) - 1) / 100) async def async_set_volume_level(self, volume): """Send volume_up command to media player.""" @@ -954,7 +995,8 @@ class BluesoundPlayer(MediaPlayerDevice): elif volume > 1: volume = 1 return await self.send_bluesound_command( - 'Volume?level=' + str(float(volume) * 100)) + "Volume?level=" + str(float(volume) * 100) + ) async def async_mute_volume(self, mute): """Send mute command to media player.""" @@ -962,6 +1004,7 @@ class BluesoundPlayer(MediaPlayerDevice): volume = self.volume_level if volume > 0: self._lastvol = volume - return await self.send_bluesound_command('Volume?level=0') + return await self.send_bluesound_command("Volume?level=0") return await self.send_bluesound_command( - 'Volume?level=' + str(float(self._lastvol) * 100)) + "Volume?level=" + str(float(self._lastvol) * 100) + ) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 6b5fcd7df06..8cba3032f54 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -3,10 +3,14 @@ import logging from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker.legacy import ( - YAML_DEVICES, async_load_config + YAML_DEVICES, + async_load_config, ) from homeassistant.components.device_tracker.const import ( - CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH_LE + CONF_TRACK_NEW, + CONF_SCAN_INTERVAL, + SCAN_INTERVAL, + SOURCE_TYPE_BLUETOOTH_LE, ) from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.util.dt as dt_util @@ -14,9 +18,9 @@ from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) -DATA_BLE = 'BLE' -DATA_BLE_ADAPTER = 'ADAPTER' -BLE_PREFIX = 'BLE_' +DATA_BLE = "BLE" +DATA_BLE_ADAPTER = "ADAPTER" +BLE_PREFIX = "BLE_" MIN_SEEN_NEW = 5 @@ -24,6 +28,7 @@ def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth LE Scanner.""" # pylint: disable=import-error import pygatt + new_devices = {} hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None}) @@ -41,8 +46,7 @@ def setup_scanner(hass, config, see, discovery_info=None): """Mark a device as seen.""" if new_device: if address in new_devices: - _LOGGER.debug( - "Seen %s %s times", address, new_devices[address]) + _LOGGER.debug("Seen %s %s times", address, new_devices[address]) new_devices[address] += 1 if new_devices[address] >= MIN_SEEN_NEW: _LOGGER.debug("Adding %s to tracked devices", address) @@ -57,8 +61,11 @@ def setup_scanner(hass, config, see, discovery_info=None): if name is not None: name = name.strip("\x00") - see(mac=BLE_PREFIX + address, host_name=name, - source_type=SOURCE_TYPE_BLUETOOTH_LE) + see( + mac=BLE_PREFIX + address, + host_name=name, + source_type=SOURCE_TYPE_BLUETOOTH_LE, + ) def discover_ble_devices(): """Discover Bluetooth LE devices.""" @@ -68,7 +75,7 @@ def setup_scanner(hass, config, see, discovery_info=None): hass.data[DATA_BLE][DATA_BLE_ADAPTER] = adapter devs = adapter.scan() - devices = {x['address']: x['name'] for x in devs} + devices = {x["address"]: x["name"] for x in devs} _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) except RuntimeError as error: _LOGGER.error("Error during Bluetooth LE scan: %s", error) @@ -83,8 +90,7 @@ def setup_scanner(hass, config, see, discovery_info=None): # We just need the devices so set consider_home and home range # to 0 for device in run_coroutine_threadsafe( - async_load_config(yaml_path, hass, 0), - hass.loop + async_load_config(yaml_path, hass, 0), hass.loop ).result(): # check if device is a valid bluetooth device if device.mac and device.mac[:4].upper() == BLE_PREFIX: @@ -118,8 +124,7 @@ def setup_scanner(hass, config, see, discovery_info=None): if track_new: for address in devs: - if address not in devs_to_track and \ - address not in devs_donot_track: + if address not in devs_to_track and address not in devs_donot_track: _LOGGER.info("Discovered Bluetooth LE device %s", address) see_device(address, devs[address], new_device=True) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 7b48252c6b6..65db87fa072 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -7,31 +7,39 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker.legacy import ( - YAML_DEVICES, async_load_config + YAML_DEVICES, + async_load_config, ) from homeassistant.components.device_tracker.const import ( - CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, DEFAULT_TRACK_NEW, - SOURCE_TYPE_BLUETOOTH, DOMAIN + CONF_TRACK_NEW, + CONF_SCAN_INTERVAL, + SCAN_INTERVAL, + DEFAULT_TRACK_NEW, + SOURCE_TYPE_BLUETOOTH, + DOMAIN, ) import homeassistant.util.dt as dt_util from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) -BT_PREFIX = 'BT_' +BT_PREFIX = "BT_" -CONF_REQUEST_RSSI = 'request_rssi' +CONF_REQUEST_RSSI = "request_rssi" CONF_DEVICE_ID = "device_id" DEFAULT_DEVICE_ID = -1 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_TRACK_NEW): cv.boolean, - vol.Optional(CONF_REQUEST_RSSI): cv.boolean, - vol.Optional(CONF_DEVICE_ID, default=DEFAULT_DEVICE_ID): - vol.All(vol.Coerce(int), vol.Range(min=-1)) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_TRACK_NEW): cv.boolean, + vol.Optional(CONF_REQUEST_RSSI): cv.boolean, + vol.Optional(CONF_DEVICE_ID, default=DEFAULT_DEVICE_ID): vol.All( + vol.Coerce(int), vol.Range(min=-1) + ), + } +) def setup_scanner(hass, config, see, discovery_info=None): @@ -44,17 +52,25 @@ def setup_scanner(hass, config, see, discovery_info=None): """Mark a device as seen.""" attributes = {} if rssi is not None: - attributes['rssi'] = rssi - see(mac="{}{}".format(BT_PREFIX, mac), host_name=name, - attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH) + attributes["rssi"] = rssi + see( + mac="{}{}".format(BT_PREFIX, mac), + host_name=name, + attributes=attributes, + source_type=SOURCE_TYPE_BLUETOOTH, + ) device_id = config.get(CONF_DEVICE_ID) def discover_devices(): """Discover Bluetooth devices.""" result = bluetooth.discover_devices( - duration=8, lookup_names=True, flush_cache=True, - lookup_class=False, device_id=device_id) + duration=8, + lookup_names=True, + flush_cache=True, + lookup_class=False, + device_id=device_id, + ) _LOGGER.debug("Bluetooth devices discovered = %d", len(result)) return result @@ -66,8 +82,7 @@ def setup_scanner(hass, config, see, discovery_info=None): # We just need the devices so set consider_home and home range # to 0 for device in run_coroutine_threadsafe( - async_load_config(yaml_path, hass, 0), - hass.loop + async_load_config(yaml_path, hass, 0), hass.loop ).result(): # Check if device is a valid bluetooth device if device.mac and device.mac[:3].upper() == BT_PREFIX: @@ -80,8 +95,7 @@ def setup_scanner(hass, config, see, discovery_info=None): track_new = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) if track_new: for dev in discover_devices(): - if dev[0] not in devs_to_track and \ - dev[0] not in devs_donot_track: + if dev[0] not in devs_to_track and dev[0] not in devs_donot_track: devs_to_track.append(dev[0]) see_device(dev[0], dev[1]) @@ -92,16 +106,14 @@ def setup_scanner(hass, config, see, discovery_info=None): def update_bluetooth(_): """Update Bluetooth and set timer for the next update.""" update_bluetooth_once() - track_point_in_utc_time( - hass, update_bluetooth, dt_util.utcnow() + interval) + track_point_in_utc_time(hass, update_bluetooth, dt_util.utcnow() + interval) def update_bluetooth_once(): """Lookup Bluetooth device and update status.""" try: if track_new: for dev in discover_devices(): - if dev[0] not in devs_to_track and \ - dev[0] not in devs_donot_track: + if dev[0] not in devs_to_track and dev[0] not in devs_donot_track: devs_to_track.append(dev[0]) for mac in devs_to_track: _LOGGER.debug("Scanning %s", mac) @@ -124,7 +136,6 @@ def setup_scanner(hass, config, see, discovery_info=None): update_bluetooth(dt_util.utcnow()) - hass.services.register( - DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) + hass.services.register(DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) return True diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index 66b4ba67258..bdd91e6dfe1 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -7,26 +7,25 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS) +from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit _LOGGER = logging.getLogger(__name__) -CONF_I2C_ADDRESS = 'i2c_address' -CONF_I2C_BUS = 'i2c_bus' -CONF_OVERSAMPLING_TEMP = 'oversampling_temperature' -CONF_OVERSAMPLING_PRES = 'oversampling_pressure' -CONF_OVERSAMPLING_HUM = 'oversampling_humidity' -CONF_OPERATION_MODE = 'operation_mode' -CONF_T_STANDBY = 'time_standby' -CONF_FILTER_MODE = 'filter_mode' -CONF_DELTA_TEMP = 'delta_temperature' +CONF_I2C_ADDRESS = "i2c_address" +CONF_I2C_BUS = "i2c_bus" +CONF_OVERSAMPLING_TEMP = "oversampling_temperature" +CONF_OVERSAMPLING_PRES = "oversampling_pressure" +CONF_OVERSAMPLING_HUM = "oversampling_humidity" +CONF_OPERATION_MODE = "operation_mode" +CONF_T_STANDBY = "time_standby" +CONF_FILTER_MODE = "filter_mode" +CONF_DELTA_TEMP = "delta_temperature" -DEFAULT_NAME = 'BME280 Sensor' -DEFAULT_I2C_ADDRESS = '0x76' +DEFAULT_NAME = "BME280 Sensor" +DEFAULT_I2C_ADDRESS = "0x76" DEFAULT_I2C_BUS = 1 DEFAULT_OVERSAMPLING_TEMP = 1 # Temperature oversampling x 1 DEFAULT_OVERSAMPLING_PRES = 1 # Pressure oversampling x 1 @@ -34,45 +33,48 @@ DEFAULT_OVERSAMPLING_HUM = 1 # Humidity oversampling x 1 DEFAULT_OPERATION_MODE = 3 # Normal mode (forced mode: 2) DEFAULT_T_STANDBY = 5 # Tstandby 5ms DEFAULT_FILTER_MODE = 0 # Filter off -DEFAULT_DELTA_TEMP = 0. +DEFAULT_DELTA_TEMP = 0.0 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=3) -SENSOR_TEMP = 'temperature' -SENSOR_HUMID = 'humidity' -SENSOR_PRESS = 'pressure' +SENSOR_TEMP = "temperature" +SENSOR_HUMID = "humidity" +SENSOR_PRESS = "pressure" SENSOR_TYPES = { - SENSOR_TEMP: ['Temperature', None], - SENSOR_HUMID: ['Humidity', '%'], - SENSOR_PRESS: ['Pressure', 'mb'] + SENSOR_TEMP: ["Temperature", None], + SENSOR_HUMID: ["Humidity", "%"], + SENSOR_PRESS: ["Pressure", "mb"], } DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=DEFAULT_MONITORED): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), - vol.Optional(CONF_OVERSAMPLING_TEMP, - default=DEFAULT_OVERSAMPLING_TEMP): vol.Coerce(int), - vol.Optional(CONF_OVERSAMPLING_PRES, - default=DEFAULT_OVERSAMPLING_PRES): vol.Coerce(int), - vol.Optional(CONF_OVERSAMPLING_HUM, - default=DEFAULT_OVERSAMPLING_HUM): vol.Coerce(int), - vol.Optional(CONF_OPERATION_MODE, - default=DEFAULT_OPERATION_MODE): vol.Coerce(int), - vol.Optional(CONF_T_STANDBY, - default=DEFAULT_T_STANDBY): vol.Coerce(int), - vol.Optional(CONF_FILTER_MODE, - default=DEFAULT_FILTER_MODE): vol.Coerce(int), - vol.Optional(CONF_DELTA_TEMP, - default=DEFAULT_DELTA_TEMP): vol.Coerce(float), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=DEFAULT_MONITORED): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), + vol.Optional( + CONF_OVERSAMPLING_TEMP, default=DEFAULT_OVERSAMPLING_TEMP + ): vol.Coerce(int), + vol.Optional( + CONF_OVERSAMPLING_PRES, default=DEFAULT_OVERSAMPLING_PRES + ): vol.Coerce(int), + vol.Optional( + CONF_OVERSAMPLING_HUM, default=DEFAULT_OVERSAMPLING_HUM + ): vol.Coerce(int), + vol.Optional(CONF_OPERATION_MODE, default=DEFAULT_OPERATION_MODE): vol.Coerce( + int + ), + vol.Optional(CONF_T_STANDBY, default=DEFAULT_T_STANDBY): vol.Coerce(int), + vol.Optional(CONF_FILTER_MODE, default=DEFAULT_FILTER_MODE): vol.Coerce(int), + vol.Optional(CONF_DELTA_TEMP, default=DEFAULT_DELTA_TEMP): vol.Coerce(float), + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BME280 sensor.""" import smbus # pylint: disable=import-error from i2csense.bme280 import BME280 # pylint: disable=import-error @@ -83,15 +85,19 @@ async def async_setup_platform(hass, config, async_add_entities, bus = smbus.SMBus(config.get(CONF_I2C_BUS)) sensor = await hass.async_add_job( - partial(BME280, bus, i2c_address, - osrs_t=config.get(CONF_OVERSAMPLING_TEMP), - osrs_p=config.get(CONF_OVERSAMPLING_PRES), - osrs_h=config.get(CONF_OVERSAMPLING_HUM), - mode=config.get(CONF_OPERATION_MODE), - t_sb=config.get(CONF_T_STANDBY), - filter_mode=config.get(CONF_FILTER_MODE), - delta_temp=config.get(CONF_DELTA_TEMP), - logger=_LOGGER) + partial( + BME280, + bus, + i2c_address, + osrs_t=config.get(CONF_OVERSAMPLING_TEMP), + osrs_p=config.get(CONF_OVERSAMPLING_PRES), + osrs_h=config.get(CONF_OVERSAMPLING_HUM), + mode=config.get(CONF_OPERATION_MODE), + t_sb=config.get(CONF_T_STANDBY), + filter_mode=config.get(CONF_FILTER_MODE), + delta_temp=config.get(CONF_DELTA_TEMP), + logger=_LOGGER, + ) ) if not sensor.sample_ok: _LOGGER.error("BME280 sensor not detected at %s", i2c_address) @@ -102,8 +108,9 @@ async def async_setup_platform(hass, config, async_add_entities, dev = [] try: for variable in config[CONF_MONITORED_CONDITIONS]: - dev.append(BME280Sensor( - sensor_handler, variable, SENSOR_TYPES[variable][1], name)) + dev.append( + BME280Sensor(sensor_handler, variable, SENSOR_TYPES[variable][1], name) + ) except KeyError: pass @@ -140,7 +147,7 @@ class BME280Sensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self.client_name, self._name) + return "{} {}".format(self.client_name, self._name) @property def state(self): diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 73fe827be6b..58b343b3de0 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -8,28 +8,27 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS) +from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.util.temperature import celsius_to_fahrenheit _LOGGER = logging.getLogger(__name__) -CONF_I2C_ADDRESS = 'i2c_address' -CONF_I2C_BUS = 'i2c_bus' -CONF_OVERSAMPLING_TEMP = 'oversampling_temperature' -CONF_OVERSAMPLING_PRES = 'oversampling_pressure' -CONF_OVERSAMPLING_HUM = 'oversampling_humidity' -CONF_FILTER_SIZE = 'filter_size' -CONF_GAS_HEATER_TEMP = 'gas_heater_temperature' -CONF_GAS_HEATER_DURATION = 'gas_heater_duration' -CONF_AQ_BURN_IN_TIME = 'aq_burn_in_time' -CONF_AQ_HUM_BASELINE = 'aq_humidity_baseline' -CONF_AQ_HUM_WEIGHTING = 'aq_humidity_bias' -CONF_TEMP_OFFSET = 'temp_offset' +CONF_I2C_ADDRESS = "i2c_address" +CONF_I2C_BUS = "i2c_bus" +CONF_OVERSAMPLING_TEMP = "oversampling_temperature" +CONF_OVERSAMPLING_PRES = "oversampling_pressure" +CONF_OVERSAMPLING_HUM = "oversampling_humidity" +CONF_FILTER_SIZE = "filter_size" +CONF_GAS_HEATER_TEMP = "gas_heater_temperature" +CONF_GAS_HEATER_DURATION = "gas_heater_duration" +CONF_AQ_BURN_IN_TIME = "aq_burn_in_time" +CONF_AQ_HUM_BASELINE = "aq_humidity_baseline" +CONF_AQ_HUM_WEIGHTING = "aq_humidity_bias" +CONF_TEMP_OFFSET = "temp_offset" -DEFAULT_NAME = 'BME680 Sensor' +DEFAULT_NAME = "BME680 Sensor" DEFAULT_I2C_ADDRESS = 0x77 DEFAULT_I2C_BUS = 1 DEFAULT_OVERSAMPLING_TEMP = 8 # Temperature oversampling x 8 @@ -43,55 +42,65 @@ DEFAULT_AQ_HUM_BASELINE = 40 # 40%, an optimal indoor humidity. DEFAULT_AQ_HUM_WEIGHTING = 25 # 25% Weighting of humidity to gas in AQ score DEFAULT_TEMP_OFFSET = 0 # No calibration out of the box. -SENSOR_TEMP = 'temperature' -SENSOR_HUMID = 'humidity' -SENSOR_PRESS = 'pressure' -SENSOR_GAS = 'gas' -SENSOR_AQ = 'airquality' +SENSOR_TEMP = "temperature" +SENSOR_HUMID = "humidity" +SENSOR_PRESS = "pressure" +SENSOR_GAS = "gas" +SENSOR_AQ = "airquality" SENSOR_TYPES = { - SENSOR_TEMP: ['Temperature', None], - SENSOR_HUMID: ['Humidity', '%'], - SENSOR_PRESS: ['Pressure', 'mb'], - SENSOR_GAS: ['Gas Resistance', 'Ohms'], - SENSOR_AQ: ['Air Quality', '%'] + SENSOR_TEMP: ["Temperature", None], + SENSOR_HUMID: ["Humidity", "%"], + SENSOR_PRESS: ["Pressure", "mb"], + SENSOR_GAS: ["Gas Resistance", "Ohms"], + SENSOR_AQ: ["Air Quality", "%"], } DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS, SENSOR_AQ] OVERSAMPLING_VALUES = set([0, 1, 2, 4, 8, 16]) FILTER_VALUES = set([0, 1, 3, 7, 15, 31, 63, 127]) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): - cv.positive_int, - vol.Optional(CONF_MONITORED_CONDITIONS, default=DEFAULT_MONITORED): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, - vol.Optional(CONF_OVERSAMPLING_TEMP, default=DEFAULT_OVERSAMPLING_TEMP): - vol.All(vol.Coerce(int), vol.In(OVERSAMPLING_VALUES)), - vol.Optional(CONF_OVERSAMPLING_PRES, default=DEFAULT_OVERSAMPLING_PRES): - vol.All(vol.Coerce(int), vol.In(OVERSAMPLING_VALUES)), - vol.Optional(CONF_OVERSAMPLING_HUM, default=DEFAULT_OVERSAMPLING_HUM): - vol.All(vol.Coerce(int), vol.In(OVERSAMPLING_VALUES)), - vol.Optional(CONF_FILTER_SIZE, default=DEFAULT_FILTER_SIZE): - vol.All(vol.Coerce(int), vol.In(FILTER_VALUES)), - vol.Optional(CONF_GAS_HEATER_TEMP, default=DEFAULT_GAS_HEATER_TEMP): - vol.All(vol.Coerce(int), vol.Range(200, 400)), - vol.Optional(CONF_GAS_HEATER_DURATION, - default=DEFAULT_GAS_HEATER_DURATION): - vol.All(vol.Coerce(int), vol.Range(1, 4032)), - vol.Optional(CONF_AQ_BURN_IN_TIME, default=DEFAULT_AQ_BURN_IN_TIME): - cv.positive_int, - vol.Optional(CONF_AQ_HUM_BASELINE, default=DEFAULT_AQ_HUM_BASELINE): - vol.All(vol.Coerce(int), vol.Range(1, 100)), - vol.Optional(CONF_AQ_HUM_WEIGHTING, default=DEFAULT_AQ_HUM_WEIGHTING): - vol.All(vol.Coerce(int), vol.Range(1, 100)), - vol.Optional(CONF_TEMP_OFFSET, default=DEFAULT_TEMP_OFFSET): - vol.All(vol.Coerce(float), vol.Range(-100.0, 100.0)), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.positive_int, + vol.Optional(CONF_MONITORED_CONDITIONS, default=DEFAULT_MONITORED): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, + vol.Optional( + CONF_OVERSAMPLING_TEMP, default=DEFAULT_OVERSAMPLING_TEMP + ): vol.All(vol.Coerce(int), vol.In(OVERSAMPLING_VALUES)), + vol.Optional( + CONF_OVERSAMPLING_PRES, default=DEFAULT_OVERSAMPLING_PRES + ): vol.All(vol.Coerce(int), vol.In(OVERSAMPLING_VALUES)), + vol.Optional(CONF_OVERSAMPLING_HUM, default=DEFAULT_OVERSAMPLING_HUM): vol.All( + vol.Coerce(int), vol.In(OVERSAMPLING_VALUES) + ), + vol.Optional(CONF_FILTER_SIZE, default=DEFAULT_FILTER_SIZE): vol.All( + vol.Coerce(int), vol.In(FILTER_VALUES) + ), + vol.Optional(CONF_GAS_HEATER_TEMP, default=DEFAULT_GAS_HEATER_TEMP): vol.All( + vol.Coerce(int), vol.Range(200, 400) + ), + vol.Optional( + CONF_GAS_HEATER_DURATION, default=DEFAULT_GAS_HEATER_DURATION + ): vol.All(vol.Coerce(int), vol.Range(1, 4032)), + vol.Optional( + CONF_AQ_BURN_IN_TIME, default=DEFAULT_AQ_BURN_IN_TIME + ): cv.positive_int, + vol.Optional(CONF_AQ_HUM_BASELINE, default=DEFAULT_AQ_HUM_BASELINE): vol.All( + vol.Coerce(int), vol.Range(1, 100) + ), + vol.Optional(CONF_AQ_HUM_WEIGHTING, default=DEFAULT_AQ_HUM_WEIGHTING): vol.All( + vol.Coerce(int), vol.Range(1, 100) + ), + vol.Optional(CONF_TEMP_OFFSET, default=DEFAULT_TEMP_OFFSET): vol.All( + vol.Coerce(float), vol.Range(-100.0, 100.0) + ), + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BME680 sensor.""" SENSOR_TYPES[SENSOR_TEMP][1] = hass.config.units.temperature_unit name = config.get(CONF_NAME) @@ -102,8 +111,9 @@ async def async_setup_platform(hass, config, async_add_entities, dev = [] for variable in config[CONF_MONITORED_CONDITIONS]: - dev.append(BME680Sensor( - sensor_handler, variable, SENSOR_TYPES[variable][1], name)) + dev.append( + BME680Sensor(sensor_handler, variable, SENSOR_TYPES[variable][1], name) + ) async_add_entities(dev) return @@ -112,7 +122,8 @@ async def async_setup_platform(hass, config, async_add_entities, def _setup_bme680(config): """Set up and configure the BME680 sensor.""" from smbus import SMBus # pylint: disable=import-error - bme680 = importlib.import_module('bme680') + + bme680 = importlib.import_module("bme680") sensor_handler = None sensor = None @@ -129,20 +140,12 @@ def _setup_bme680(config): 2: bme680.OS_2X, 4: bme680.OS_4X, 8: bme680.OS_8X, - 16: bme680.OS_16X + 16: bme680.OS_16X, } - sensor.set_temperature_oversample( - os_lookup[config.get(CONF_OVERSAMPLING_TEMP)] - ) - sensor.set_temp_offset( - config.get(CONF_TEMP_OFFSET) - ) - sensor.set_humidity_oversample( - os_lookup[config.get(CONF_OVERSAMPLING_HUM)] - ) - sensor.set_pressure_oversample( - os_lookup[config.get(CONF_OVERSAMPLING_PRES)] - ) + sensor.set_temperature_oversample(os_lookup[config.get(CONF_OVERSAMPLING_TEMP)]) + sensor.set_temp_offset(config.get(CONF_TEMP_OFFSET)) + sensor.set_humidity_oversample(os_lookup[config.get(CONF_OVERSAMPLING_HUM)]) + sensor.set_pressure_oversample(os_lookup[config.get(CONF_OVERSAMPLING_PRES)]) # Configure IIR Filter filter_lookup = { @@ -153,16 +156,14 @@ def _setup_bme680(config): 15: bme680.FILTER_SIZE_15, 31: bme680.FILTER_SIZE_31, 63: bme680.FILTER_SIZE_63, - 127: bme680.FILTER_SIZE_127 + 127: bme680.FILTER_SIZE_127, } - sensor.set_filter( - filter_lookup[config.get(CONF_FILTER_SIZE)] - ) + sensor.set_filter(filter_lookup[config.get(CONF_FILTER_SIZE)]) # Configure the Gas Heater if ( - SENSOR_GAS in config[CONF_MONITORED_CONDITIONS] or - SENSOR_AQ in config[CONF_MONITORED_CONDITIONS] + SENSOR_GAS in config[CONF_MONITORED_CONDITIONS] + or SENSOR_AQ in config[CONF_MONITORED_CONDITIONS] ): sensor.set_gas_status(bme680.ENABLE_GAS_MEAS) sensor.set_gas_heater_duration(config[CONF_GAS_HEATER_DURATION]) @@ -176,11 +177,13 @@ def _setup_bme680(config): sensor_handler = BME680Handler( sensor, - (SENSOR_GAS in config[CONF_MONITORED_CONDITIONS] or - SENSOR_AQ in config[CONF_MONITORED_CONDITIONS]), + ( + SENSOR_GAS in config[CONF_MONITORED_CONDITIONS] + or SENSOR_AQ in config[CONF_MONITORED_CONDITIONS] + ), config[CONF_AQ_BURN_IN_TIME], config[CONF_AQ_HUM_BASELINE], - config[CONF_AQ_HUM_WEIGHTING] + config[CONF_AQ_HUM_WEIGHTING], ) sleep(0.5) # Wait for device to stabilize if not sensor_handler.sensor_data.temperature: @@ -205,8 +208,12 @@ class BME680Handler: self.air_quality = None def __init__( - self, sensor, gas_measurement=False, - burn_in_time=300, hum_baseline=40, hum_weighting=25 + self, + sensor, + gas_measurement=False, + burn_in_time=300, + hum_baseline=40, + hum_weighting=25, ): """Initialize the sensor handler.""" self.sensor_data = BME680Handler.SensorData() @@ -218,10 +225,11 @@ class BME680Handler: if gas_measurement: import threading + threading.Thread( target=self._run_gas_sensor, - kwargs={'burn_in_time': burn_in_time}, - name='BME680Handler_run_gas_sensor' + kwargs={"burn_in_time": burn_in_time}, + name="BME680Handler_run_gas_sensor", ).start() self.update(first_read=True) @@ -239,34 +247,31 @@ class BME680Handler: curr_time = time() burn_in_data = [] - _LOGGER.info("Beginning %d second gas sensor burn in for Air Quality", - burn_in_time) + _LOGGER.info( + "Beginning %d second gas sensor burn in for Air Quality", burn_in_time + ) while curr_time - start_time < burn_in_time: curr_time = time() - if ( - self._sensor.get_sensor_data() and - self._sensor.data.heat_stable - ): + if self._sensor.get_sensor_data() and self._sensor.data.heat_stable: gas_resistance = self._sensor.data.gas_resistance burn_in_data.append(gas_resistance) self.sensor_data.gas_resistance = gas_resistance - _LOGGER.debug("AQ Gas Resistance Baseline reading %2f Ohms", - gas_resistance) + _LOGGER.debug( + "AQ Gas Resistance Baseline reading %2f Ohms", gas_resistance + ) sleep(1) - _LOGGER.debug("AQ Gas Resistance Burn In Data (Size: %d): \n\t%s", - len(burn_in_data), burn_in_data) + _LOGGER.debug( + "AQ Gas Resistance Burn In Data (Size: %d): \n\t%s", + len(burn_in_data), + burn_in_data, + ) self._gas_baseline = sum(burn_in_data[-50:]) / 50.0 _LOGGER.info("Completed gas sensor burn in for Air Quality") _LOGGER.info("AQ Gas Resistance Baseline: %f", self._gas_baseline) while True: - if ( - self._sensor.get_sensor_data() and - self._sensor.data.heat_stable - ): - self.sensor_data.gas_resistance = ( - self._sensor.data.gas_resistance - ) + if self._sensor.get_sensor_data() and self._sensor.data.heat_stable: + self.sensor_data.gas_resistance = self._sensor.data.gas_resistance self.sensor_data.air_quality = self._calculate_aq_score() sleep(1) @@ -295,16 +300,10 @@ class BME680Handler: # Calculate hum_score as the distance from the hum_baseline. if hum_offset > 0: hum_score = ( - (100 - hum_baseline - hum_offset) / - (100 - hum_baseline) * - hum_weighting + (100 - hum_baseline - hum_offset) / (100 - hum_baseline) * hum_weighting ) else: - hum_score = ( - (hum_baseline + hum_offset) / - hum_baseline * - hum_weighting - ) + hum_score = (hum_baseline + hum_offset) / hum_baseline * hum_weighting # Calculate gas_score as the distance from the gas_baseline. if gas_offset > 0: @@ -332,7 +331,7 @@ class BME680Sensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self.client_name, self._name) + return "{} {}".format(self.client_name, self._name) @property def state(self): @@ -357,9 +356,7 @@ class BME680Sensor(Entity): elif self.type == SENSOR_PRESS: self._state = round(self.bme680_client.sensor_data.pressure, 1) elif self.type == SENSOR_GAS: - self._state = int( - round(self.bme680_client.sensor_data.gas_resistance, 0) - ) + self._state = int(round(self.bme680_client.sensor_data.gas_resistance, 0)) elif self.type == SENSOR_AQ: aq_score = self.bme680_client.sensor_data.air_quality if aq_score is not None: diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 10c58696740..9b44012e758 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -4,46 +4,41 @@ import logging import voluptuous as vol -from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD) +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DOMAIN = 'bmw_connected_drive' -CONF_REGION = 'region' -CONF_READ_ONLY = 'read_only' -ATTR_VIN = 'vin' +DOMAIN = "bmw_connected_drive" +CONF_REGION = "region" +CONF_READ_ONLY = "read_only" +ATTR_VIN = "vin" -ACCOUNT_SCHEMA = vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_REGION): vol.Any('north_america', 'china', - 'rest_of_world'), - vol.Optional(CONF_READ_ONLY, default=False): cv.boolean, -}) +ACCOUNT_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_REGION): vol.Any("north_america", "china", "rest_of_world"), + vol.Optional(CONF_READ_ONLY, default=False): cv.boolean, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: { - cv.string: ACCOUNT_SCHEMA - }, -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: ACCOUNT_SCHEMA}}, extra=vol.ALLOW_EXTRA) -SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_VIN): cv.string, -}) +SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string}) -BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor'] +BMW_COMPONENTS = ["binary_sensor", "device_tracker", "lock", "sensor"] UPDATE_INTERVAL = 5 # in minutes -SERVICE_UPDATE_STATE = 'update_state' +SERVICE_UPDATE_STATE = "update_state" _SERVICE_MAP = { - 'light_flash': 'trigger_remote_light_flash', - 'sound_horn': 'trigger_remote_horn', - 'activate_air_conditioning': 'trigger_remote_air_conditioning', + "light_flash": "trigger_remote_light_flash", + "sound_horn": "trigger_remote_horn", + "activate_air_conditioning": "trigger_remote_air_conditioning", } @@ -71,17 +66,15 @@ def setup(hass, config: dict): return True -def setup_account(account_config: dict, hass, name: str) \ - -> 'BMWConnectedDriveAccount': +def setup_account(account_config: dict, hass, name: str) -> "BMWConnectedDriveAccount": """Set up a new BMWConnectedDriveAccount based on the config.""" username = account_config[CONF_USERNAME] password = account_config[CONF_PASSWORD] region = account_config[CONF_REGION] read_only = account_config[CONF_READ_ONLY] - _LOGGER.debug('Adding new account %s', name) - cd_account = BMWConnectedDriveAccount( - username, password, region, name, read_only) + _LOGGER.debug("Adding new account %s", name) + cd_account = BMWConnectedDriveAccount(username, password, region, name, read_only) def execute_service(call): """Execute a service for a vehicle. @@ -97,19 +90,23 @@ def setup_account(account_config: dict, hass, name: str) \ function_name = _SERVICE_MAP[call.service] function_call = getattr(vehicle.remote_services, function_name) function_call() + if not read_only: # register the remote services for service in _SERVICE_MAP: hass.services.register( - DOMAIN, service, execute_service, schema=SERVICE_SCHEMA) + DOMAIN, service, execute_service, schema=SERVICE_SCHEMA + ) # update every UPDATE_INTERVAL minutes, starting now # this should even out the load on the servers now = datetime.datetime.now() track_utc_time_change( - hass, cd_account.update, + hass, + cd_account.update, minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL), - second=now.second) + second=now.second, + ) return cd_account @@ -117,8 +114,9 @@ def setup_account(account_config: dict, hass, name: str) \ class BMWConnectedDriveAccount: """Representation of a BMW vehicle.""" - def __init__(self, username: str, password: str, region_str: str, - name: str, read_only) -> None: + def __init__( + self, username: str, password: str, region_str: str, name: str, read_only + ) -> None: """Constructor.""" from bimmer_connected.account import ConnectedDriveAccount from bimmer_connected.country_selector import get_region_from_name @@ -137,7 +135,9 @@ class BMWConnectedDriveAccount: """ _LOGGER.debug( "Updating vehicle state for account %s, notifying %d listeners", - self.name, len(self._update_listeners)) + self.name, + len(self._update_listeners), + ) try: self.account.update_vehicle_states() for listener in self._update_listeners: diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 8769fcf7d62..d52bec330fb 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -9,17 +9,17 @@ from . import DOMAIN as BMW_DOMAIN _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'lids': ['Doors', 'opening'], - 'windows': ['Windows', 'opening'], - 'door_lock_state': ['Door lock state', 'safety'], - 'lights_parking': ['Parking lights', 'light'], - 'condition_based_services': ['Condition based services', 'problem'], - 'check_control_messages': ['Control messages', 'problem'] + "lids": ["Doors", "opening"], + "windows": ["Windows", "opening"], + "door_lock_state": ["Door lock state", "safety"], + "lights_parking": ["Parking lights", "light"], + "condition_based_services": ["Condition based services", "problem"], + "check_control_messages": ["Control messages", "problem"], } SENSOR_TYPES_ELEC = { - 'charging_status': ['Charging status', 'power'], - 'connection_status': ['Connection status', 'plug'] + "charging_status": ["Charging status", "power"], + "connection_status": ["Connection status", "plug"], } SENSOR_TYPES_ELEC.update(SENSOR_TYPES) @@ -28,22 +28,23 @@ SENSOR_TYPES_ELEC.update(SENSOR_TYPES) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the BMW sensors.""" accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug('Found BMW accounts: %s', - ', '.join([a.name for a in accounts])) + _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) devices = [] for account in accounts: for vehicle in account.account.vehicles: if vehicle.has_hv_battery: - _LOGGER.debug('BMW with a high voltage battery') + _LOGGER.debug("BMW with a high voltage battery") for key, value in sorted(SENSOR_TYPES_ELEC.items()): device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1]) + account, vehicle, key, value[0], value[1] + ) devices.append(device) elif vehicle.has_internal_combustion_engine: - _LOGGER.debug('BMW with an internal combustion engine') + _LOGGER.debug("BMW with an internal combustion engine") for key, value in sorted(SENSOR_TYPES.items()): device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1]) + account, vehicle, key, value[0], value[1] + ) devices.append(device) add_entities(devices, True) @@ -51,14 +52,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BMWConnectedDriveSensor(BinarySensorDevice): """Representation of a BMW vehicle binary sensor.""" - def __init__(self, account, vehicle, attribute: str, sensor_name, - device_class): + def __init__(self, account, vehicle, attribute: str, sensor_name, device_class): """Constructor.""" self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = '{} {}'.format(self._vehicle.name, self._attribute) - self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) + self._name = "{} {}".format(self._vehicle.name, self._attribute) + self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) self._sensor_name = sensor_name self._device_class = device_class self._state = None @@ -95,43 +95,40 @@ class BMWConnectedDriveSensor(BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the binary sensor.""" vehicle_state = self._vehicle.state - result = { - 'car': self._vehicle.name - } + result = {"car": self._vehicle.name} - if self._attribute == 'lids': + if self._attribute == "lids": for lid in vehicle_state.lids: result[lid.name] = lid.state.value - elif self._attribute == 'windows': + elif self._attribute == "windows": for window in vehicle_state.windows: result[window.name] = window.state.value - elif self._attribute == 'door_lock_state': - result['door_lock_state'] = vehicle_state.door_lock_state.value - result['last_update_reason'] = vehicle_state.last_update_reason - elif self._attribute == 'lights_parking': - result['lights_parking'] = vehicle_state.parking_lights.value - elif self._attribute == 'condition_based_services': + elif self._attribute == "door_lock_state": + result["door_lock_state"] = vehicle_state.door_lock_state.value + result["last_update_reason"] = vehicle_state.last_update_reason + elif self._attribute == "lights_parking": + result["lights_parking"] = vehicle_state.parking_lights.value + elif self._attribute == "condition_based_services": for report in vehicle_state.condition_based_services: - result.update( - self._format_cbs_report(report)) - elif self._attribute == 'check_control_messages': + result.update(self._format_cbs_report(report)) + elif self._attribute == "check_control_messages": check_control_messages = vehicle_state.check_control_messages if not check_control_messages: - result['check_control_messages'] = 'OK' + result["check_control_messages"] = "OK" else: cbs_list = [] for message in check_control_messages: - cbs_list.append(message['ccmDescriptionShort']) - result['check_control_messages'] = cbs_list - elif self._attribute == 'charging_status': - result['charging_status'] = vehicle_state.charging_status.value + cbs_list.append(message["ccmDescriptionShort"]) + result["check_control_messages"] = cbs_list + elif self._attribute == "charging_status": + result["charging_status"] = vehicle_state.charging_status.value # pylint: disable=protected-access - result['last_charging_end_result'] = \ - vehicle_state._attributes['lastChargingEndResult'] - if self._attribute == 'connection_status': + result["last_charging_end_result"] = vehicle_state._attributes[ + "lastChargingEndResult" + ] + if self._attribute == "connection_status": # pylint: disable=protected-access - result['connection_status'] = \ - vehicle_state._attributes['connectionStatus'] + result["connection_status"] = vehicle_state._attributes["connectionStatus"] return sorted(result.items()) @@ -139,50 +136,54 @@ class BMWConnectedDriveSensor(BinarySensorDevice): """Read new state data from the library.""" from bimmer_connected.state import LockState from bimmer_connected.state import ChargingState + vehicle_state = self._vehicle.state # device class opening: On means open, Off means closed - if self._attribute == 'lids': + if self._attribute == "lids": _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) self._state = not vehicle_state.all_lids_closed - if self._attribute == 'windows': + if self._attribute == "windows": self._state = not vehicle_state.all_windows_closed # device class safety: On means unsafe, Off means safe - if self._attribute == 'door_lock_state': + if self._attribute == "door_lock_state": # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED - self._state = vehicle_state.door_lock_state not in \ - [LockState.LOCKED, LockState.SECURED] + self._state = vehicle_state.door_lock_state not in [ + LockState.LOCKED, + LockState.SECURED, + ] # device class light: On means light detected, Off means no light - if self._attribute == 'lights_parking': + if self._attribute == "lights_parking": self._state = vehicle_state.are_parking_lights_on # device class problem: On means problem detected, Off means no problem - if self._attribute == 'condition_based_services': + if self._attribute == "condition_based_services": self._state = not vehicle_state.are_all_cbs_ok - if self._attribute == 'check_control_messages': + if self._attribute == "check_control_messages": self._state = vehicle_state.has_check_control_messages # device class power: On means power detected, Off means no power - if self._attribute == 'charging_status': - self._state = vehicle_state.charging_status in \ - [ChargingState.CHARGING] + if self._attribute == "charging_status": + self._state = vehicle_state.charging_status in [ChargingState.CHARGING] # device class plug: On means device is plugged in, # Off means device is unplugged - if self._attribute == 'connection_status': + if self._attribute == "connection_status": # pylint: disable=protected-access - self._state = (vehicle_state._attributes['connectionStatus'] == - 'CONNECTED') + self._state = vehicle_state._attributes["connectionStatus"] == "CONNECTED" def _format_cbs_report(self, report): result = {} - service_type = report.service_type.lower().replace('_', ' ') - result['{} status'.format(service_type)] = report.state.value + service_type = report.service_type.lower().replace("_", " ") + result["{} status".format(service_type)] = report.state.value if report.due_date is not None: - result['{} date'.format(service_type)] = \ - report.due_date.strftime('%Y-%m-%d') + result["{} date".format(service_type)] = report.due_date.strftime( + "%Y-%m-%d" + ) if report.due_distance is not None: - distance = round(self.hass.config.units.length( - report.due_distance, LENGTH_KILOMETERS)) - result['{} distance'.format(service_type)] = '{} {}'.format( - distance, self.hass.config.units.length_unit) + distance = round( + self.hass.config.units.length(report.due_distance, LENGTH_KILOMETERS) + ) + result["{} distance".format(service_type)] = "{} {}".format( + distance, self.hass.config.units.length_unit + ) return result def update_callback(self): diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index 229488186ae..c4e835af0eb 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -11,8 +11,7 @@ _LOGGER = logging.getLogger(__name__) def setup_scanner(hass, config, see, discovery_info=None): """Set up the BMW tracker.""" accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug('Found BMW accounts: %s', - ', '.join([a.name for a in accounts])) + _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) for account in accounts: for vehicle in account.account.vehicles: tracker = BMWDeviceTracker(see, vehicle) @@ -38,15 +37,15 @@ class BMWDeviceTracker: dev_id = slugify(self.vehicle.name) if not self.vehicle.state.is_vehicle_tracking_enabled: - _LOGGER.debug('Tracking is disabled for vehicle %s', dev_id) + _LOGGER.debug("Tracking is disabled for vehicle %s", dev_id) return - _LOGGER.debug('Updating %s', dev_id) - attrs = { - 'vin': self.vehicle.vin, - } + _LOGGER.debug("Updating %s", dev_id) + attrs = {"vin": self.vehicle.vin} self._see( - dev_id=dev_id, host_name=self.vehicle.name, - gps=self.vehicle.state.gps_position, attributes=attrs, - icon='mdi:car' + dev_id=dev_id, + host_name=self.vehicle.name, + gps=self.vehicle.state.gps_position, + attributes=attrs, + icon="mdi:car", ) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 455e1427b05..a16dbc6b341 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -12,13 +12,12 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the BMW Connected Drive lock.""" accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug('Found BMW accounts: %s', - ', '.join([a.name for a in accounts])) + _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) devices = [] for account in accounts: if not account.read_only: for vehicle in account.account.vehicles: - device = BMWLock(account, vehicle, 'lock', 'BMW lock') + device = BMWLock(account, vehicle, "lock", "BMW lock") devices.append(device) add_entities(devices, True) @@ -31,8 +30,8 @@ class BMWLock(LockDevice): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = '{} {}'.format(self._vehicle.name, self._attribute) - self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) + self._name = "{} {}".format(self._vehicle.name, self._attribute) + self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) self._sensor_name = sensor_name self._state = None @@ -59,8 +58,8 @@ class BMWLock(LockDevice): """Return the state attributes of the lock.""" vehicle_state = self._vehicle.state return { - 'car': self._vehicle.name, - 'door_lock_state': vehicle_state.door_lock_state.value + "car": self._vehicle.name, + "door_lock_state": vehicle_state.door_lock_state.value, } @property @@ -90,15 +89,15 @@ class BMWLock(LockDevice): """Update state of the lock.""" from bimmer_connected.state import LockState - _LOGGER.debug("%s: updating data for %s", self._vehicle.name, - self._attribute) + _LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute) vehicle_state = self._vehicle.state # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED - self._state = STATE_LOCKED \ - if vehicle_state.door_lock_state \ - in [LockState.LOCKED, LockState.SECURED] \ + self._state = ( + STATE_LOCKED + if vehicle_state.door_lock_state in [LockState.LOCKED, LockState.SECURED] else STATE_UNLOCKED + ) def update_callback(self): """Schedule a state update.""" diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 67bfac91052..eec81aa6525 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -7,6 +7,5 @@ ], "dependencies": [], "codeowners": [ - "@ChristianKuehnel" ] } diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 4d8b7adde1b..bc133fa4034 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -2,8 +2,12 @@ import logging from homeassistant.const import ( - CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, VOLUME_GALLONS, - VOLUME_LITERS) + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, + LENGTH_MILES, + VOLUME_GALLONS, + VOLUME_LITERS, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -12,25 +16,25 @@ from . import DOMAIN as BMW_DOMAIN _LOGGER = logging.getLogger(__name__) ATTR_TO_HA_METRIC = { - 'mileage': ['mdi:speedometer', LENGTH_KILOMETERS], - 'remaining_range_total': ['mdi:ruler', LENGTH_KILOMETERS], - 'remaining_range_electric': ['mdi:ruler', LENGTH_KILOMETERS], - 'remaining_range_fuel': ['mdi:ruler', LENGTH_KILOMETERS], - 'max_range_electric': ['mdi:ruler', LENGTH_KILOMETERS], - 'remaining_fuel': ['mdi:gas-station', VOLUME_LITERS], - 'charging_time_remaining': ['mdi:update', 'h'], - 'charging_status': ['mdi:battery-charging', None], + "mileage": ["mdi:speedometer", LENGTH_KILOMETERS], + "remaining_range_total": ["mdi:ruler", LENGTH_KILOMETERS], + "remaining_range_electric": ["mdi:ruler", LENGTH_KILOMETERS], + "remaining_range_fuel": ["mdi:ruler", LENGTH_KILOMETERS], + "max_range_electric": ["mdi:ruler", LENGTH_KILOMETERS], + "remaining_fuel": ["mdi:gas-station", VOLUME_LITERS], + "charging_time_remaining": ["mdi:update", "h"], + "charging_status": ["mdi:battery-charging", None], } ATTR_TO_HA_IMPERIAL = { - 'mileage': ['mdi:speedometer', LENGTH_MILES], - 'remaining_range_total': ['mdi:ruler', LENGTH_MILES], - 'remaining_range_electric': ['mdi:ruler', LENGTH_MILES], - 'remaining_range_fuel': ['mdi:ruler', LENGTH_MILES], - 'max_range_electric': ['mdi:ruler', LENGTH_MILES], - 'remaining_fuel': ['mdi:gas-station', VOLUME_GALLONS], - 'charging_time_remaining': ['mdi:update', 'h'], - 'charging_status': ['mdi:battery-charging', None], + "mileage": ["mdi:speedometer", LENGTH_MILES], + "remaining_range_total": ["mdi:ruler", LENGTH_MILES], + "remaining_range_electric": ["mdi:ruler", LENGTH_MILES], + "remaining_range_fuel": ["mdi:ruler", LENGTH_MILES], + "max_range_electric": ["mdi:ruler", LENGTH_MILES], + "remaining_fuel": ["mdi:gas-station", VOLUME_GALLONS], + "charging_time_remaining": ["mdi:update", "h"], + "charging_status": ["mdi:battery-charging", None], } @@ -42,17 +46,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): attribute_info = ATTR_TO_HA_METRIC accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug('Found BMW accounts: %s', - ', '.join([a.name for a in accounts])) + _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) devices = [] for account in accounts: for vehicle in account.account.vehicles: for attribute_name in vehicle.drive_train_attributes: device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info) + account, vehicle, attribute_name, attribute_info + ) devices.append(device) device = BMWConnectedDriveSensor( - account, vehicle, 'mileage', attribute_info) + account, vehicle, "mileage", attribute_info + ) devices.append(device) add_entities(devices, True) @@ -66,8 +71,8 @@ class BMWConnectedDriveSensor(Entity): self._account = account self._attribute = attribute self._state = None - self._name = '{} {}'.format(self._vehicle.name, self._attribute) - self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) + self._name = "{} {}".format(self._vehicle.name, self._attribute) + self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) self._attribute_info = attribute_info @property @@ -92,14 +97,14 @@ class BMWConnectedDriveSensor(Entity): def icon(self): """Icon to use in the frontend, if any.""" from bimmer_connected.state import ChargingState - vehicle_state = self._vehicle.state - charging_state = vehicle_state.charging_status in [ - ChargingState.CHARGING] - if self._attribute == 'charging_level_hv': + vehicle_state = self._vehicle.state + charging_state = vehicle_state.charging_status in [ChargingState.CHARGING] + + if self._attribute == "charging_level_hv": return icon_for_battery_level( - battery_level=vehicle_state.charging_level_hv, - charging=charging_state) + battery_level=vehicle_state.charging_level_hv, charging=charging_state + ) icon, _ = self._attribute_info.get(self._attribute, [None, None]) return icon @@ -121,25 +126,21 @@ class BMWConnectedDriveSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - return { - 'car': self._vehicle.name - } + return {"car": self._vehicle.name} def update(self) -> None: """Read new state data from the library.""" - _LOGGER.debug('Updating %s', self._vehicle.name) + _LOGGER.debug("Updating %s", self._vehicle.name) vehicle_state = self._vehicle.state - if self._attribute == 'charging_status': + if self._attribute == "charging_status": self._state = getattr(vehicle_state, self._attribute).value elif self.unit_of_measurement == VOLUME_GALLONS: value = getattr(vehicle_state, self._attribute) - value_converted = self.hass.config.units.volume( - value, VOLUME_LITERS) + value_converted = self.hass.config.units.volume(value, VOLUME_LITERS) self._state = round(value_converted) elif self.unit_of_measurement == LENGTH_MILES: value = getattr(vehicle_state, self._attribute) - value_converted = self.hass.config.units.length( - value, LENGTH_KILOMETERS) + value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS) self._state = round(value_converted) else: self._state = getattr(vehicle_state, self._attribute) diff --git a/homeassistant/components/bom/camera.py b/homeassistant/components/bom/camera.py index 87ffd4ab791..3a5d6cdc503 100644 --- a/homeassistant/components/bom/camera.py +++ b/homeassistant/components/bom/camera.py @@ -5,22 +5,68 @@ from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_ID, CONF_NAME from homeassistant.helpers import config_validation as cv -CONF_DELTA = 'delta' -CONF_FRAMES = 'frames' -CONF_LOCATION = 'location' -CONF_OUTFILE = 'filename' +CONF_DELTA = "delta" +CONF_FRAMES = "frames" +CONF_LOCATION = "location" +CONF_OUTFILE = "filename" LOCATIONS = [ - 'Adelaide', 'Albany', 'AliceSprings', 'Bairnsdale', 'Bowen', 'Brisbane', - 'Broome', 'Cairns', 'Canberra', 'Carnarvon', 'Ceduna', 'Dampier', 'Darwin', - 'Emerald', 'Esperance', 'Geraldton', 'Giles', 'Gladstone', 'Gove', - 'Grafton', 'Gympie', 'HallsCreek', 'Hobart', 'Kalgoorlie', 'Katherine', - 'Learmonth', 'Longreach', 'Mackay', 'Marburg', 'Melbourne', 'Mildura', - 'Moree', 'MorningtonIs', 'MountIsa', 'MtGambier', 'Namoi', 'Newcastle', - 'Newdegate', 'NorfolkIs', 'NWTasmania', 'Perth', 'PortHedland', - 'SellicksHill', 'SouthDoodlakine', 'Sydney', 'Townsville', 'WaggaWagga', - 'Warrego', 'Warruwi', 'Watheroo', 'Weipa', 'WillisIs', 'Wollongong', - 'Woomera', 'Wyndham', 'Yarrawonga', + "Adelaide", + "Albany", + "AliceSprings", + "Bairnsdale", + "Bowen", + "Brisbane", + "Broome", + "Cairns", + "Canberra", + "Carnarvon", + "Ceduna", + "Dampier", + "Darwin", + "Emerald", + "Esperance", + "Geraldton", + "Giles", + "Gladstone", + "Gove", + "Grafton", + "Gympie", + "HallsCreek", + "Hobart", + "Kalgoorlie", + "Katherine", + "Learmonth", + "Longreach", + "Mackay", + "Marburg", + "Melbourne", + "Mildura", + "Moree", + "MorningtonIs", + "MountIsa", + "MtGambier", + "Namoi", + "Newcastle", + "Newdegate", + "NorfolkIs", + "NWTasmania", + "Perth", + "PortHedland", + "SellicksHill", + "SouthDoodlakine", + "Sydney", + "Townsville", + "WaggaWagga", + "Warrego", + "Warruwi", + "Watheroo", + "Weipa", + "WillisIs", + "Wollongong", + "Woomera", + "Wyndham", + "Yarrawonga", ] @@ -29,32 +75,42 @@ def _validate_schema(config): if not all(config.get(x) for x in (CONF_ID, CONF_DELTA, CONF_FRAMES)): raise vol.Invalid( "Specify '{}', '{}' and '{}' when '{}' is unspecified".format( - CONF_ID, CONF_DELTA, CONF_FRAMES, CONF_LOCATION)) + CONF_ID, CONF_DELTA, CONF_FRAMES, CONF_LOCATION + ) + ) return config LOCATIONS_MSG = "Set '{}' to one of: {}".format( - CONF_LOCATION, ', '.join(sorted(LOCATIONS))) + CONF_LOCATION, ", ".join(sorted(LOCATIONS)) +) XOR_MSG = "Specify exactly one of '{}' or '{}'".format(CONF_ID, CONF_LOCATION) PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend({ - vol.Exclusive(CONF_ID, 'xor', msg=XOR_MSG): cv.string, - vol.Exclusive(CONF_LOCATION, 'xor', msg=XOR_MSG): vol.In( - LOCATIONS, msg=LOCATIONS_MSG), - vol.Optional(CONF_DELTA): cv.positive_int, - vol.Optional(CONF_FRAMES): cv.positive_int, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OUTFILE): cv.string, - }), _validate_schema) + PLATFORM_SCHEMA.extend( + { + vol.Exclusive(CONF_ID, "xor", msg=XOR_MSG): cv.string, + vol.Exclusive(CONF_LOCATION, "xor", msg=XOR_MSG): vol.In( + LOCATIONS, msg=LOCATIONS_MSG + ), + vol.Optional(CONF_DELTA): cv.positive_int, + vol.Optional(CONF_FRAMES): cv.positive_int, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_OUTFILE): cv.string, + } + ), + _validate_schema, +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up BOM radar-loop camera component.""" location = config.get(CONF_LOCATION) or "ID {}".format(config.get(CONF_ID)) name = config.get(CONF_NAME) or "BOM Radar Loop - {}".format(location) - args = [config.get(x) for x in (CONF_LOCATION, CONF_ID, CONF_DELTA, - CONF_FRAMES, CONF_OUTFILE)] + args = [ + config.get(x) + for x in (CONF_LOCATION, CONF_ID, CONF_DELTA, CONF_FRAMES, CONF_OUTFILE) + ] add_entities([BOMRadarCam(name, *args)]) @@ -64,6 +120,7 @@ class BOMRadarCam(Camera): def __init__(self, name, location, radar_id, delta, frames, outfile): """Initialize the component.""" from bomradarloop import BOMRadarLoop + super().__init__() self._name = name self._cam = BOMRadarLoop(location, radar_id, delta, frames, outfile) diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 4c96315ec1f..790b2ddc74f 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -15,63 +15,68 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, CONF_NAME, ATTR_ATTRIBUTION, - CONF_LATITUDE, CONF_LONGITUDE) + CONF_MONITORED_CONDITIONS, + TEMP_CELSIUS, + CONF_NAME, + ATTR_ATTRIBUTION, + CONF_LATITUDE, + CONF_LONGITUDE, +) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -_RESOURCE = 'http://www.bom.gov.au/fwo/{}/{}.{}.json' +_RESOURCE = "http://www.bom.gov.au/fwo/{}/{}.{}.json" _LOGGER = logging.getLogger(__name__) -ATTR_LAST_UPDATE = 'last_update' -ATTR_SENSOR_ID = 'sensor_id' -ATTR_STATION_ID = 'station_id' -ATTR_STATION_NAME = 'station_name' -ATTR_ZONE_ID = 'zone_id' +ATTR_LAST_UPDATE = "last_update" +ATTR_SENSOR_ID = "sensor_id" +ATTR_STATION_ID = "station_id" +ATTR_STATION_NAME = "station_name" +ATTR_ZONE_ID = "zone_id" ATTRIBUTION = "Data provided by the Australian Bureau of Meteorology" -CONF_STATION = 'station' -CONF_ZONE_ID = 'zone_id' -CONF_WMO_ID = 'wmo_id' +CONF_STATION = "station" +CONF_ZONE_ID = "zone_id" +CONF_WMO_ID = "wmo_id" MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=60) SENSOR_TYPES = { - 'wmo': ['wmo', None], - 'name': ['Station Name', None], - 'history_product': ['Zone', None], - 'local_date_time': ['Local Time', None], - 'local_date_time_full': ['Local Time Full', None], - 'aifstime_utc': ['UTC Time Full', None], - 'lat': ['Lat', None], - 'lon': ['Long', None], - 'apparent_t': ['Feels Like C', TEMP_CELSIUS], - 'cloud': ['Cloud', None], - 'cloud_base_m': ['Cloud Base', None], - 'cloud_oktas': ['Cloud Oktas', None], - 'cloud_type_id': ['Cloud Type ID', None], - 'cloud_type': ['Cloud Type', None], - 'delta_t': ['Delta Temp C', TEMP_CELSIUS], - 'gust_kmh': ['Wind Gust kmh', 'km/h'], - 'gust_kt': ['Wind Gust kt', 'kt'], - 'air_temp': ['Air Temp C', TEMP_CELSIUS], - 'dewpt': ['Dew Point C', TEMP_CELSIUS], - 'press': ['Pressure mb', 'mbar'], - 'press_qnh': ['Pressure qnh', 'qnh'], - 'press_msl': ['Pressure msl', 'msl'], - 'press_tend': ['Pressure Tend', None], - 'rain_trace': ['Rain Today', 'mm'], - 'rel_hum': ['Relative Humidity', '%'], - 'sea_state': ['Sea State', None], - 'swell_dir_worded': ['Swell Direction', None], - 'swell_height': ['Swell Height', 'm'], - 'swell_period': ['Swell Period', None], - 'vis_km': ['Visability km', 'km'], - 'weather': ['Weather', None], - 'wind_dir': ['Wind Direction', None], - 'wind_spd_kmh': ['Wind Speed kmh', 'km/h'], - 'wind_spd_kt': ['Wind Speed kt', 'kt'] + "wmo": ["wmo", None], + "name": ["Station Name", None], + "history_product": ["Zone", None], + "local_date_time": ["Local Time", None], + "local_date_time_full": ["Local Time Full", None], + "aifstime_utc": ["UTC Time Full", None], + "lat": ["Lat", None], + "lon": ["Long", None], + "apparent_t": ["Feels Like C", TEMP_CELSIUS], + "cloud": ["Cloud", None], + "cloud_base_m": ["Cloud Base", None], + "cloud_oktas": ["Cloud Oktas", None], + "cloud_type_id": ["Cloud Type ID", None], + "cloud_type": ["Cloud Type", None], + "delta_t": ["Delta Temp C", TEMP_CELSIUS], + "gust_kmh": ["Wind Gust kmh", "km/h"], + "gust_kt": ["Wind Gust kt", "kt"], + "air_temp": ["Air Temp C", TEMP_CELSIUS], + "dewpt": ["Dew Point C", TEMP_CELSIUS], + "press": ["Pressure mb", "mbar"], + "press_qnh": ["Pressure qnh", "qnh"], + "press_msl": ["Pressure msl", "msl"], + "press_tend": ["Pressure Tend", None], + "rain_trace": ["Rain Today", "mm"], + "rel_hum": ["Relative Humidity", "%"], + "sea_state": ["Sea State", None], + "swell_dir_worded": ["Swell Direction", None], + "swell_height": ["Swell Height", "m"], + "swell_period": ["Swell Period", None], + "vis_km": ["Visability km", "km"], + "weather": ["Weather", None], + "wind_dir": ["Wind Direction", None], + "wind_spd_kmh": ["Wind Speed kmh", "km/h"], + "wind_spd_kt": ["Wind Speed kt", "kt"], } @@ -79,20 +84,23 @@ def validate_station(station): """Check that the station ID is well-formed.""" if station is None: return - station = station.replace('.shtml', '') - if not re.fullmatch(r'ID[A-Z]\d\d\d\d\d\.\d\d\d\d\d', station): - raise vol.error.Invalid('Malformed station ID') + station = station.replace(".shtml", "") + if not re.fullmatch(r"ID[A-Z]\d\d\d\d\d\.\d\d\d\d\d", station): + raise vol.error.Invalid("Malformed station ID") return station -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Inclusive(CONF_ZONE_ID, 'Deprecated partial station ID'): cv.string, - vol.Inclusive(CONF_WMO_ID, 'Deprecated partial station ID'): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_STATION): validate_station, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Inclusive(CONF_ZONE_ID, "Deprecated partial station ID"): cv.string, + vol.Inclusive(CONF_WMO_ID, "Deprecated partial station ID"): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_STATION): validate_station, + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -104,13 +112,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if zone_id and wmo_id: _LOGGER.warning( "Using config %s, not %s and %s for BOM sensor", - CONF_STATION, CONF_ZONE_ID, CONF_WMO_ID) + CONF_STATION, + CONF_ZONE_ID, + CONF_WMO_ID, + ) elif zone_id and wmo_id: - station = '{}.{}'.format(zone_id, wmo_id) + station = "{}.{}".format(zone_id, wmo_id) else: station = closest_station( - config.get(CONF_LATITUDE), config.get(CONF_LONGITUDE), - hass.config.config_dir) + config.get(CONF_LATITUDE), + config.get(CONF_LONGITUDE), + hass.config.config_dir, + ) if station is None: _LOGGER.error("Could not get BOM weather station from lat/lon") return @@ -123,8 +136,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Received error from BOM Current: %s", err) return - add_entities([BOMCurrentSensor(bom_data, variable, config.get(CONF_NAME)) - for variable in config[CONF_MONITORED_CONDITIONS]]) + add_entities( + [ + BOMCurrentSensor(bom_data, variable, config.get(CONF_NAME)) + for variable in config[CONF_MONITORED_CONDITIONS] + ] + ) class BOMCurrentSensor(Entity): @@ -140,10 +157,9 @@ class BOMCurrentSensor(Entity): def name(self): """Return the name of the sensor.""" if self.stationname is None: - return 'BOM {}'.format(SENSOR_TYPES[self._condition][0]) + return "BOM {}".format(SENSOR_TYPES[self._condition][0]) - return 'BOM {} {}'.format( - self.stationname, SENSOR_TYPES[self._condition][0]) + return "BOM {} {}".format(self.stationname, SENSOR_TYPES[self._condition][0]) @property def state(self): @@ -157,9 +173,9 @@ class BOMCurrentSensor(Entity): ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_LAST_UPDATE: self.bom_data.last_updated, ATTR_SENSOR_ID: self._condition, - ATTR_STATION_ID: self.bom_data.latest_data['wmo'], - ATTR_STATION_NAME: self.bom_data.latest_data['name'], - ATTR_ZONE_ID: self.bom_data.latest_data['history_product'], + ATTR_STATION_ID: self.bom_data.latest_data["wmo"], + ATTR_STATION_NAME: self.bom_data.latest_data["name"], + ATTR_ZONE_ID: self.bom_data.latest_data["history_product"], } return attr @@ -179,7 +195,7 @@ class BOMCurrentData: def __init__(self, station_id): """Initialize the data object.""" - self._zone_id, self._wmo_id = station_id.split('.') + self._zone_id, self._wmo_id = station_id.split(".") self._data = None self.last_updated = None @@ -208,7 +224,7 @@ class BOMCurrentData: through the entire BOM provided dataset. """ condition_readings = (entry[condition] for entry in self._data) - return next((x for x in condition_readings if x != '-'), None) + return next((x for x in condition_readings if x != "-"), None) def should_update(self): """Determine whether an update should occur. @@ -236,17 +252,20 @@ class BOMCurrentData: "BOM was updated %s minutes ago, skipping update as" " < 35 minutes, Now: %s, LastUpdate: %s", (datetime.datetime.now() - self.last_updated), - datetime.datetime.now(), self.last_updated) + datetime.datetime.now(), + self.last_updated, + ) return try: result = requests.get(self._build_url(), timeout=10).json() - self._data = result['observations']['data'] + self._data = result["observations"]["data"] # set lastupdate using self._data[0] as the first element in the # array is the latest date in the json self.last_updated = datetime.datetime.strptime( - str(self._data[0]['local_date_time_full']), '%Y%m%d%H%M%S') + str(self._data[0]["local_date_time_full"]), "%Y%m%d%H%M%S" + ) return except ValueError as err: @@ -263,33 +282,34 @@ def _get_bom_stations(): """ latlon = {} with io.BytesIO() as file_obj: - with ftplib.FTP('ftp.bom.gov.au') as ftp: + with ftplib.FTP("ftp.bom.gov.au") as ftp: ftp.login() - ftp.cwd('anon2/home/ncc/metadata/sitelists') - ftp.retrbinary('RETR stations.zip', file_obj.write) + ftp.cwd("anon2/home/ncc/metadata/sitelists") + ftp.retrbinary("RETR stations.zip", file_obj.write) file_obj.seek(0) with zipfile.ZipFile(file_obj) as zipped: - with zipped.open('stations.txt') as station_txt: + with zipped.open("stations.txt") as station_txt: for _ in range(4): station_txt.readline() # skip header while True: line = station_txt.readline().decode().strip() if len(line) < 120: break # end while loop, ignoring any footer text - wmo, lat, lon = (line[a:b].strip() for a, b in - [(128, 134), (70, 78), (79, 88)]) - if wmo != '..': + wmo, lat, lon = ( + line[a:b].strip() for a, b in [(128, 134), (70, 78), (79, 88)] + ) + if wmo != "..": latlon[wmo] = (float(lat), float(lon)) zones = {} - pattern = (r'') - for state in ('nsw', 'vic', 'qld', 'wa', 'tas', 'nt'): - url = 'http://www.bom.gov.au/{0}/observations/{0}all.shtml'.format( - state) + pattern = ( + r'' + ) + for state in ("nsw", "vic", "qld", "wa", "tas", "nt"): + url = "http://www.bom.gov.au/{0}/observations/{0}all.shtml".format(state) for zone_id, wmo_id in re.findall(pattern, requests.get(url).text): zones[wmo_id] = zone_id - return {'{}.{}'.format(zones[k], k): latlon[k] - for k in set(latlon) & set(zones)} + return {"{}.{}".format(zones[k], k): latlon[k] for k in set(latlon) & set(zones)} def bom_stations(cache_dir): @@ -298,13 +318,13 @@ def bom_stations(cache_dir): Results from internet requests are cached as compressed JSON, making subsequent calls very much faster. """ - cache_file = os.path.join(cache_dir, '.bom-stations.json.gz') + cache_file = os.path.join(cache_dir, ".bom-stations.json.gz") if not os.path.isfile(cache_file): stations = _get_bom_stations() - with gzip.open(cache_file, 'wt') as cache: + with gzip.open(cache_file, "wt") as cache: json.dump(stations, cache, sort_keys=True) return stations - with gzip.open(cache_file, 'rt') as cache: + with gzip.open(cache_file, "rt") as cache: return {k: tuple(v) for k, v in json.load(cache).items()} diff --git a/homeassistant/components/bom/weather.py b/homeassistant/components/bom/weather.py index 2444192d87d..2513c7c4c40 100644 --- a/homeassistant/components/bom/weather.py +++ b/homeassistant/components/bom/weather.py @@ -4,28 +4,24 @@ import logging import voluptuous as vol from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity -from homeassistant.const import ( - CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation -from .sensor import ( - CONF_STATION, BOMCurrentData, closest_station, validate_station) +from .sensor import CONF_STATION, BOMCurrentData, closest_station, validate_station _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_STATION): validate_station, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_STATION): validate_station} +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the BOM weather platform.""" station = config.get(CONF_STATION) or closest_station( - config.get(CONF_LATITUDE), - config.get(CONF_LONGITUDE), - hass.config.config_dir) + config.get(CONF_LATITUDE), config.get(CONF_LONGITUDE), hass.config.config_dir + ) if station is None: _LOGGER.error("Could not get BOM weather station from lat/lon") return False @@ -44,7 +40,7 @@ class BOMWeather(WeatherEntity): def __init__(self, bom_data, stationname=None): """Initialise the platform with a data instance and station name.""" self.bom_data = bom_data - self.stationname = stationname or self.bom_data.latest_data.get('name') + self.stationname = stationname or self.bom_data.latest_data.get("name") def update(self): """Update current conditions.""" @@ -53,19 +49,19 @@ class BOMWeather(WeatherEntity): @property def name(self): """Return the name of the sensor.""" - return 'BOM {}'.format(self.stationname or '(unknown station)') + return "BOM {}".format(self.stationname or "(unknown station)") @property def condition(self): """Return the current condition.""" - return self.bom_data.get_reading('weather') + return self.bom_data.get_reading("weather") # Now implement the WeatherEntity interface @property def temperature(self): """Return the platform temperature.""" - return self.bom_data.get_reading('air_temp') + return self.bom_data.get_reading("air_temp") @property def temperature_unit(self): @@ -75,27 +71,41 @@ class BOMWeather(WeatherEntity): @property def pressure(self): """Return the mean sea-level pressure.""" - return self.bom_data.get_reading('press_msl') + return self.bom_data.get_reading("press_msl") @property def humidity(self): """Return the relative humidity.""" - return self.bom_data.get_reading('rel_hum') + return self.bom_data.get_reading("rel_hum") @property def wind_speed(self): """Return the wind speed.""" - return self.bom_data.get_reading('wind_spd_kmh') + return self.bom_data.get_reading("wind_spd_kmh") @property def wind_bearing(self): """Return the wind bearing.""" - directions = ['N', 'NNE', 'NE', 'ENE', - 'E', 'ESE', 'SE', 'SSE', - 'S', 'SSW', 'SW', 'WSW', - 'W', 'WNW', 'NW', 'NNW'] + directions = [ + "N", + "NNE", + "NE", + "ENE", + "E", + "ESE", + "SE", + "SSE", + "S", + "SSW", + "SW", + "WSW", + "W", + "WNW", + "NW", + "NNW", + ] wind = {name: idx * 360 / 16 for idx, name in enumerate(directions)} - return wind.get(self.bom_data.get_reading('wind_dir')) + return wind.get(self.bom_data.get_reading("wind_dir")) @property def attribution(self): diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 637e2922222..5072cb7b74c 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -5,40 +5,55 @@ import logging from getmac import get_mac_address import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP) + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json -BRAVIA_CONFIG_FILE = 'bravia.conf' +BRAVIA_CONFIG_FILE = "bravia.conf" -CLIENTID_PREFIX = 'HomeAssistant' +CLIENTID_PREFIX = "HomeAssistant" -DEFAULT_NAME = 'Sony Bravia TV' +DEFAULT_NAME = "Sony Bravia TV" -NICKNAME = 'Home Assistant' +NICKNAME = "Home Assistant" # Map ip to request id for configuring _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ - SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY +SUPPORT_BRAVIA = ( + SUPPORT_PAUSE + | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE + | SUPPORT_PLAY +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -54,8 +69,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Set up a configured TV host_ip, host_config = bravia_config.popitem() if host_ip == host: - pin = host_config['pin'] - mac = host_config['mac'] + pin = host_config["pin"] + mac = host_config["mac"] name = config.get(CONF_NAME) add_entities([BraviaTVDevice(host, mac, name, pin)]) return @@ -74,11 +89,11 @@ def setup_bravia(config, pin, hass, add_entities): try: if ipaddress.ip_address(host).version == 6: - mode = 'ip6' + mode = "ip6" else: - mode = 'ip' + mode = "ip" except ValueError: - mode = 'hostname' + mode = "hostname" mac = get_mac_address(**{mode: host}) # If we came here and configuring this host, mark as done @@ -91,7 +106,8 @@ def setup_bravia(config, pin, hass, add_entities): # Save config save_json( hass.config.path(BRAVIA_CONFIG_FILE), - {host: {'pin': pin, 'host': host, 'mac': mac}}) + {host: {"pin": pin, "host": host, "mac": mac}}, + ) add_entities([BraviaTVDevice(host, mac, name, pin)]) @@ -106,14 +122,15 @@ def request_configuration(config, hass, add_entities): # We got an error if this method is called while we are configuring if host in _CONFIGURING: configurator.notify_errors( - _CONFIGURING[host], "Failed to register, please try again.") + _CONFIGURING[host], "Failed to register, please try again." + ) return def bravia_configuration_callback(data): """Handle the entry of user PIN.""" from braviarc import braviarc - pin = data.get('pin') + pin = data.get("pin") braviarc = braviarc.BraviaRC(host) braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) if braviarc.is_connected(): @@ -122,12 +139,13 @@ def request_configuration(config, hass, add_entities): request_configuration(config, hass, add_entities) _CONFIGURING[host] = configurator.request_config( - name, bravia_configuration_callback, - description='Enter the Pin shown on your Sony Bravia TV.' + - 'If no Pin is shown, enter 0000 to let TV show you a Pin.', + name, + bravia_configuration_callback, + description="Enter the Pin shown on your Sony Bravia TV." + + "If no Pin is shown, enter 0000 to let TV show you a Pin.", description_image="/static/images/smart-tv.png", submit_caption="Confirm", - fields=[{'id': 'pin', 'name': 'Enter the pin', 'type': ''}] + fields=[{"id": "pin", "name": "Enter the pin", "type": ""}], ) @@ -169,7 +187,7 @@ class BraviaTVDevice(MediaPlayerDevice): def update(self): """Update TV info.""" if not self._braviarc.is_connected(): - if self._braviarc.get_power_status() != 'off': + if self._braviarc.get_power_status() != "off": self._braviarc.connect(self._pin, CLIENTID_PREFIX, NICKNAME) if not self._braviarc.is_connected(): return @@ -182,22 +200,21 @@ class BraviaTVDevice(MediaPlayerDevice): self._refresh_channels() power_status = self._braviarc.get_power_status() - if power_status == 'active': + if power_status == "active": self._state = STATE_ON playing_info = self._braviarc.get_playing_info() self._reset_playing_info() if playing_info is None or not playing_info: - self._channel_name = 'App' + self._channel_name = "App" else: - self._program_name = playing_info.get('programTitle') - self._channel_name = playing_info.get('title') - self._program_media_type = playing_info.get( - 'programMediaType') - self._channel_number = playing_info.get('dispNum') - self._source = playing_info.get('source') - self._content_uri = playing_info.get('uri') - self._duration = playing_info.get('durationSec') - self._start_date_time = playing_info.get('startDateTime') + self._program_name = playing_info.get("programTitle") + self._channel_name = playing_info.get("title") + self._program_media_type = playing_info.get("programMediaType") + self._channel_number = playing_info.get("dispNum") + self._source = playing_info.get("source") + self._content_uri = playing_info.get("uri") + self._duration = playing_info.get("durationSec") + self._start_date_time = playing_info.get("startDateTime") else: self._state = STATE_OFF @@ -219,15 +236,14 @@ class BraviaTVDevice(MediaPlayerDevice): """Refresh volume information.""" volume_info = self._braviarc.get_volume_info() if volume_info is not None: - self._volume = volume_info.get('volume') - self._min_volume = volume_info.get('minVolume') - self._max_volume = volume_info.get('maxVolume') - self._muted = volume_info.get('mute') + self._volume = volume_info.get("volume") + self._min_volume = volume_info.get("minVolume") + self._max_volume = volume_info.get("maxVolume") + self._muted = volume_info.get("mute") def _refresh_channels(self): if not self._source_list: - self._content_mapping = self._braviarc. \ - load_source_list() + self._content_mapping = self._braviarc.load_source_list() self._source_list = [] for key in self._content_mapping: self._source_list.append(key) @@ -276,7 +292,7 @@ class BraviaTVDevice(MediaPlayerDevice): if self._channel_name is not None: return_value = self._channel_name if self._program_name is not None: - return_value = return_value + ': ' + self._program_name + return_value = return_value + ": " + self._program_name return return_value @property diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index a1cc0a0caa3..5fb5af2732b 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -23,18 +23,18 @@ def data_packet(value): value = cv.string(value) extra = len(value) % 4 if extra > 0: - value = value + ('=' * (4 - extra)) + value = value + ("=" * (4 - extra)) return b64decode(value) -SERVICE_SEND_SCHEMA = vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PACKET): vol.All(cv.ensure_list, [data_packet]) -}) +SERVICE_SEND_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PACKET): vol.All(cv.ensure_list, [data_packet]), + } +) -SERVICE_LEARN_SCHEMA = vol.Schema({ - vol.Required(CONF_HOST): cv.string, -}) +SERVICE_LEARN_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string}) def async_setup_service(hass, host, device): @@ -61,24 +61,24 @@ def async_setup_service(hass, host, device): _LOGGER.info("Press the key you want Home Assistant to learn") start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=20): - packet = await hass.async_add_executor_job( - device.check_data) + packet = await hass.async_add_executor_job(device.check_data) if packet: - data = b64encode(packet).decode('utf8') - log_msg = "Received packet is: {}".\ - format(data) + data = b64encode(packet).decode("utf8") + log_msg = "Received packet is: {}".format(data) _LOGGER.info(log_msg) hass.components.persistent_notification.async_create( - log_msg, title='Broadlink switch') + log_msg, title="Broadlink switch" + ) return await asyncio.sleep(1) _LOGGER.error("No signal was received") hass.components.persistent_notification.async_create( - "No signal was received", title='Broadlink switch') + "No signal was received", title="Broadlink switch" + ) hass.services.async_register( - DOMAIN, SERVICE_LEARN, _learn_command, - schema=SERVICE_LEARN_SCHEMA) + DOMAIN, SERVICE_LEARN, _learn_command, schema=SERVICE_LEARN_SCHEMA + ) if not hass.services.has_service(DOMAIN, SERVICE_SEND): @@ -89,18 +89,15 @@ def async_setup_service(hass, host, device): for packet in packets: for retry in range(DEFAULT_RETRY): try: - await hass.async_add_executor_job( - device.send_data, packet) + await hass.async_add_executor_job(device.send_data, packet) break except (socket.timeout, ValueError): try: - await hass.async_add_executor_job( - device.auth) + await hass.async_add_executor_job(device.auth) except socket.timeout: - if retry == DEFAULT_RETRY-1: - _LOGGER.error( - "Failed to send packet to device") + if retry == DEFAULT_RETRY - 1: + _LOGGER.error("Failed to send packet to device") hass.services.async_register( - DOMAIN, SERVICE_SEND, _send_packet, - schema=SERVICE_SEND_SCHEMA) + DOMAIN, SERVICE_SEND, _send_packet, schema=SERVICE_SEND_SCHEMA + ) diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index 1c4e0ae7948..c31a1540349 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -1,7 +1,7 @@ """Constants for broadlink platform.""" -CONF_PACKET = 'packet' +CONF_PACKET = "packet" -DOMAIN = 'broadlink' +DOMAIN = "broadlink" -SERVICE_LEARN = 'learn' -SERVICE_SEND = 'send' +SERVICE_LEARN = "learn" +SERVICE_SEND = "send" diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index d9a8121e635..98988965ca0 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -8,39 +8,48 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_CELSIUS, - CONF_TIMEOUT, CONF_SCAN_INTERVAL) + CONF_HOST, + CONF_MAC, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + TEMP_CELSIUS, + CONF_TIMEOUT, + CONF_SCAN_INTERVAL, +) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -DEVICE_DEFAULT_NAME = 'Broadlink sensor' +DEVICE_DEFAULT_NAME = "Broadlink sensor" DEFAULT_TIMEOUT = 10 SCAN_INTERVAL = timedelta(seconds=300) SENSOR_TYPES = { - 'temperature': ['Temperature', TEMP_CELSIUS], - 'air_quality': ['Air Quality', ' '], - 'humidity': ['Humidity', '%'], - 'light': ['Light', ' '], - 'noise': ['Noise', ' '], + "temperature": ["Temperature", TEMP_CELSIUS], + "air_quality": ["Air Quality", " "], + "humidity": ["Humidity", "%"], + "light": ["Light", " "], + "noise": ["Noise", " "], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str), - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str), + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Broadlink device sensors.""" host = config.get(CONF_HOST) - mac = config.get(CONF_MAC).encode().replace(b':', b'') + mac = config.get(CONF_MAC).encode().replace(b":", b"") mac_addr = binascii.unhexlify(mac) name = config.get(CONF_NAME) timeout = config.get(CONF_TIMEOUT) @@ -57,7 +66,7 @@ class BroadlinkSensor(Entity): def __init__(self, name, broadlink_data, sensor_type): """Initialize the sensor.""" - self._name = '{} {}'.format(name, SENSOR_TYPES[sensor_type][0]) + self._name = "{} {}".format(name, SENSOR_TYPES[sensor_type][0]) self._state = None self._is_available = False self._type = sensor_type @@ -105,19 +114,22 @@ class BroadlinkData: self.mac_addr = mac_addr self.timeout = timeout self._connect() - self._schema = vol.Schema({ - vol.Optional('temperature'): vol.Range(min=-50, max=150), - vol.Optional('humidity'): vol.Range(min=0, max=100), - vol.Optional('light'): vol.Any(0, 1, 2, 3), - vol.Optional('air_quality'): vol.Any(0, 1, 2, 3), - vol.Optional('noise'): vol.Any(0, 1, 2), - }) + self._schema = vol.Schema( + { + vol.Optional("temperature"): vol.Range(min=-50, max=150), + vol.Optional("humidity"): vol.Range(min=0, max=100), + vol.Optional("light"): vol.Any(0, 1, 2, 3), + vol.Optional("air_quality"): vol.Any(0, 1, 2, 3), + vol.Optional("noise"): vol.Any(0, 1, 2), + } + ) self.update = Throttle(interval)(self._update) if not self._auth(): _LOGGER.warning("Failed to connect to device") def _connect(self): import broadlink + self._device = broadlink.a1((self.ip_addr, 80), self.mac_addr, None) self._device.timeout = self.timeout @@ -135,7 +147,7 @@ class BroadlinkData: except (vol.Invalid, vol.MultipleInvalid): pass # Continue quietly if device returned malformed data if retry > 0 and self._auth(): - self._update(retry-1) + self._update(retry - 1) def _auth(self, retry=3): try: @@ -144,5 +156,5 @@ class BroadlinkData: auth = False if not auth and retry > 0: self._connect() - return self._auth(retry-1) + return self._auth(retry - 1) return auth diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 5c67e9dbc28..2a7255f5a61 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -7,10 +7,21 @@ import socket import voluptuous as vol from homeassistant.components.switch import ( - ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SwitchDevice) + ENTITY_ID_FORMAT, + PLATFORM_SCHEMA, + SwitchDevice, +) from homeassistant.const import ( - CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC, - CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE, STATE_ON) + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_FRIENDLY_NAME, + CONF_HOST, + CONF_MAC, + CONF_SWITCHES, + CONF_TIMEOUT, + CONF_TYPE, + STATE_ON, +) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle, slugify from homeassistant.helpers.restore_state import RestoreEntity @@ -21,60 +32,76 @@ _LOGGER = logging.getLogger(__name__) TIME_BETWEEN_UPDATES = timedelta(seconds=5) -DEFAULT_NAME = 'Broadlink switch' +DEFAULT_NAME = "Broadlink switch" DEFAULT_TIMEOUT = 10 -CONF_SLOTS = 'slots' +CONF_SLOTS = "slots" -RM_TYPES = ['rm', 'rm2', 'rm_mini', 'rm_pro_phicomm', 'rm2_home_plus', - 'rm2_home_plus_gdt', 'rm2_pro_plus', 'rm2_pro_plus2', - 'rm2_pro_plus_bl', 'rm_mini_shate'] -SP1_TYPES = ['sp1'] -SP2_TYPES = ['sp2', 'honeywell_sp2', 'sp3', 'spmini2', 'spminiplus'] -MP1_TYPES = ['mp1'] +RM_TYPES = [ + "rm", + "rm2", + "rm_mini", + "rm_pro_phicomm", + "rm2_home_plus", + "rm2_home_plus_gdt", + "rm2_pro_plus", + "rm2_pro_plus2", + "rm2_pro_plus_bl", + "rm_mini_shate", +] +SP1_TYPES = ["sp1"] +SP2_TYPES = ["sp2", "honeywell_sp2", "sp3", "spmini2", "spminiplus"] +MP1_TYPES = ["mp1"] SWITCH_TYPES = RM_TYPES + SP1_TYPES + SP2_TYPES + MP1_TYPES -SWITCH_SCHEMA = vol.Schema({ - vol.Optional(CONF_COMMAND_OFF): data_packet, - vol.Optional(CONF_COMMAND_ON): data_packet, - vol.Optional(CONF_FRIENDLY_NAME): cv.string, -}) +SWITCH_SCHEMA = vol.Schema( + { + vol.Optional(CONF_COMMAND_OFF): data_packet, + vol.Optional(CONF_COMMAND_ON): data_packet, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + } +) -MP1_SWITCH_SLOT_SCHEMA = vol.Schema({ - vol.Optional('slot_1'): cv.string, - vol.Optional('slot_2'): cv.string, - vol.Optional('slot_3'): cv.string, - vol.Optional('slot_4'): cv.string -}) +MP1_SWITCH_SLOT_SCHEMA = vol.Schema( + { + vol.Optional("slot_1"): cv.string, + vol.Optional("slot_2"): cv.string, + vol.Optional("slot_3"): cv.string, + vol.Optional("slot_4"): cv.string, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_SWITCHES, default={}): - cv.schema_with_slug_keys(SWITCH_SCHEMA), - vol.Optional(CONF_SLOTS, default={}): MP1_SWITCH_SLOT_SCHEMA, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TYPE, default=SWITCH_TYPES[0]): vol.In(SWITCH_TYPES), - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_SWITCHES, default={}): cv.schema_with_slug_keys( + SWITCH_SCHEMA + ), + vol.Optional(CONF_SLOTS, default={}): MP1_SWITCH_SLOT_SCHEMA, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TYPE, default=SWITCH_TYPES[0]): vol.In(SWITCH_TYPES), + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Broadlink switches.""" import broadlink + devices = config.get(CONF_SWITCHES) - slots = config.get('slots', {}) + slots = config.get("slots", {}) ip_addr = config.get(CONF_HOST) friendly_name = config.get(CONF_FRIENDLY_NAME) - mac_addr = binascii.unhexlify( - config.get(CONF_MAC).encode().replace(b':', b'')) + mac_addr = binascii.unhexlify(config.get(CONF_MAC).encode().replace(b":", b"")) switch_type = config.get(CONF_TYPE) def _get_mp1_slot_name(switch_friendly_name, slot): """Get slot name.""" - if not slots['slot_{}'.format(slot)]: - return '{} slot {}'.format(switch_friendly_name, slot) - return slots['slot_{}'.format(slot)] + if not slots["slot_{}".format(slot)]: + return "{} slot {}".format(switch_friendly_name, slot) + return slots["slot_{}".format(slot)] if switch_type in RM_TYPES: broadlink_device = broadlink.rm((ip_addr, 80), mac_addr, None) @@ -88,7 +115,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_config.get(CONF_FRIENDLY_NAME, object_id), broadlink_device, device_config.get(CONF_COMMAND_ON), - device_config.get(CONF_COMMAND_OFF) + device_config.get(CONF_COMMAND_OFF), ) ) elif switch_type in SP1_TYPES: @@ -103,8 +130,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): parent_device = BroadlinkMP1Switch(broadlink_device) for i in range(1, 5): slot = BroadlinkMP1Slot( - _get_mp1_slot_name(friendly_name, i), - broadlink_device, i, parent_device) + _get_mp1_slot_name(friendly_name, i), broadlink_device, i, parent_device + ) switches.append(slot) broadlink_device.timeout = config.get(CONF_TIMEOUT) @@ -186,7 +213,7 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): return False if not self._auth(): return False - return self._sendpacket(packet, retry-1) + return self._sendpacket(packet, retry - 1) return True def _auth(self, retry=2): @@ -197,7 +224,7 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): if retry < 1: _LOGGER.error("Timeout during authorization") if not auth and retry > 0: - return self._auth(retry-1) + return self._auth(retry - 1) return auth @@ -221,7 +248,7 @@ class BroadlinkSP1Switch(BroadlinkRMSwitch): return False if not self._auth(): return False - return self._sendpacket(packet, retry-1) + return self._sendpacket(packet, retry - 1) return True @@ -262,9 +289,9 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): return if not self._auth(): return - return self._update(retry-1) + return self._update(retry - 1) if state is None and retry > 0: - return self._update(retry-1) + return self._update(retry - 1) self._state = state self._load_power = load_power self._is_available = True @@ -297,7 +324,7 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): return False if not self._auth(): return False - return self._sendpacket(packet, max(0, retry-1)) + return self._sendpacket(packet, max(0, retry - 1)) self._is_available = True return True @@ -324,7 +351,7 @@ class BroadlinkMP1Switch: """Get status of outlet from cached status list.""" if self._states is None: return None - return self._states['s{}'.format(slot)] + return self._states["s{}".format(slot)] @Throttle(TIME_BETWEEN_UPDATES) def update(self): @@ -341,9 +368,9 @@ class BroadlinkMP1Switch: return if not self._auth(): return - return self._update(max(0, retry-1)) + return self._update(max(0, retry - 1)) if states is None and retry > 0: - return self._update(max(0, retry-1)) + return self._update(max(0, retry - 1)) self._states = states def _auth(self, retry=2): @@ -353,5 +380,5 @@ class BroadlinkMP1Switch: except OSError: auth = False if not auth and retry > 0: - return self._auth(retry-1) + return self._auth(retry - 1) return auth diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index c36c5c0ad1c..5a3f72c3ef2 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -8,34 +8,54 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME) + ATTR_ATTRIBUTION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_AREA = 'area' +CONF_AREA = "area" -DEFAULT_NAME = 'Brottsplatskartan' +DEFAULT_NAME = "Brottsplatskartan" SCAN_INTERVAL = timedelta(minutes=30) AREAS = [ - "Blekinge län", "Dalarnas län", "Gotlands län", "Gävleborgs län", - "Hallands län", "Jämtlands län", "Jönköpings län", "Kalmar län", - "Kronobergs län", "Norrbottens län", "Skåne län", "Stockholms län", - "Södermanlands län", "Uppsala län", "Värmlands län", "Västerbottens län", - "Västernorrlands län", "Västmanlands län", "Västra Götalands län", - "Örebro län", "Östergötlands län" + "Blekinge län", + "Dalarnas län", + "Gotlands län", + "Gävleborgs län", + "Hallands län", + "Jämtlands län", + "Jönköpings län", + "Kalmar län", + "Kronobergs län", + "Norrbottens län", + "Skåne län", + "Stockholms län", + "Södermanlands län", + "Uppsala län", + "Värmlands län", + "Västerbottens län", + "Västernorrlands län", + "Västmanlands län", + "Västra Götalands län", + "Örebro län", + "Östergötlands län", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_AREA, default=[]): - vol.All(cv.ensure_list, [vol.In(AREAS)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_AREA, default=[]): vol.All(cv.ensure_list, [vol.In(AREAS)]), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -49,10 +69,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Every Home Assistant instance should have their own unique # app parameter: https://brottsplatskartan.se/sida/api - app = 'ha-{}'.format(uuid.getnode()) + app = "ha-{}".format(uuid.getnode()) bpk = brottsplatskartan.BrottsplatsKartan( - app=app, area=area, latitude=latitude, longitude=longitude) + app=app, area=area, latitude=latitude, longitude=longitude + ) add_entities([BrottsplatskartanSensor(bpk, name)], True) @@ -85,6 +106,7 @@ class BrottsplatskartanSensor(Entity): def update(self): """Update device state.""" import brottsplatskartan + incident_counts = defaultdict(int) incidents = self._brottsplatskartan.get_incidents() @@ -93,7 +115,7 @@ class BrottsplatskartanSensor(Entity): return for incident in incidents: - incident_type = incident.get('title_type') + incident_type = incident.get("title_type") incident_counts[incident_type] += 1 self._attributes = {ATTR_ATTRIBUTION: brottsplatskartan.ATTRIBUTION} diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py index 1c002f21f5f..b163f16a5c4 100644 --- a/homeassistant/components/browser/__init__.py +++ b/homeassistant/components/browser/__init__.py @@ -1,26 +1,30 @@ """Support for launching a web browser on the host machine.""" import voluptuous as vol -ATTR_URL = 'url' -ATTR_URL_DEFAULT = 'https://www.google.com' +ATTR_URL = "url" +ATTR_URL_DEFAULT = "https://www.google.com" -DOMAIN = 'browser' +DOMAIN = "browser" -SERVICE_BROWSE_URL = 'browse_url' +SERVICE_BROWSE_URL = "browse_url" -SERVICE_BROWSE_URL_SCHEMA = vol.Schema({ - # pylint: disable=no-value-for-parameter - vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url(), -}) +SERVICE_BROWSE_URL_SCHEMA = vol.Schema( + { + # pylint: disable=no-value-for-parameter + vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url() + } +) def setup(hass, config): """Listen for browse_url events.""" import webbrowser - hass.services.register(DOMAIN, SERVICE_BROWSE_URL, - lambda service: - webbrowser.open(service.data[ATTR_URL]), - schema=SERVICE_BROWSE_URL_SCHEMA) + hass.services.register( + DOMAIN, + SERVICE_BROWSE_URL, + lambda service: webbrowser.open(service.data[ATTR_URL]), + schema=SERVICE_BROWSE_URL_SCHEMA, + ) return True diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index f9455ae0910..af809cc7878 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -4,58 +4,65 @@ import logging import voluptuous as vol -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME from homeassistant.components.cover import ( - ATTR_POSITION, CoverDevice, - PLATFORM_SCHEMA, SUPPORT_CLOSE, - SUPPORT_OPEN, SUPPORT_SET_POSITION + ATTR_POSITION, + CoverDevice, + PLATFORM_SCHEMA, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, ) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION -DEVICE_CLASS = 'window' +DEVICE_CLASS = "window" -ATTR_REQUEST_POSITION = 'request_position' -NOTIFICATION_ID = 'brunt_notification' -NOTIFICATION_TITLE = 'Brunt Cover Setup' -ATTRIBUTION = 'Based on an unofficial Brunt SDK.' +ATTR_REQUEST_POSITION = "request_position" +NOTIFICATION_ID = "brunt_notification" +NOTIFICATION_TITLE = "Brunt Cover Setup" +ATTRIBUTION = "Based on an unofficial Brunt SDK." CLOSED_POSITION = 0 OPEN_POSITION = 100 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the brunt platform.""" # pylint: disable=no-name-in-module from brunt import BruntAPI + username = config[CONF_USERNAME] password = config[CONF_PASSWORD] bapi = BruntAPI(username=username, password=password) try: - things = bapi.getThings()['things'] + things = bapi.getThings()["things"] if not things: _LOGGER.error("No things present in account.") else: - add_entities([BruntDevice( - bapi, thing['NAME'], - thing['thingUri']) for thing in things], True) + add_entities( + [ + BruntDevice(bapi, thing["NAME"], thing["thingUri"]) + for thing in things + ], + True, + ) except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), + "Error: {}
" + "You will need to restart hass after fixing." + "".format(ex), title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) + notification_id=NOTIFICATION_ID, + ) class BruntDevice(CoverDevice): @@ -91,7 +98,7 @@ class BruntDevice(CoverDevice): None is unknown, 0 is closed, 100 is fully open. """ - pos = self._state.get('currentPosition') + pos = self._state.get("currentPosition") return int(pos) if pos else None @property @@ -103,7 +110,7 @@ class BruntDevice(CoverDevice): to Brunt, at times there is a diff of 1 to current None is unknown, 0 is closed, 100 is fully open. """ - pos = self._state.get('requestPosition') + pos = self._state.get("requestPosition") return int(pos) if pos else None @property @@ -113,7 +120,7 @@ class BruntDevice(CoverDevice): None is unknown, 0 when stopped, 1 when opening, 2 when closing """ - mov = self._state.get('moveState') + mov = self._state.get("moveState") return int(mov) if mov else None @property @@ -131,7 +138,7 @@ class BruntDevice(CoverDevice): """Return the detailed device state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_REQUEST_POSITION: self.request_cover_position + ATTR_REQUEST_POSITION: self.request_cover_position, } @property @@ -152,8 +159,7 @@ class BruntDevice(CoverDevice): def update(self): """Poll the current state of the device.""" try: - self._state = self._bapi.getState( - thingUri=self._thing_uri).get('thing') + self._state = self._bapi.getState(thingUri=self._thing_uri).get("thing") self._available = True except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) @@ -161,15 +167,14 @@ class BruntDevice(CoverDevice): def open_cover(self, **kwargs): """Set the cover to the open position.""" - self._bapi.changeRequestPosition( - OPEN_POSITION, thingUri=self._thing_uri) + self._bapi.changeRequestPosition(OPEN_POSITION, thingUri=self._thing_uri) def close_cover(self, **kwargs): """Set the cover to the closed position.""" - self._bapi.changeRequestPosition( - CLOSED_POSITION, thingUri=self._thing_uri) + self._bapi.changeRequestPosition(CLOSED_POSITION, thingUri=self._thing_uri) def set_cover_position(self, **kwargs): """Set the cover to a specific position.""" self._bapi.changeRequestPosition( - kwargs[ATTR_POSITION], thingUri=self._thing_uri) + kwargs[ATTR_POSITION], thingUri=self._thing_uri + ) diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 65f88e05d1c..0a068a3981f 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -4,17 +4,20 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA, - DeviceScanner) +from homeassistant.components.device_tracker import ( + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) from homeassistant.const import CONF_HOST _LOGGER = logging.getLogger(__name__) -CONF_DEFAULT_IP = '192.168.1.254' +CONF_DEFAULT_IP = "192.168.1.254" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string} +) def get_scanner(hass, config): diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index adc873f56b3..58f409c2d4b 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -5,16 +5,19 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) from homeassistant.const import CONF_HOST _LOGGER = logging.getLogger(__name__) -CONF_DEFAULT_IP = '192.168.1.254' +CONF_DEFAULT_IP = "192.168.1.254" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string} +) def get_scanner(hass, config): @@ -44,15 +47,15 @@ class BTSmartHubScanner(DeviceScanner): def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" self._update_info() - return [client['mac'] for client in self.last_results] + return [client["mac"] for client in self.last_results] def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" if not self.last_results: return None for client in self.last_results: - if client['mac'] == device: - return client['host'] + if client["mac"] == device: + return client["host"] return None def _update_info(self): @@ -72,18 +75,20 @@ class BTSmartHubScanner(DeviceScanner): def get_bt_smarthub_data(self): """Retrieve data from BT Smart Hub and return parsed result.""" import btsmarthub_devicelist + # Request data from bt smarthub into a list of dicts. data = btsmarthub_devicelist.get_devicelist( - router_ip=self.host, only_active_devices=True) + router_ip=self.host, only_active_devices=True + ) # Renaming keys from parsed result. devices = {} for device in data: try: - devices[device['UserHostName']] = { - 'ip': device['IPAddress'], - 'mac': device['PhysAddress'], - 'host': device['UserHostName'], - 'status': device['Active'] + devices[device["UserHostName"]] = { + "ip": device["IPAddress"], + "mac": device["PhysAddress"], + "host": device["UserHostName"], + "status": device["Active"], } except KeyError: pass diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index b390a86d622..1db9e2beaf9 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -16,11 +16,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util -CONF_DIMENSION = 'dimension' -CONF_DELTA = 'delta' +CONF_DIMENSION = "dimension" +CONF_DELTA = "delta" -RADAR_MAP_URL_TEMPLATE = ('https://api.buienradar.nl/image/1.0/' - 'RadarMapNL?w={w}&h={h}') +RADAR_MAP_URL_TEMPLATE = "https://api.buienradar.nl/image/1.0/" "RadarMapNL?w={w}&h={h}" _LOG = logging.getLogger(__name__) @@ -28,16 +27,19 @@ _LOG = logging.getLogger(__name__) DIM_RANGE = vol.All(vol.Coerce(int), vol.Range(min=120, max=700)) PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_DIMENSION, default=512): DIM_RANGE, - vol.Optional(CONF_DELTA, default=600.0): vol.All(vol.Coerce(float), - vol.Range(min=0)), - vol.Optional(CONF_NAME, default="Buienradar loop"): cv.string, - })) + PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DIMENSION, default=512): DIM_RANGE, + vol.Optional(CONF_DELTA, default=600.0): vol.All( + vol.Coerce(float), vol.Range(min=0) + ), + vol.Optional(CONF_NAME, default="Buienradar loop"): cv.string, + } + ) +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up buienradar radar-loop camera component.""" dimension = config[CONF_DIMENSION] delta = config[CONF_DELTA] @@ -79,13 +81,13 @@ class BuienradarCam(Camera): # invariant: this condition is private to and owned by this instance. self._condition = asyncio.Condition() - self._last_image = None # type: Optional[bytes] + self._last_image = None # type: Optional[bytes] # value of the last seen last modified header self._last_modified = None # type: Optional[str] # loading status self._loading = False # deadline for image refresh - self.delta after last successful load - self._deadline = None # type: Optional[datetime] + self._deadline = None # type: Optional[datetime] @property def name(self) -> str: @@ -102,11 +104,10 @@ class BuienradarCam(Camera): """Retrieve new radar image and return whether this succeeded.""" session = async_get_clientsession(self.hass) - url = RADAR_MAP_URL_TEMPLATE.format(w=self._dimension, - h=self._dimension) + url = RADAR_MAP_URL_TEMPLATE.format(w=self._dimension, h=self._dimension) if self._last_modified: - headers = {'If-Modified-Since': self._last_modified} + headers = {"If-Modified-Since": self._last_modified} else: headers = {} @@ -118,7 +119,7 @@ class BuienradarCam(Camera): _LOG.debug("HTTP 304 - success") return True - last_modified = res.headers.get('Last-Modified', None) + last_modified = res.headers.get("Last-Modified", None) if last_modified: self._last_modified = last_modified diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index 1ed313348f7..d25a2526a5d 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -3,8 +3,10 @@ "name": "Buienradar", "documentation": "https://www.home-assistant.io/components/buienradar", "requirements": [ - "buienradar==0.91" + "buienradar==1.0.1" ], "dependencies": [], - "codeowners": ["@ties"] + "codeowners": [ + "@mjj4791", "@ties" + ] } diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 71ad6abb914..72da5164dab 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -9,8 +9,13 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, - CONF_NAME, TEMP_CELSIUS) + ATTR_ATTRIBUTION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + TEMP_CELSIUS, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -19,9 +24,9 @@ from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) -MEASURED_LABEL = 'Measured' -TIMEFRAME_LABEL = 'Timeframe' -SYMBOL = 'symbol' +MEASURED_LABEL = "Measured" +TIMEFRAME_LABEL = "Timeframe" +SYMBOL = "symbol" # Schedule next call after (minutes): SCHEDULE_OK = 10 @@ -31,110 +36,152 @@ SCHEDULE_NOK = 2 # Supported sensor types: # Key: ['label', unit, icon] SENSOR_TYPES = { - 'stationname': ['Stationname', None, None], - 'condition': ['Condition', None, None], - 'conditioncode': ['Condition code', None, None], - 'conditiondetailed': ['Detailed condition', None, None], - 'conditionexact': ['Full condition', None, None], - 'symbol': ['Symbol', None, None], - 'humidity': ['Humidity', '%', 'mdi:water-percent'], - 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], - 'groundtemperature': ['Ground temperature', TEMP_CELSIUS, - 'mdi:thermometer'], - 'windspeed': ['Wind speed', 'm/s', 'mdi:weather-windy'], - 'windforce': ['Wind force', 'Bft', 'mdi:weather-windy'], - 'winddirection': ['Wind direction', None, 'mdi:compass-outline'], - 'windazimuth': ['Wind direction azimuth', '°', 'mdi:compass-outline'], - 'pressure': ['Pressure', 'hPa', 'mdi:gauge'], - 'visibility': ['Visibility', 'm', None], - 'windgust': ['Wind gust', 'm/s', 'mdi:weather-windy'], - 'precipitation': ['Precipitation', 'mm/h', 'mdi:weather-pouring'], - 'irradiance': ['Irradiance', 'W/m2', 'mdi:sunglasses'], - 'precipitation_forecast_average': ['Precipitation forecast average', - 'mm/h', 'mdi:weather-pouring'], - 'precipitation_forecast_total': ['Precipitation forecast total', - 'mm', 'mdi:weather-pouring'], - 'temperature_1d': ['Temperature 1d', TEMP_CELSIUS, 'mdi:thermometer'], - 'temperature_2d': ['Temperature 2d', TEMP_CELSIUS, 'mdi:thermometer'], - 'temperature_3d': ['Temperature 3d', TEMP_CELSIUS, 'mdi:thermometer'], - 'temperature_4d': ['Temperature 4d', TEMP_CELSIUS, 'mdi:thermometer'], - 'temperature_5d': ['Temperature 5d', TEMP_CELSIUS, 'mdi:thermometer'], - 'mintemp_1d': ['Minimum temperature 1d', TEMP_CELSIUS, 'mdi:thermometer'], - 'mintemp_2d': ['Minimum temperature 2d', TEMP_CELSIUS, 'mdi:thermometer'], - 'mintemp_3d': ['Minimum temperature 3d', TEMP_CELSIUS, 'mdi:thermometer'], - 'mintemp_4d': ['Minimum temperature 4d', TEMP_CELSIUS, 'mdi:thermometer'], - 'mintemp_5d': ['Minimum temperature 5d', TEMP_CELSIUS, 'mdi:thermometer'], - 'rain_1d': ['Rain 1d', 'mm', 'mdi:weather-pouring'], - 'rain_2d': ['Rain 2d', 'mm', 'mdi:weather-pouring'], - 'rain_3d': ['Rain 3d', 'mm', 'mdi:weather-pouring'], - 'rain_4d': ['Rain 4d', 'mm', 'mdi:weather-pouring'], - 'rain_5d': ['Rain 5d', 'mm', 'mdi:weather-pouring'], - 'snow_1d': ['Snow 1d', 'cm', 'mdi:snowflake'], - 'snow_2d': ['Snow 2d', 'cm', 'mdi:snowflake'], - 'snow_3d': ['Snow 3d', 'cm', 'mdi:snowflake'], - 'snow_4d': ['Snow 4d', 'cm', 'mdi:snowflake'], - 'snow_5d': ['Snow 5d', 'cm', 'mdi:snowflake'], - 'rainchance_1d': ['Rainchance 1d', '%', 'mdi:weather-pouring'], - 'rainchance_2d': ['Rainchance 2d', '%', 'mdi:weather-pouring'], - 'rainchance_3d': ['Rainchance 3d', '%', 'mdi:weather-pouring'], - 'rainchance_4d': ['Rainchance 4d', '%', 'mdi:weather-pouring'], - 'rainchance_5d': ['Rainchance 5d', '%', 'mdi:weather-pouring'], - 'sunchance_1d': ['Sunchance 1d', '%', 'mdi:weather-partlycloudy'], - 'sunchance_2d': ['Sunchance 2d', '%', 'mdi:weather-partlycloudy'], - 'sunchance_3d': ['Sunchance 3d', '%', 'mdi:weather-partlycloudy'], - 'sunchance_4d': ['Sunchance 4d', '%', 'mdi:weather-partlycloudy'], - 'sunchance_5d': ['Sunchance 5d', '%', 'mdi:weather-partlycloudy'], - 'windforce_1d': ['Wind force 1d', 'Bft', 'mdi:weather-windy'], - 'windforce_2d': ['Wind force 2d', 'Bft', 'mdi:weather-windy'], - 'windforce_3d': ['Wind force 3d', 'Bft', 'mdi:weather-windy'], - 'windforce_4d': ['Wind force 4d', 'Bft', 'mdi:weather-windy'], - 'windforce_5d': ['Wind force 5d', 'Bft', 'mdi:weather-windy'], - 'condition_1d': ['Condition 1d', None, None], - 'condition_2d': ['Condition 2d', None, None], - 'condition_3d': ['Condition 3d', None, None], - 'condition_4d': ['Condition 4d', None, None], - 'condition_5d': ['Condition 5d', None, None], - 'conditioncode_1d': ['Condition code 1d', None, None], - 'conditioncode_2d': ['Condition code 2d', None, None], - 'conditioncode_3d': ['Condition code 3d', None, None], - 'conditioncode_4d': ['Condition code 4d', None, None], - 'conditioncode_5d': ['Condition code 5d', None, None], - 'conditiondetailed_1d': ['Detailed condition 1d', None, None], - 'conditiondetailed_2d': ['Detailed condition 2d', None, None], - 'conditiondetailed_3d': ['Detailed condition 3d', None, None], - 'conditiondetailed_4d': ['Detailed condition 4d', None, None], - 'conditiondetailed_5d': ['Detailed condition 5d', None, None], - 'conditionexact_1d': ['Full condition 1d', None, None], - 'conditionexact_2d': ['Full condition 2d', None, None], - 'conditionexact_3d': ['Full condition 3d', None, None], - 'conditionexact_4d': ['Full condition 4d', None, None], - 'conditionexact_5d': ['Full condition 5d', None, None], - 'symbol_1d': ['Symbol 1d', None, None], - 'symbol_2d': ['Symbol 2d', None, None], - 'symbol_3d': ['Symbol 3d', None, None], - 'symbol_4d': ['Symbol 4d', None, None], - 'symbol_5d': ['Symbol 5d', None, None], + "stationname": ["Stationname", None, None], + # new in json api (>1.0.0): + "barometerfc": ["Barometer value", None, "mdi:gauge"], + # new in json api (>1.0.0): + "barometerfcname": ["Barometer", None, "mdi:gauge"], + # new in json api (>1.0.0): + "barometerfcnamenl": ["Barometer", None, "mdi:gauge"], + "condition": ["Condition", None, None], + "conditioncode": ["Condition code", None, None], + "conditiondetailed": ["Detailed condition", None, None], + "conditionexact": ["Full condition", None, None], + "symbol": ["Symbol", None, None], + # new in json api (>1.0.0): + "feeltemperature": ["Feel temperature", TEMP_CELSIUS, "mdi:thermometer"], + "humidity": ["Humidity", "%", "mdi:water-percent"], + "temperature": ["Temperature", TEMP_CELSIUS, "mdi:thermometer"], + "groundtemperature": ["Ground temperature", TEMP_CELSIUS, "mdi:thermometer"], + "windspeed": ["Wind speed", "km/h", "mdi:weather-windy"], + "windforce": ["Wind force", "Bft", "mdi:weather-windy"], + "winddirection": ["Wind direction", None, "mdi:compass-outline"], + "windazimuth": ["Wind direction azimuth", "°", "mdi:compass-outline"], + "pressure": ["Pressure", "hPa", "mdi:gauge"], + "visibility": ["Visibility", "km", None], + "windgust": ["Wind gust", "km/h", "mdi:weather-windy"], + "precipitation": ["Precipitation", "mm/h", "mdi:weather-pouring"], + "irradiance": ["Irradiance", "W/m2", "mdi:sunglasses"], + "precipitation_forecast_average": [ + "Precipitation forecast average", + "mm/h", + "mdi:weather-pouring", + ], + "precipitation_forecast_total": [ + "Precipitation forecast total", + "mm", + "mdi:weather-pouring", + ], + # new in json api (>1.0.0): + "rainlast24hour": ["Rain last 24h", "mm", "mdi:weather-pouring"], + # new in json api (>1.0.0): + "rainlasthour": ["Rain last hour", "mm", "mdi:weather-pouring"], + "temperature_1d": ["Temperature 1d", TEMP_CELSIUS, "mdi:thermometer"], + "temperature_2d": ["Temperature 2d", TEMP_CELSIUS, "mdi:thermometer"], + "temperature_3d": ["Temperature 3d", TEMP_CELSIUS, "mdi:thermometer"], + "temperature_4d": ["Temperature 4d", TEMP_CELSIUS, "mdi:thermometer"], + "temperature_5d": ["Temperature 5d", TEMP_CELSIUS, "mdi:thermometer"], + "mintemp_1d": ["Minimum temperature 1d", TEMP_CELSIUS, "mdi:thermometer"], + "mintemp_2d": ["Minimum temperature 2d", TEMP_CELSIUS, "mdi:thermometer"], + "mintemp_3d": ["Minimum temperature 3d", TEMP_CELSIUS, "mdi:thermometer"], + "mintemp_4d": ["Minimum temperature 4d", TEMP_CELSIUS, "mdi:thermometer"], + "mintemp_5d": ["Minimum temperature 5d", TEMP_CELSIUS, "mdi:thermometer"], + "rain_1d": ["Rain 1d", "mm", "mdi:weather-pouring"], + "rain_2d": ["Rain 2d", "mm", "mdi:weather-pouring"], + "rain_3d": ["Rain 3d", "mm", "mdi:weather-pouring"], + "rain_4d": ["Rain 4d", "mm", "mdi:weather-pouring"], + "rain_5d": ["Rain 5d", "mm", "mdi:weather-pouring"], + # new in json api (>1.0.0): + "minrain_1d": ["Minimum rain 1d", "mm", "mdi:weather-pouring"], + "minrain_2d": ["Minimum rain 2d", "mm", "mdi:weather-pouring"], + "minrain_3d": ["Minimum rain 3d", "mm", "mdi:weather-pouring"], + "minrain_4d": ["Minimum rain 4d", "mm", "mdi:weather-pouring"], + "minrain_5d": ["Minimum rain 5d", "mm", "mdi:weather-pouring"], + # new in json api (>1.0.0): + "maxrain_1d": ["Maximum rain 1d", "mm", "mdi:weather-pouring"], + "maxrain_2d": ["Maximum rain 2d", "mm", "mdi:weather-pouring"], + "maxrain_3d": ["Maximum rain 3d", "mm", "mdi:weather-pouring"], + "maxrain_4d": ["Maximum rain 4d", "mm", "mdi:weather-pouring"], + "maxrain_5d": ["Maximum rain 5d", "mm", "mdi:weather-pouring"], + "rainchance_1d": ["Rainchance 1d", "%", "mdi:weather-pouring"], + "rainchance_2d": ["Rainchance 2d", "%", "mdi:weather-pouring"], + "rainchance_3d": ["Rainchance 3d", "%", "mdi:weather-pouring"], + "rainchance_4d": ["Rainchance 4d", "%", "mdi:weather-pouring"], + "rainchance_5d": ["Rainchance 5d", "%", "mdi:weather-pouring"], + "sunchance_1d": ["Sunchance 1d", "%", "mdi:weather-partlycloudy"], + "sunchance_2d": ["Sunchance 2d", "%", "mdi:weather-partlycloudy"], + "sunchance_3d": ["Sunchance 3d", "%", "mdi:weather-partlycloudy"], + "sunchance_4d": ["Sunchance 4d", "%", "mdi:weather-partlycloudy"], + "sunchance_5d": ["Sunchance 5d", "%", "mdi:weather-partlycloudy"], + "windforce_1d": ["Wind force 1d", "Bft", "mdi:weather-windy"], + "windforce_2d": ["Wind force 2d", "Bft", "mdi:weather-windy"], + "windforce_3d": ["Wind force 3d", "Bft", "mdi:weather-windy"], + "windforce_4d": ["Wind force 4d", "Bft", "mdi:weather-windy"], + "windforce_5d": ["Wind force 5d", "Bft", "mdi:weather-windy"], + "windspeed_1d": ["Wind speed 1d", "km/h", "mdi:weather-windy"], + "windspeed_2d": ["Wind speed 2d", "km/h", "mdi:weather-windy"], + "windspeed_3d": ["Wind speed 3d", "km/h", "mdi:weather-windy"], + "windspeed_4d": ["Wind speed 4d", "km/h", "mdi:weather-windy"], + "windspeed_5d": ["Wind speed 5d", "km/h", "mdi:weather-windy"], + "winddirection_1d": ["Wind direction 1d", None, "mdi:compass-outline"], + "winddirection_2d": ["Wind direction 2d", None, "mdi:compass-outline"], + "winddirection_3d": ["Wind direction 3d", None, "mdi:compass-outline"], + "winddirection_4d": ["Wind direction 4d", None, "mdi:compass-outline"], + "winddirection_5d": ["Wind direction 5d", None, "mdi:compass-outline"], + "windazimuth_1d": ["Wind direction azimuth 1d", "°", "mdi:compass-outline"], + "windazimuth_2d": ["Wind direction azimuth 2d", "°", "mdi:compass-outline"], + "windazimuth_3d": ["Wind direction azimuth 3d", "°", "mdi:compass-outline"], + "windazimuth_4d": ["Wind direction azimuth 4d", "°", "mdi:compass-outline"], + "windazimuth_5d": ["Wind direction azimuth 5d", "°", "mdi:compass-outline"], + "condition_1d": ["Condition 1d", None, None], + "condition_2d": ["Condition 2d", None, None], + "condition_3d": ["Condition 3d", None, None], + "condition_4d": ["Condition 4d", None, None], + "condition_5d": ["Condition 5d", None, None], + "conditioncode_1d": ["Condition code 1d", None, None], + "conditioncode_2d": ["Condition code 2d", None, None], + "conditioncode_3d": ["Condition code 3d", None, None], + "conditioncode_4d": ["Condition code 4d", None, None], + "conditioncode_5d": ["Condition code 5d", None, None], + "conditiondetailed_1d": ["Detailed condition 1d", None, None], + "conditiondetailed_2d": ["Detailed condition 2d", None, None], + "conditiondetailed_3d": ["Detailed condition 3d", None, None], + "conditiondetailed_4d": ["Detailed condition 4d", None, None], + "conditiondetailed_5d": ["Detailed condition 5d", None, None], + "conditionexact_1d": ["Full condition 1d", None, None], + "conditionexact_2d": ["Full condition 2d", None, None], + "conditionexact_3d": ["Full condition 3d", None, None], + "conditionexact_4d": ["Full condition 4d", None, None], + "conditionexact_5d": ["Full condition 5d", None, None], + "symbol_1d": ["Symbol 1d", None, None], + "symbol_2d": ["Symbol 2d", None, None], + "symbol_3d": ["Symbol 3d", None, None], + "symbol_4d": ["Symbol 4d", None, None], + "symbol_5d": ["Symbol 5d", None, None], } -CONF_TIMEFRAME = 'timeframe' +CONF_TIMEFRAME = "timeframe" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MONITORED_CONDITIONS, - default=['symbol', 'temperature']): vol.All( - cv.ensure_list, vol.Length(min=1), - [vol.In(SENSOR_TYPES.keys())]), - vol.Inclusive(CONF_LATITUDE, 'coordinates', - 'Latitude and longitude must exist together'): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, 'coordinates', - 'Latitude and longitude must exist together'): cv.longitude, - vol.Optional(CONF_TIMEFRAME, default=60): - vol.All(vol.Coerce(int), vol.Range(min=5, max=120)), - vol.Optional(CONF_NAME, default='br'): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional( + CONF_MONITORED_CONDITIONS, default=["symbol", "temperature"] + ): vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES.keys())]), + vol.Inclusive( + CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" + ): cv.latitude, + vol.Inclusive( + CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" + ): cv.longitude, + vol.Optional(CONF_TIMEFRAME, default=60): vol.All( + vol.Coerce(int), vol.Range(min=5, max=120) + ), + vol.Optional(CONF_NAME, default="br"): cv.string, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the buienradar sensor.""" from .weather import DEFAULT_TIMEFRAME @@ -146,16 +193,17 @@ async def async_setup_platform(hass, config, async_add_entities, _LOGGER.error("Latitude or longitude not set in HomeAssistant config") return False - coordinates = {CONF_LATITUDE: float(latitude), - CONF_LONGITUDE: float(longitude)} + coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)} - _LOGGER.debug("Initializing buienradar sensor coordinate %s, timeframe %s", - coordinates, timeframe) + _LOGGER.debug( + "Initializing buienradar sensor coordinate %s, timeframe %s", + coordinates, + timeframe, + ) dev = [] for sensor_type in config[CONF_MONITORED_CONDITIONS]: - dev.append(BrSensor(sensor_type, config.get(CONF_NAME), - coordinates)) + dev.append(BrSensor(sensor_type, config.get(CONF_NAME), coordinates)) async_add_entities(dev) data = BrData(hass, coordinates, timeframe, dev) @@ -168,7 +216,7 @@ class BrSensor(Entity): def __init__(self, sensor_type, client_name, coordinates): """Initialize the sensor.""" - from buienradar.buienradar import (PRECIPITATION_FORECAST, CONDITION) + from buienradar.constants import PRECIPITATION_FORECAST, CONDITION self.client_name = client_name self._name = SENSOR_TYPES[sensor_type][0] @@ -182,8 +230,7 @@ class BrSensor(Entity): self._unique_id = self.uid(coordinates) # All continuous sensors should be forced to be updated - self._force_update = self.type != SYMBOL and \ - not self.type.startswith(CONDITION) + self._force_update = self.type != SYMBOL and not self.type.startswith(CONDITION) if self.type.startswith(PRECIPITATION_FORECAST): self._timeframe = None @@ -191,18 +238,32 @@ class BrSensor(Entity): def uid(self, coordinates): """Generate a unique id using coordinates and sensor type.""" # The combination of the location, name and sensor type is unique - return "%2.6f%2.6f%s" % (coordinates[CONF_LATITUDE], - coordinates[CONF_LONGITUDE], - self.type) + return "%2.6f%2.6f%s" % ( + coordinates[CONF_LATITUDE], + coordinates[CONF_LONGITUDE], + self.type, + ) def load_data(self, data): """Load the sensor with relevant data.""" # Find sensor - from buienradar.buienradar import (ATTRIBUTION, CONDITION, CONDCODE, - DETAILED, EXACT, EXACTNL, FORECAST, - IMAGE, MEASURED, - PRECIPITATION_FORECAST, STATIONNAME, - TIMEFRAME) + from buienradar.constants import ( + ATTRIBUTION, + CONDITION, + CONDCODE, + DETAILED, + EXACT, + EXACTNL, + FORECAST, + IMAGE, + MEASURED, + PRECIPITATION_FORECAST, + STATIONNAME, + TIMEFRAME, + VISIBILITY, + WINDGUST, + WINDSPEED, + ) # Check if we have a new measurement, # otherwise we do not have to update the sensor @@ -213,23 +274,26 @@ class BrSensor(Entity): self._stationname = data.get(STATIONNAME) self._measured = data.get(MEASURED) - if self.type.endswith('_1d') or \ - self.type.endswith('_2d') or \ - self.type.endswith('_3d') or \ - self.type.endswith('_4d') or \ - self.type.endswith('_5d'): + if ( + self.type.endswith("_1d") + or self.type.endswith("_2d") + or self.type.endswith("_3d") + or self.type.endswith("_4d") + or self.type.endswith("_5d") + ): + # update forcasting sensors: fcday = 0 - if self.type.endswith('_2d'): + if self.type.endswith("_2d"): fcday = 1 - if self.type.endswith('_3d'): + if self.type.endswith("_3d"): fcday = 2 - if self.type.endswith('_4d'): + if self.type.endswith("_4d"): fcday = 3 - if self.type.endswith('_5d'): + if self.type.endswith("_5d"): fcday = 4 - # update all other sensors + # update weather symbol & status text if self.type.startswith(SYMBOL) or self.type.startswith(CONDITION): try: condition = data.get(FORECAST)[fcday].get(CONDITION) @@ -241,11 +305,11 @@ class BrSensor(Entity): new_state = condition.get(CONDITION, None) if self.type.startswith(SYMBOL): new_state = condition.get(EXACTNL, None) - if self.type.startswith('conditioncode'): + if self.type.startswith("conditioncode"): new_state = condition.get(CONDCODE, None) - if self.type.startswith('conditiondetailed'): + if self.type.startswith("conditiondetailed"): new_state = condition.get(DETAILED, None) - if self.type.startswith('conditionexact'): + if self.type.startswith("conditionexact"): new_state = condition.get(EXACT, None) img = condition.get(IMAGE, None) @@ -256,6 +320,18 @@ class BrSensor(Entity): return True return False + if self.type.startswith(WINDSPEED): + # hass wants windspeeds in km/h not m/s, so convert: + try: + self._state = data.get(FORECAST)[fcday].get(self.type[:-3]) + if self._state is not None: + self._state = round(self._state * 3.6, 1) + return True + except IndexError: + _LOGGER.warning("No forecast for fcday=%s...", fcday) + return False + + # update all other sensors try: self._state = data.get(FORECAST)[fcday].get(self.type[:-3]) return True @@ -271,11 +347,11 @@ class BrSensor(Entity): new_state = condition.get(EXACTNL, None) if self.type == CONDITION: new_state = condition.get(CONDITION, None) - if self.type == 'conditioncode': + if self.type == "conditioncode": new_state = condition.get(CONDCODE, None) - if self.type == 'conditiondetailed': + if self.type == "conditiondetailed": new_state = condition.get(DETAILED, None) - if self.type == 'conditionexact': + if self.type == "conditionexact": new_state = condition.get(EXACT, None) img = condition.get(IMAGE, None) @@ -291,7 +367,21 @@ class BrSensor(Entity): # update nested precipitation forecast sensors nested = data.get(PRECIPITATION_FORECAST) self._timeframe = nested.get(TIMEFRAME) - self._state = nested.get(self.type[len(PRECIPITATION_FORECAST)+1:]) + self._state = nested.get(self.type[len(PRECIPITATION_FORECAST) + 1 :]) + return True + + if self.type == WINDSPEED or self.type == WINDGUST: + # hass wants windspeeds in km/h not m/s, so convert: + self._state = data.get(self.type) + if self._state is not None: + self._state = round(data.get(self.type) * 3.6, 1) + return True + + if self.type == VISIBILITY: + # hass wants visibility in km (not m), so convert: + self._state = data.get(self.type) + if self._state is not None: + self._state = round(self._state / 1000, 1) return True # update all other sensors @@ -311,7 +401,7 @@ class BrSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self.client_name, self._name) + return "{} {}".format(self.client_name, self._name) @property def state(self): @@ -331,7 +421,7 @@ class BrSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - from buienradar.buienradar import (PRECIPITATION_FORECAST) + from buienradar.constants import PRECIPITATION_FORECAST if self.type.startswith(PRECIPITATION_FORECAST): result = {ATTR_ATTRIBUTION: self._attribution} @@ -342,7 +432,7 @@ class BrSensor(Entity): result = { ATTR_ATTRIBUTION: self._attribution, - SENSOR_TYPES['stationname'][0]: self._stationname, + SENSOR_TYPES["stationname"][0]: self._stationname, } if self._measured is not None: # convert datetime (Europe/Amsterdam) into local datetime @@ -394,13 +484,11 @@ class BrData: """Schedule an update after minute minutes.""" _LOGGER.debug("Scheduling next update in %s minutes.", minute) nxt = dt_util.utcnow() + timedelta(minutes=minute) - async_track_point_in_utc_time(self.hass, self.async_update, - nxt) + async_track_point_in_utc_time(self.hass, self.async_update, nxt) async def get_data(self, url): """Load data from specified url.""" - from buienradar.buienradar import (CONTENT, - MESSAGE, STATUS_CODE, SUCCESS) + from buienradar.constants import CONTENT, MESSAGE, STATUS_CODE, SUCCESS _LOGGER.debug("Calling url: %s...", url) result = {SUCCESS: False, MESSAGE: None} @@ -427,53 +515,57 @@ class BrData: async def async_update(self, *_): """Update the data from buienradar.""" - from buienradar.buienradar import (parse_data, CONTENT, - DATA, MESSAGE, STATUS_CODE, SUCCESS) + from buienradar.constants import CONTENT, DATA, MESSAGE, STATUS_CODE, SUCCESS + from buienradar.buienradar import parse_data + from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url - content = await self.get_data('http://xml.buienradar.nl') - if not content.get(SUCCESS, False): - content = await self.get_data('http://api.buienradar.nl') + content = await self.get_data(JSON_FEED_URL) if content.get(SUCCESS) is not True: # unable to get the data - _LOGGER.warning("Unable to retrieve xml data from Buienradar." - "(Msg: %s, status: %s,)", - content.get(MESSAGE), - content.get(STATUS_CODE),) + _LOGGER.warning( + "Unable to retrieve json data from Buienradar." + "(Msg: %s, status: %s,)", + content.get(MESSAGE), + content.get(STATUS_CODE), + ) # schedule new call await self.schedule_update(SCHEDULE_NOK) return # rounding coordinates prevents unnecessary redirects/calls - rainurl = 'http://gadgets.buienradar.nl/data/raintext/?lat={}&lon={}' - rainurl = rainurl.format( - round(self.coordinates[CONF_LATITUDE], 2), - round(self.coordinates[CONF_LONGITUDE], 2) - ) + lat = self.coordinates[CONF_LATITUDE] + lon = self.coordinates[CONF_LONGITUDE] + rainurl = json_precipitation_forecast_url(lat, lon) raincontent = await self.get_data(rainurl) if raincontent.get(SUCCESS) is not True: # unable to get the data - _LOGGER.warning("Unable to retrieve raindata from Buienradar." - "(Msg: %s, status: %s,)", - raincontent.get(MESSAGE), - raincontent.get(STATUS_CODE),) + _LOGGER.warning( + "Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)", + raincontent.get(MESSAGE), + raincontent.get(STATUS_CODE), + ) # schedule new call await self.schedule_update(SCHEDULE_NOK) return - result = parse_data(content.get(CONTENT), - raincontent.get(CONTENT), - self.coordinates[CONF_LATITUDE], - self.coordinates[CONF_LONGITUDE], - self.timeframe) + result = parse_data( + content.get(CONTENT), + raincontent.get(CONTENT), + self.coordinates[CONF_LATITUDE], + self.coordinates[CONF_LONGITUDE], + self.timeframe, + False, + ) _LOGGER.debug("Buienradar parsed data: %s", result) if result.get(SUCCESS) is not True: - if int(datetime.now().strftime('%H')) > 0: - _LOGGER.warning("Unable to parse data from Buienradar." - "(Msg: %s)", - result.get(MESSAGE),) + if int(datetime.now().strftime("%H")) > 0: + _LOGGER.warning( + "Unable to parse data from Buienradar." "(Msg: %s)", + result.get(MESSAGE), + ) await self.schedule_update(SCHEDULE_NOK) return @@ -484,25 +576,29 @@ class BrData: @property def attribution(self): """Return the attribution.""" - from buienradar.buienradar import ATTRIBUTION + from buienradar.constants import ATTRIBUTION + return self.data.get(ATTRIBUTION) @property def stationname(self): """Return the name of the selected weatherstation.""" - from buienradar.buienradar import STATIONNAME + from buienradar.constants import STATIONNAME + return self.data.get(STATIONNAME) @property def condition(self): """Return the condition.""" - from buienradar.buienradar import CONDITION + from buienradar.constants import CONDITION + return self.data.get(CONDITION) @property def temperature(self): """Return the temperature, or None.""" - from buienradar.buienradar import TEMPERATURE + from buienradar.constants import TEMPERATURE + try: return float(self.data.get(TEMPERATURE)) except (ValueError, TypeError): @@ -511,7 +607,8 @@ class BrData: @property def pressure(self): """Return the pressure, or None.""" - from buienradar.buienradar import PRESSURE + from buienradar.constants import PRESSURE + try: return float(self.data.get(PRESSURE)) except (ValueError, TypeError): @@ -520,7 +617,8 @@ class BrData: @property def humidity(self): """Return the humidity, or None.""" - from buienradar.buienradar import HUMIDITY + from buienradar.constants import HUMIDITY + try: return int(self.data.get(HUMIDITY)) except (ValueError, TypeError): @@ -529,7 +627,8 @@ class BrData: @property def visibility(self): """Return the visibility, or None.""" - from buienradar.buienradar import VISIBILITY + from buienradar.constants import VISIBILITY + try: return int(self.data.get(VISIBILITY)) except (ValueError, TypeError): @@ -538,7 +637,8 @@ class BrData: @property def wind_speed(self): """Return the windspeed, or None.""" - from buienradar.buienradar import WINDSPEED + from buienradar.constants import WINDSPEED + try: return float(self.data.get(WINDSPEED)) except (ValueError, TypeError): @@ -547,7 +647,8 @@ class BrData: @property def wind_bearing(self): """Return the wind bearing, or None.""" - from buienradar.buienradar import WINDAZIMUTH + from buienradar.constants import WINDAZIMUTH + try: return int(self.data.get(WINDAZIMUTH)) except (ValueError, TypeError): @@ -556,5 +657,6 @@ class BrData: @property def forecast(self): """Return the forecast data.""" - from buienradar.buienradar import FORECAST + from buienradar.constants import FORECAST + return self.data.get(FORECAST) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index a8e4f9d424d..d8ae448c981 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -4,11 +4,17 @@ import logging import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity, - ATTR_FORECAST_PRECIPITATION) -from homeassistant.const import ( - CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + PLATFORM_SCHEMA, + WeatherEntity, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation @@ -16,40 +22,41 @@ from .sensor import BrData _LOGGER = logging.getLogger(__name__) -DATA_CONDITION = 'buienradar_condition' +DATA_CONDITION = "buienradar_condition" DEFAULT_TIMEFRAME = 60 -CONF_FORECAST = 'forecast' +CONF_FORECAST = "forecast" CONDITION_CLASSES = { - 'cloudy': ['c', 'p'], - 'fog': ['d', 'n'], - 'hail': [], - 'lightning': ['g'], - 'lightning-rainy': ['s'], - 'partlycloudy': ['b', 'j', 'o', 'r'], - 'pouring': ['l', 'q'], - 'rainy': ['f', 'h', 'k', 'm'], - 'snowy': ['u', 'i', 'v', 't'], - 'snowy-rainy': ['w'], - 'sunny': ['a'], - 'windy': [], - 'windy-variant': [], - 'exceptional': [], + "cloudy": ["c", "p"], + "fog": ["d", "n"], + "hail": [], + "lightning": ["g"], + "lightning-rainy": ["s"], + "partlycloudy": ["b", "j", "o", "r"], + "pouring": ["l", "q"], + "rainy": ["f", "h", "k", "m"], + "snowy": ["u", "i", "v", "t"], + "snowy-rainy": ["w"], + "sunny": ["a"], + "windy": [], + "windy-variant": [], + "exceptional": [], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_FORECAST, default=True): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_FORECAST, default=True): cv.boolean, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the buienradar platform.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -58,14 +65,12 @@ async def async_setup_platform(hass, config, async_add_entities, _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False - coordinates = {CONF_LATITUDE: float(latitude), - CONF_LONGITUDE: float(longitude)} + coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)} # create weather data: data = BrData(hass, coordinates, DEFAULT_TIMEFRAME, None) # create weather device: - _LOGGER.debug("Initializing buienradar weather: coordinates %s", - coordinates) + _LOGGER.debug("Initializing buienradar weather: coordinates %s", coordinates) # create condition helper if DATA_CONDITION not in hass.data: @@ -98,13 +103,15 @@ class BrWeather(WeatherEntity): @property def name(self): """Return the name of the sensor.""" - return self._stationname or 'BR {}'.format(self._data.stationname - or '(unknown station)') + return self._stationname or "BR {}".format( + self._data.stationname or "(unknown station)" + ) @property def condition(self): """Return the current condition.""" - from buienradar.buienradar import (CONDCODE) + from buienradar.constants import CONDCODE + if self._data and self._data.condition: ccode = self._data.condition.get(CONDCODE) if ccode: @@ -129,13 +136,17 @@ class BrWeather(WeatherEntity): @property def visibility(self): - """Return the current visibility.""" - return self._data.visibility + """Return the current visibility in km.""" + if self._data.visibility is None: + return None + return round(self._data.visibility / 1000, 1) @property def wind_speed(self): - """Return the current windspeed.""" - return self._data.wind_speed + """Return the current windspeed in km/h.""" + if self._data.wind_speed is None: + return None + return round(self._data.wind_speed * 3.6, 1) @property def wind_bearing(self): @@ -150,25 +161,40 @@ class BrWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - from buienradar.buienradar import (CONDITION, CONDCODE, RAIN, DATETIME, - MIN_TEMP, MAX_TEMP) + from buienradar.constants import ( + CONDITION, + CONDCODE, + RAIN, + DATETIME, + MIN_TEMP, + MAX_TEMP, + WINDAZIMUTH, + WINDSPEED, + ) - if self._forecast: - fcdata_out = [] - cond = self.hass.data[DATA_CONDITION] - if self._data.forecast: - for data_in in self._data.forecast: - # remap keys from external library to - # keys understood by the weather component: - data_out = {} - condcode = data_in.get(CONDITION, []).get(CONDCODE) + if not self._forecast: + return None - data_out[ATTR_FORECAST_TIME] = data_in.get(DATETIME) - data_out[ATTR_FORECAST_CONDITION] = cond[condcode] - data_out[ATTR_FORECAST_TEMP_LOW] = data_in.get(MIN_TEMP) - data_out[ATTR_FORECAST_TEMP] = data_in.get(MAX_TEMP) - data_out[ATTR_FORECAST_PRECIPITATION] = data_in.get(RAIN) + fcdata_out = [] + cond = self.hass.data[DATA_CONDITION] - fcdata_out.append(data_out) + if not self._data.forecast: + return None - return fcdata_out + for data_in in self._data.forecast: + # remap keys from external library to + # keys understood by the weather component: + condcode = data_in.get(CONDITION, []).get(CONDCODE) + data_out = { + ATTR_FORECAST_TIME: data_in.get(DATETIME), + ATTR_FORECAST_CONDITION: cond[condcode], + ATTR_FORECAST_TEMP_LOW: data_in.get(MIN_TEMP), + ATTR_FORECAST_TEMP: data_in.get(MAX_TEMP), + ATTR_FORECAST_PRECIPITATION: data_in.get(RAIN), + ATTR_FORECAST_WIND_BEARING: data_in.get(WINDAZIMUTH), + ATTR_FORECAST_WIND_SPEED: round(data_in.get(WINDSPEED) * 3.6, 1), + } + + fcdata_out.append(data_out) + + return fcdata_out diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index befa1f40843..6251679b225 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -7,40 +7,55 @@ import re import voluptuous as vol from homeassistant.components.calendar import ( - ENTITY_ID_FORMAT, PLATFORM_SCHEMA, CalendarEventDevice, calculate_offset, - get_date, is_offset_reached) + ENTITY_ID_FORMAT, + PLATFORM_SCHEMA, + CalendarEventDevice, + calculate_offset, + get_date, + is_offset_reached, +) from homeassistant.const import ( - CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL) + CONF_NAME, + CONF_PASSWORD, + CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.util import Throttle, dt _LOGGER = logging.getLogger(__name__) -CONF_CALENDARS = 'calendars' -CONF_CUSTOM_CALENDARS = 'custom_calendars' -CONF_CALENDAR = 'calendar' -CONF_SEARCH = 'search' +CONF_CALENDARS = "calendars" +CONF_CUSTOM_CALENDARS = "custom_calendars" +CONF_CALENDAR = "calendar" +CONF_SEARCH = "search" -OFFSET = '!!' +OFFSET = "!!" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - # pylint: disable=no-value-for-parameter - vol.Required(CONF_URL): vol.Url(), - vol.Optional(CONF_CALENDARS, default=[]): - vol.All(cv.ensure_list, [cv.string]), - vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, - vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, - vol.Optional(CONF_CUSTOM_CALENDARS, default=[]): - vol.All(cv.ensure_list, [ - vol.Schema({ - vol.Required(CONF_CALENDAR): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_SEARCH): cv.string, - }) - ]), - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + # pylint: disable=no-value-for-parameter + vol.Required(CONF_URL): vol.Url(), + vol.Optional(CONF_CALENDARS, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, + vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, + vol.Optional(CONF_CUSTOM_CALENDARS, default=[]): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_CALENDAR): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_SEARCH): cv.string, + } + ) + ], + ), + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + } +) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) @@ -54,7 +69,8 @@ def setup_platform(hass, config, add_entities, disc_info=None): password = config.get(CONF_PASSWORD) client = caldav.DAVClient( - url, None, username, password, ssl_verify_cert=config[CONF_VERIFY_SSL]) + url, None, username, password, ssl_verify_cert=config[CONF_VERIFY_SSL] + ) calendars = client.principal().calendars() @@ -62,8 +78,7 @@ def setup_platform(hass, config, add_entities, disc_info=None): for calendar in list(calendars): # If a calendar name was given in the configuration, # ignore all the others - if (config[CONF_CALENDARS] - and calendar.name not in config[CONF_CALENDARS]): + if config[CONF_CALENDARS] and calendar.name not in config[CONF_CALENDARS]: _LOGGER.debug("Ignoring calendar '%s'", calendar.name) continue @@ -75,20 +90,20 @@ def setup_platform(hass, config, add_entities, disc_info=None): name = cust_calendar[CONF_NAME] device_id = "{} {}".format( - cust_calendar[CONF_CALENDAR], cust_calendar[CONF_NAME]) - entity_id = generate_entity_id( - ENTITY_ID_FORMAT, device_id, hass=hass) + cust_calendar[CONF_CALENDAR], cust_calendar[CONF_NAME] + ) + entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass) calendar_devices.append( WebDavCalendarEventDevice( - name, calendar, entity_id, True, - cust_calendar[CONF_SEARCH])) + name, calendar, entity_id, True, cust_calendar[CONF_SEARCH] + ) + ) # Create a default calendar if there was no custom one if not config[CONF_CUSTOM_CALENDARS]: name = calendar.name device_id = calendar.name - entity_id = generate_entity_id( - ENTITY_ID_FORMAT, device_id, hass=hass) + entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass) calendar_devices.append( WebDavCalendarEventDevice(name, calendar, entity_id) ) @@ -110,9 +125,7 @@ class WebDavCalendarEventDevice(CalendarEventDevice): @property def device_state_attributes(self): """Return the device state attributes.""" - return { - 'offset_reached': self._offset_reached, - } + return {"offset_reached": self._offset_reached} @property def event(self): @@ -153,13 +166,14 @@ class WebDavCalendarData: async def async_get_events(self, hass, start_date, end_date): """Get all events in a specific time frame.""" # Get event list from the current calendar - vevent_list = await hass.async_add_job(self.calendar.date_search, - start_date, end_date) + vevent_list = await hass.async_add_job( + self.calendar.date_search, start_date, end_date + ) event_list = [] for event in vevent_list: vevent = event.instance.vevent uid = None - if hasattr(vevent, 'uid'): + if hasattr(vevent, "uid"): uid = vevent.uid.value data = { "uid": uid, @@ -170,8 +184,8 @@ class WebDavCalendarData: "description": self.get_attr_value(vevent, "description"), } - data['start'] = get_date(data['start']).isoformat() - data['end'] = get_date(data['end']).isoformat() + data["start"] = get_date(data["start"]).isoformat() + data["end"] = get_date(data["end"]).isoformat() event_list.append(data) @@ -183,30 +197,36 @@ class WebDavCalendarData: # We have to retrieve the results for the whole day as the server # won't return events that have already started results = self.calendar.date_search( - dt.start_of_local_day(), - dt.start_of_local_day() + timedelta(days=1) + dt.start_of_local_day(), dt.start_of_local_day() + timedelta(days=1) ) # dtstart can be a date or datetime depending if the event lasts a # whole day. Convert everything to datetime to be able to sort it - results.sort(key=lambda x: self.to_datetime( - x.instance.vevent.dtstart.value - )) + results.sort(key=lambda x: self.to_datetime(x.instance.vevent.dtstart.value)) - vevent = next(( - event.instance.vevent - for event in results - if (self.is_matching(event.instance.vevent, self.search) - and ( - not self.is_all_day(event.instance.vevent) - or self.include_all_day) - and not self.is_over(event.instance.vevent))), None) + vevent = next( + ( + event.instance.vevent + for event in results + if ( + self.is_matching(event.instance.vevent, self.search) + and ( + not self.is_all_day(event.instance.vevent) + or self.include_all_day + ) + and not self.is_over(event.instance.vevent) + ) + ), + None, + ) # If no matching event could be found if vevent is None: _LOGGER.debug( "No matching event found in the %d results for %s", - len(results), self.calendar.name) + len(results), + self.calendar.name, + ) self.event = None return @@ -216,7 +236,7 @@ class WebDavCalendarData: "start": self.get_hass_date(vevent.dtstart.value), "end": self.get_hass_date(self.get_end_date(vevent)), "location": self.get_attr_value(vevent, "location"), - "description": self.get_attr_value(vevent, "description") + "description": self.get_attr_value(vevent, "description"), } @staticmethod @@ -232,7 +252,8 @@ class WebDavCalendarData: or hasattr(vevent, "location") and pattern.match(vevent.location.value) or hasattr(vevent, "description") - and pattern.match(vevent.description.value)) + and pattern.match(vevent.description.value) + ) @staticmethod def is_all_day(vevent): diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index b02511470a4..e538b6b802a 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -8,7 +8,10 @@ from aiohttp import web from homeassistant.components import http from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.config_validation import ( # noqa - PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, time_period_str) + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, + time_period_str, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.template import DATE_STR_FORMAT @@ -16,15 +19,16 @@ from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) -DOMAIN = 'calendar' -ENTITY_ID_FORMAT = DOMAIN + '.{}' +DOMAIN = "calendar" +ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = timedelta(seconds=60) async def async_setup(hass, config): """Track states and offer events for calendars.""" component = hass.data[DOMAIN] = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DOMAIN) + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DOMAIN + ) hass.http.register_view(CalendarListView(component)) hass.http.register_view(CalendarEventView(component)) @@ -39,34 +43,35 @@ async def async_setup(hass, config): def get_date(date): """Get the dateTime from date or dateTime as a local.""" - if 'date' in date: - return dt.start_of_local_day(dt.dt.datetime.combine( - dt.parse_date(date['date']), dt.dt.time.min)) - return dt.as_local(dt.parse_datetime(date['dateTime'])) + if "date" in date: + return dt.start_of_local_day( + dt.dt.datetime.combine(dt.parse_date(date["date"]), dt.dt.time.min) + ) + return dt.as_local(dt.parse_datetime(date["dateTime"])) def normalize_event(event): """Normalize a calendar event.""" normalized_event = {} - start = event.get('start') - end = event.get('end') + start = event.get("start") + end = event.get("end") start = get_date(start) if start is not None else None end = get_date(end) if end is not None else None - normalized_event['dt_start'] = start - normalized_event['dt_end'] = end + normalized_event["dt_start"] = start + normalized_event["dt_end"] = end start = start.strftime(DATE_STR_FORMAT) if start is not None else None end = end.strftime(DATE_STR_FORMAT) if end is not None else None - normalized_event['start'] = start - normalized_event['end'] = end + normalized_event["start"] = start + normalized_event["end"] = end # cleanup the string so we don't have a bunch of double+ spaces - summary = event.get('summary', '') - normalized_event['message'] = re.sub(' +', '', summary).strip() - normalized_event['location'] = event.get('location', '') - normalized_event['description'] = event.get('description', '') - normalized_event['all_day'] = 'date' in event['start'] + summary = event.get("summary", "") + normalized_event["message"] = re.sub(" +", "", summary).strip() + normalized_event["location"] = event.get("location", "") + normalized_event["description"] = event.get("description", "") + normalized_event["all_day"] = "date" in event["start"] return normalized_event @@ -76,37 +81,36 @@ def calculate_offset(event, offset): Return the updated event with the offset_time included. """ - summary = event.get('summary', '') + summary = event.get("summary", "") # check if we have an offset tag in the message # time is HH:MM or MM - reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(offset) + reg = "{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)".format(offset) search = re.search(reg, summary) if search and search.group(1): time = search.group(1) - if ':' not in time: - if time[0] == '+' or time[0] == '-': - time = '{}0:{}'.format(time[0], time[1:]) + if ":" not in time: + if time[0] == "+" or time[0] == "-": + time = "{}0:{}".format(time[0], time[1:]) else: - time = '0:{}'.format(time) + time = "0:{}".format(time) offset_time = time_period_str(time) - summary = ( - summary[:search.start()] + summary[search.end():]).strip() - event['summary'] = summary + summary = (summary[: search.start()] + summary[search.end() :]).strip() + event["summary"] = summary else: offset_time = dt.dt.timedelta() # default it - event['offset_time'] = offset_time + event["offset_time"] = offset_time return event def is_offset_reached(event): """Have we reached the offset time specified in the event title.""" - start = get_date(event['start']) - if start is None or event['offset_time'] == dt.dt.timedelta(): + start = get_date(event["start"]) + if start is None or event["offset_time"] == dt.dt.timedelta(): return False - return start + event['offset_time'] <= dt.now(start.tzinfo) + return start + event["offset_time"] <= dt.now(start.tzinfo) class CalendarEventDevice(Entity): @@ -126,12 +130,12 @@ class CalendarEventDevice(Entity): event = normalize_event(event) return { - 'message': event['message'], - 'all_day': event['all_day'], - 'start_time': event['start'], - 'end_time': event['end'], - 'location': event['location'], - 'description': event['description'], + "message": event["message"], + "all_day": event["all_day"], + "start_time": event["start"], + "end_time": event["end"], + "location": event["location"], + "description": event["description"], } @property @@ -142,8 +146,8 @@ class CalendarEventDevice(Entity): return STATE_OFF event = normalize_event(event) - start = event['dt_start'] - end = event['dt_end'] + start = event["dt_start"] + end = event["dt_end"] if start is None or end is None: return STATE_OFF @@ -163,8 +167,8 @@ class CalendarEventDevice(Entity): class CalendarEventView(http.HomeAssistantView): """View to retrieve calendar content.""" - url = '/api/calendars/{entity_id}' - name = 'api:calendars:calendar' + url = "/api/calendars/{entity_id}" + name = "api:calendars:calendar" def __init__(self, component): """Initialize calendar view.""" @@ -173,8 +177,8 @@ class CalendarEventView(http.HomeAssistantView): async def get(self, request, entity_id): """Return calendar events.""" entity = self.component.get_entity(entity_id) - start = request.query.get('start') - end = request.query.get('end') + start = request.query.get("start") + end = request.query.get("end") if None in (start, end, entity): return web.Response(status=400) try: @@ -183,14 +187,15 @@ class CalendarEventView(http.HomeAssistantView): except (ValueError, AttributeError): return web.Response(status=400) event_list = await entity.async_get_events( - request.app['hass'], start_date, end_date) + request.app["hass"], start_date, end_date + ) return self.json(event_list) class CalendarListView(http.HomeAssistantView): """View to retrieve calendar list.""" - url = '/api/calendars' + url = "/api/calendars" name = "api:calendars" def __init__(self, component): @@ -199,14 +204,11 @@ class CalendarListView(http.HomeAssistantView): async def get(self, request): """Retrieve calendar list.""" - hass = request.app['hass'] + hass = request.app["hass"] calendar_list = [] for entity in self.component.entities: state = hass.states.get(entity.entity_id) - calendar_list.append({ - "name": state.name, - "entity_id": entity.entity_id, - }) + calendar_list.append({"name": state.name, "entity_id": entity.entity_id}) - return self.json(sorted(calendar_list, key=lambda x: x['name'])) + return self.json(sorted(calendar_list, key=lambda x: x["name"])) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index c31f1b03b55..efe7a37b310 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -14,22 +14,37 @@ import async_timeout import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, \ - SERVICE_TURN_ON, CONF_FILENAME +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + CONF_FILENAME, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import ( # noqa - PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED from homeassistant.components.media_player.const import ( - ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - SERVICE_PLAY_MEDIA, DOMAIN as DOMAIN_MP) + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + SERVICE_PLAY_MEDIA, + DOMAIN as DOMAIN_MP, +) from homeassistant.components.stream import request_stream from homeassistant.components.stream.const import ( - OUTPUT_FORMATS, FORMAT_CONTENT_TYPE, CONF_STREAM_SOURCE, CONF_LOOKBACK, - CONF_DURATION, SERVICE_RECORD, DOMAIN as DOMAIN_STREAM) + OUTPUT_FORMATS, + FORMAT_CONTENT_TYPE, + CONF_STREAM_SOURCE, + CONF_LOOKBACK, + CONF_DURATION, + SERVICE_RECORD, + DOMAIN as DOMAIN_STREAM, +) from homeassistant.components import websocket_api import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_when_setup @@ -39,58 +54,62 @@ from .prefs import CameraPreferences _LOGGER = logging.getLogger(__name__) -SERVICE_ENABLE_MOTION = 'enable_motion_detection' -SERVICE_DISABLE_MOTION = 'disable_motion_detection' -SERVICE_SNAPSHOT = 'snapshot' -SERVICE_PLAY_STREAM = 'play_stream' +SERVICE_ENABLE_MOTION = "enable_motion_detection" +SERVICE_DISABLE_MOTION = "disable_motion_detection" +SERVICE_SNAPSHOT = "snapshot" +SERVICE_PLAY_STREAM = "play_stream" SCAN_INTERVAL = timedelta(seconds=30) -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" -ATTR_FILENAME = 'filename' -ATTR_MEDIA_PLAYER = 'media_player' -ATTR_FORMAT = 'format' +ATTR_FILENAME = "filename" +ATTR_MEDIA_PLAYER = "media_player" +ATTR_FORMAT = "format" -STATE_RECORDING = 'recording' -STATE_STREAMING = 'streaming' -STATE_IDLE = 'idle' +STATE_RECORDING = "recording" +STATE_STREAMING = "streaming" +STATE_IDLE = "idle" # Bitfield of features supported by the camera entity SUPPORT_ON_OFF = 1 SUPPORT_STREAM = 2 -DEFAULT_CONTENT_TYPE = 'image/jpeg' -ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' +DEFAULT_CONTENT_TYPE = "image/jpeg" +ENTITY_IMAGE_URL = "/api/camera_proxy/{0}?token={1}" TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) _RND = SystemRandom() MIN_STREAM_INTERVAL = 0.5 # seconds -CAMERA_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, -}) +CAMERA_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) -CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_FILENAME): cv.template -}) +CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_FILENAME): cv.template} +) -CAMERA_SERVICE_PLAY_STREAM = CAMERA_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP), - vol.Optional(ATTR_FORMAT, default='hls'): vol.In(OUTPUT_FORMATS), -}) +CAMERA_SERVICE_PLAY_STREAM = CAMERA_SERVICE_SCHEMA.extend( + { + vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP), + vol.Optional(ATTR_FORMAT, default="hls"): vol.In(OUTPUT_FORMATS), + } +) -CAMERA_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend({ - vol.Required(CONF_FILENAME): cv.template, - vol.Optional(CONF_DURATION, default=30): vol.Coerce(int), - vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), -}) +CAMERA_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend( + { + vol.Required(CONF_FILENAME): cv.template, + vol.Optional(CONF_DURATION, default=30): vol.Coerce(int), + vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), + } +) -WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail' -SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_CAMERA_THUMBNAIL, - vol.Required('entity_id'): cv.entity_id -}) +WS_TYPE_CAMERA_THUMBNAIL = "camera_thumbnail" +SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_CAMERA_THUMBNAIL, + vol.Required("entity_id"): cv.entity_id, + } +) @attr.s @@ -111,11 +130,11 @@ async def async_request_stream(hass, entity_id, fmt): source = await camera.stream_source() if not source: - raise HomeAssistantError("{} does not support play stream service" - .format(camera.entity_id)) + raise HomeAssistantError( + "{} does not support play stream service".format(camera.entity_id) + ) - return request_stream(hass, source, fmt=fmt, - keepalive=camera_prefs.preload_stream) + return request_stream(hass, source, fmt=fmt, keepalive=camera_prefs.preload_stream) @bind_hass @@ -130,7 +149,7 @@ async def async_get_image(hass, entity_id, timeout=10): if image: return Image(camera.content_type, image) - raise HomeAssistantError('Unable to get image') + raise HomeAssistantError("Unable to get image") @bind_hass @@ -147,18 +166,21 @@ async def async_get_still_stream(request, image_cb, content_type, interval): This method must be run in the event loop. """ response = web.StreamResponse() - response.content_type = ('multipart/x-mixed-replace; ' - 'boundary=--frameboundary') + response.content_type = "multipart/x-mixed-replace; " "boundary=--frameboundary" await response.prepare(request) async def write_to_mjpeg_stream(img_bytes): """Write image to stream.""" - await response.write(bytes( - '--frameboundary\r\n' - 'Content-Type: {}\r\n' - 'Content-Length: {}\r\n\r\n'.format( - content_type, len(img_bytes)), - 'utf-8') + img_bytes + b'\r\n') + await response.write( + bytes( + "--frameboundary\r\n" + "Content-Type: {}\r\n" + "Content-Length: {}\r\n\r\n".format(content_type, len(img_bytes)), + "utf-8", + ) + + img_bytes + + b"\r\n" + ) last_image = None @@ -186,23 +208,24 @@ def _get_camera_from_entity_id(hass, entity_id): component = hass.data.get(DOMAIN) if component is None: - raise HomeAssistantError('Camera integration not set up') + raise HomeAssistantError("Camera integration not set up") camera = component.get_entity(entity_id) if camera is None: - raise HomeAssistantError('Camera not found') + raise HomeAssistantError("Camera not found") if not camera.is_on: - raise HomeAssistantError('Camera is off') + raise HomeAssistantError("Camera is off") return camera async def async_setup(hass, config): """Set up the camera component.""" - component = hass.data[DOMAIN] = \ - EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + component = hass.data[DOMAIN] = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) prefs = CameraPreferences(hass) await prefs.async_initialize() @@ -211,13 +234,11 @@ async def async_setup(hass, config): hass.http.register_view(CameraImageView(component)) hass.http.register_view(CameraMjpegStream(component)) hass.components.websocket_api.async_register_command( - WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail, - SCHEMA_WS_CAMERA_THUMBNAIL + WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail, SCHEMA_WS_CAMERA_THUMBNAIL ) hass.components.websocket_api.async_register_command(ws_camera_stream) hass.components.websocket_api.async_register_command(websocket_get_prefs) - hass.components.websocket_api.async_register_command( - websocket_update_prefs) + hass.components.websocket_api.async_register_command(websocket_update_prefs) await component.async_setup(config) @@ -244,36 +265,30 @@ async def async_setup(hass, config): entity.async_update_token() hass.async_create_task(entity.async_update_ha_state()) - hass.helpers.event.async_track_time_interval( - update_tokens, TOKEN_CHANGE_INTERVAL) + hass.helpers.event.async_track_time_interval(update_tokens, TOKEN_CHANGE_INTERVAL) component.async_register_entity_service( - SERVICE_ENABLE_MOTION, CAMERA_SERVICE_SCHEMA, - 'async_enable_motion_detection' + SERVICE_ENABLE_MOTION, CAMERA_SERVICE_SCHEMA, "async_enable_motion_detection" ) component.async_register_entity_service( - SERVICE_DISABLE_MOTION, CAMERA_SERVICE_SCHEMA, - 'async_disable_motion_detection' + SERVICE_DISABLE_MOTION, CAMERA_SERVICE_SCHEMA, "async_disable_motion_detection" ) component.async_register_entity_service( - SERVICE_TURN_OFF, CAMERA_SERVICE_SCHEMA, - 'async_turn_off' + SERVICE_TURN_OFF, CAMERA_SERVICE_SCHEMA, "async_turn_off" ) component.async_register_entity_service( - SERVICE_TURN_ON, CAMERA_SERVICE_SCHEMA, - 'async_turn_on' + SERVICE_TURN_ON, CAMERA_SERVICE_SCHEMA, "async_turn_on" ) component.async_register_entity_service( - SERVICE_SNAPSHOT, CAMERA_SERVICE_SNAPSHOT, - async_handle_snapshot_service + SERVICE_SNAPSHOT, CAMERA_SERVICE_SNAPSHOT, async_handle_snapshot_service ) component.async_register_entity_service( - SERVICE_PLAY_STREAM, CAMERA_SERVICE_PLAY_STREAM, - async_handle_play_stream_service + SERVICE_PLAY_STREAM, + CAMERA_SERVICE_PLAY_STREAM, + async_handle_play_stream_service, ) component.async_register_entity_service( - SERVICE_RECORD, CAMERA_SERVICE_RECORD, - async_handle_record_service + SERVICE_RECORD, CAMERA_SERVICE_RECORD, async_handle_record_service ) return True @@ -360,8 +375,9 @@ class Camera(Entity): This method must be run in the event loop. """ - return await async_get_still_stream(request, self.async_camera_image, - self.content_type, interval) + return await async_get_still_stream( + request, self.async_camera_image, self.content_type, interval + ) async def handle_async_mjpeg_stream(self, request): """Serve an HTTP MJPEG stream from the camera. @@ -370,8 +386,7 @@ class Camera(Entity): a direct stream from the camera. This method must be run in the event loop. """ - return await self.handle_async_still_stream( - request, self.frame_interval) + return await self.handle_async_still_stream(request, self.frame_interval) @property def state(self): @@ -426,18 +441,16 @@ class Camera(Entity): @property def state_attributes(self): """Return the camera state attributes.""" - attrs = { - 'access_token': self.access_tokens[-1], - } + attrs = {"access_token": self.access_tokens[-1]} if self.model: - attrs['model_name'] = self.model + attrs["model_name"] = self.model if self.brand: - attrs['brand'] = self.brand + attrs["brand"] = self.brand if self.motion_detection_enabled: - attrs['motion_detection'] = self.motion_detection_enabled + attrs["motion_detection"] = self.motion_detection_enabled return attrs @@ -445,8 +458,8 @@ class Camera(Entity): def async_update_token(self): """Update the used token.""" self.access_tokens.append( - hashlib.sha256( - _RND.getrandbits(256).to_bytes(32, 'little')).hexdigest()) + hashlib.sha256(_RND.getrandbits(256).to_bytes(32, "little")).hexdigest() + ) class CameraView(HomeAssistantView): @@ -465,14 +478,16 @@ class CameraView(HomeAssistantView): if camera is None: raise web.HTTPNotFound() - authenticated = (request[KEY_AUTHENTICATED] or - request.query.get('token') in camera.access_tokens) + authenticated = ( + request[KEY_AUTHENTICATED] + or request.query.get("token") in camera.access_tokens + ) if not authenticated: raise web.HTTPUnauthorized() if not camera.is_on: - _LOGGER.debug('Camera is off.') + _LOGGER.debug("Camera is off.") raise web.HTTPServiceUnavailable() return await self.handle(request, camera) @@ -485,8 +500,8 @@ class CameraView(HomeAssistantView): class CameraImageView(CameraView): """Camera view to serve an image.""" - url = '/api/camera_proxy/{entity_id}' - name = 'api:camera:image' + url = "/api/camera_proxy/{entity_id}" + name = "api:camera:image" async def handle(self, request, camera): """Serve camera image.""" @@ -495,8 +510,7 @@ class CameraImageView(CameraView): image = await camera.async_camera_image() if image: - return web.Response(body=image, - content_type=camera.content_type) + return web.Response(body=image, content_type=camera.content_type) raise web.HTTPInternalServerError() @@ -504,21 +518,22 @@ class CameraImageView(CameraView): class CameraMjpegStream(CameraView): """Camera View to serve an MJPEG stream.""" - url = '/api/camera_proxy_stream/{entity_id}' - name = 'api:camera:stream' + url = "/api/camera_proxy_stream/{entity_id}" + name = "api:camera:stream" async def handle(self, request, camera): """Serve camera stream, possibly with interval.""" - interval = request.query.get('interval') + interval = request.query.get("interval") if interval is None: return await camera.handle_async_mjpeg_stream(request) try: # Compose camera stream from stills - interval = float(request.query.get('interval')) + interval = float(request.query.get("interval")) if interval < MIN_STREAM_INTERVAL: - raise ValueError("Stream interval must be be > {}" - .format(MIN_STREAM_INTERVAL)) + raise ValueError( + "Stream interval must be be > {}".format(MIN_STREAM_INTERVAL) + ) return await camera.handle_async_still_stream(request, interval) except ValueError: raise web.HTTPBadRequest() @@ -531,29 +546,37 @@ async def websocket_camera_thumbnail(hass, connection, msg): Async friendly. """ try: - image = await async_get_image(hass, msg['entity_id']) - await connection.send_big_result(msg['id'], { - 'content_type': image.content_type, - 'content': base64.b64encode(image.content).decode('utf-8') - }) + image = await async_get_image(hass, msg["entity_id"]) + await connection.send_big_result( + msg["id"], + { + "content_type": image.content_type, + "content": base64.b64encode(image.content).decode("utf-8"), + }, + ) except HomeAssistantError: - connection.send_message(websocket_api.error_message( - msg['id'], 'image_fetch_failed', 'Unable to fetch image')) + connection.send_message( + websocket_api.error_message( + msg["id"], "image_fetch_failed", "Unable to fetch image" + ) + ) @websocket_api.async_response -@websocket_api.websocket_command({ - vol.Required('type'): 'camera/stream', - vol.Required('entity_id'): cv.entity_id, - vol.Optional('format', default='hls'): vol.In(OUTPUT_FORMATS), -}) +@websocket_api.websocket_command( + { + vol.Required("type"): "camera/stream", + vol.Required("entity_id"): cv.entity_id, + vol.Optional("format", default="hls"): vol.In(OUTPUT_FORMATS), + } +) async def ws_camera_stream(hass, connection, msg): """Handle get camera stream websocket command. Async friendly. """ try: - entity_id = msg['entity_id'] + entity_id = msg["entity_id"] camera = _get_camera_from_entity_id(hass, entity_id) camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id) @@ -561,51 +584,54 @@ async def ws_camera_stream(hass, connection, msg): source = await camera.stream_source() if not source: - raise HomeAssistantError("{} does not support play stream service" - .format(camera.entity_id)) + raise HomeAssistantError( + "{} does not support play stream service".format(camera.entity_id) + ) - fmt = msg['format'] - url = request_stream(hass, source, fmt=fmt, - keepalive=camera_prefs.preload_stream) - connection.send_result(msg['id'], {'url': url}) + fmt = msg["format"] + url = request_stream( + hass, source, fmt=fmt, keepalive=camera_prefs.preload_stream + ) + connection.send_result(msg["id"], {"url": url}) except HomeAssistantError as ex: _LOGGER.error("Error requesting stream: %s", ex) - connection.send_error( - msg['id'], 'start_stream_failed', str(ex)) + connection.send_error(msg["id"], "start_stream_failed", str(ex)) except asyncio.TimeoutError: _LOGGER.error("Timeout getting stream source") connection.send_error( - msg['id'], 'start_stream_failed', "Timeout getting stream source") + msg["id"], "start_stream_failed", "Timeout getting stream source" + ) @websocket_api.async_response -@websocket_api.websocket_command({ - vol.Required('type'): 'camera/get_prefs', - vol.Required('entity_id'): cv.entity_id, -}) +@websocket_api.websocket_command( + {vol.Required("type"): "camera/get_prefs", vol.Required("entity_id"): cv.entity_id} +) async def websocket_get_prefs(hass, connection, msg): """Handle request for account info.""" - prefs = hass.data[DATA_CAMERA_PREFS].get(msg['entity_id']) - connection.send_result(msg['id'], prefs.as_dict()) + prefs = hass.data[DATA_CAMERA_PREFS].get(msg["entity_id"]) + connection.send_result(msg["id"], prefs.as_dict()) @websocket_api.async_response -@websocket_api.websocket_command({ - vol.Required('type'): 'camera/update_prefs', - vol.Required('entity_id'): cv.entity_id, - vol.Optional('preload_stream'): bool, -}) +@websocket_api.websocket_command( + { + vol.Required("type"): "camera/update_prefs", + vol.Required("entity_id"): cv.entity_id, + vol.Optional("preload_stream"): bool, + } +) async def websocket_update_prefs(hass, connection, msg): """Handle request for account info.""" prefs = hass.data[DATA_CAMERA_PREFS] changes = dict(msg) - changes.pop('id') - changes.pop('type') - entity_id = changes.pop('entity_id') + changes.pop("id") + changes.pop("type") + entity_id = changes.pop("entity_id") await prefs.async_update(entity_id, **changes) - connection.send_result(msg['id'], prefs.get(entity_id).as_dict()) + connection.send_result(msg["id"], prefs.get(entity_id).as_dict()) async def async_handle_snapshot_service(camera, service): @@ -614,25 +640,22 @@ async def async_handle_snapshot_service(camera, service): filename = service.data[ATTR_FILENAME] filename.hass = hass - snapshot_file = filename.async_render( - variables={ATTR_ENTITY_ID: camera}) + snapshot_file = filename.async_render(variables={ATTR_ENTITY_ID: camera}) # check if we allow to access to that file if not hass.config.is_allowed_path(snapshot_file): - _LOGGER.error( - "Can't write %s, no access to path!", snapshot_file) + _LOGGER.error("Can't write %s, no access to path!", snapshot_file) return image = await camera.async_camera_image() def _write_image(to_file, image_data): """Executor helper to write image.""" - with open(to_file, 'wb') as img_file: + with open(to_file, "wb") as img_file: img_file.write(image_data) try: - await hass.async_add_executor_job( - _write_image, snapshot_file, image) + await hass.async_add_executor_job(_write_image, snapshot_file, image) except OSError as err: _LOGGER.error("Can't write image to file: %s", err) @@ -643,25 +666,25 @@ async def async_handle_play_stream_service(camera, service_call): source = await camera.stream_source() if not source: - raise HomeAssistantError("{} does not support play stream service" - .format(camera.entity_id)) + raise HomeAssistantError( + "{} does not support play stream service".format(camera.entity_id) + ) hass = camera.hass camera_prefs = hass.data[DATA_CAMERA_PREFS].get(camera.entity_id) fmt = service_call.data[ATTR_FORMAT] entity_ids = service_call.data[ATTR_MEDIA_PLAYER] - url = request_stream(hass, source, fmt=fmt, - keepalive=camera_prefs.preload_stream) + url = request_stream(hass, source, fmt=fmt, keepalive=camera_prefs.preload_stream) data = { ATTR_ENTITY_ID: entity_ids, ATTR_MEDIA_CONTENT_ID: "{}{}".format(hass.config.api.base_url, url), - ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt] + ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], } await hass.services.async_call( - DOMAIN_MP, SERVICE_PLAY_MEDIA, data, - blocking=True, context=service_call.context) + DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True, context=service_call.context + ) async def async_handle_record_service(camera, call): @@ -670,14 +693,14 @@ async def async_handle_record_service(camera, call): source = await camera.stream_source() if not source: - raise HomeAssistantError("{} does not support record service" - .format(camera.entity_id)) + raise HomeAssistantError( + "{} does not support record service".format(camera.entity_id) + ) hass = camera.hass filename = call.data[CONF_FILENAME] filename.hass = hass - video_path = filename.async_render( - variables={ATTR_ENTITY_ID: camera}) + video_path = filename.async_render(variables={ATTR_ENTITY_ID: camera}) data = { CONF_STREAM_SOURCE: source, @@ -687,5 +710,5 @@ async def async_handle_record_service(camera, call): } await hass.services.async_call( - DOMAIN_STREAM, SERVICE_RECORD, data, - blocking=True, context=call.context) + DOMAIN_STREAM, SERVICE_RECORD, data, blocking=True, context=call.context + ) diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py index f87ca47460e..563f0554f0f 100644 --- a/homeassistant/components/camera/const.py +++ b/homeassistant/components/camera/const.py @@ -1,6 +1,6 @@ """Constants for Camera component.""" -DOMAIN = 'camera' +DOMAIN = "camera" -DATA_CAMERA_PREFS = 'camera_prefs' +DATA_CAMERA_PREFS = "camera_prefs" -PREF_PRELOAD_STREAM = 'preload_stream' +PREF_PRELOAD_STREAM = "preload_stream" diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 927929bdf6e..5e22b882d0a 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -41,15 +41,14 @@ class CameraPreferences: self._prefs = prefs - async def async_update(self, entity_id, *, preload_stream=_UNDEF, - stream_options=_UNDEF): + async def async_update( + self, entity_id, *, preload_stream=_UNDEF, stream_options=_UNDEF + ): """Update camera preferences.""" if not self._prefs.get(entity_id): self._prefs[entity_id] = {} - for key, value in ( - (PREF_PRELOAD_STREAM, preload_stream), - ): + for key, value in ((PREF_PRELOAD_STREAM, preload_stream),): if value is not _UNDEF: self._prefs[entity_id][key] = value diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 52b38f14795..f23b6ad46c9 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -12,25 +12,28 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -NOTIFICATION_ID = 'canary_notification' -NOTIFICATION_TITLE = 'Canary Setup' +NOTIFICATION_ID = "canary_notification" +NOTIFICATION_TITLE = "Canary Setup" -DOMAIN = 'canary' -DATA_CANARY = 'canary' +DOMAIN = "canary" +DATA_CANARY = "canary" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) DEFAULT_TIMEOUT = 10 -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -CANARY_COMPONENTS = [ - 'alarm_control_panel', 'camera', 'sensor' -] +CANARY_COMPONENTS = ["alarm_control_panel", "camera", "sensor"] def setup(hass, config): @@ -45,11 +48,12 @@ def setup(hass, config): except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Canary service: %s", str(ex)) hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), + "Error: {}
" + "You will need to restart hass after fixing." + "".format(ex), title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) + notification_id=NOTIFICATION_ID, + ) return False for component in CANARY_COMPONENTS: @@ -64,6 +68,7 @@ class CanaryData: def __init__(self, username, password, timeout): """Init the Canary data object.""" from canary.api import Api + self._api = Api(username, password, timeout) self._locations_by_id = {} @@ -80,12 +85,14 @@ class CanaryData: self._locations_by_id[location_id] = location self._entries_by_location_id[location_id] = self._api.get_entries( - location_id, entry_type="motion", limit=1) + location_id, entry_type="motion", limit=1 + ) for device in location.devices: if device.is_online: - self._readings_by_device_id[device.device_id] = \ - self._api.get_latest_readings(device.device_id) + self._readings_by_device_id[ + device.device_id + ] = self._api.get_latest_readings(device.device_id) @property def locations(self): @@ -107,9 +114,14 @@ class CanaryData: def get_reading(self, device_id, sensor_type): """Return reading for device_id and sensor type.""" readings = self._readings_by_device_id.get(device_id, []) - return next(( - reading.value for reading in readings - if reading.sensor_type == sensor_type), None) + return next( + ( + reading.value + for reading in readings + if reading.sensor_type == sensor_type + ), + None, + ) def set_location_mode(self, location_id, mode_name, is_private=False): """Set location mode.""" diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 7402d785532..42b5048bc1d 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -3,8 +3,11 @@ import logging from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_DISARMED) + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, +) from . import DATA_CANARY @@ -39,8 +42,11 @@ class CanaryAlarm(AlarmControlPanel): @property def state(self): """Return the state of the device.""" - from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \ - LOCATION_MODE_NIGHT + from canary.api import ( + LOCATION_MODE_AWAY, + LOCATION_MODE_HOME, + LOCATION_MODE_NIGHT, + ) location = self._data.get_location(self._location_id) @@ -60,27 +66,27 @@ class CanaryAlarm(AlarmControlPanel): def device_state_attributes(self): """Return the state attributes.""" location = self._data.get_location(self._location_id) - return { - 'private': location.is_private - } + return {"private": location.is_private} def alarm_disarm(self, code=None): """Send disarm command.""" location = self._data.get_location(self._location_id) - self._data.set_location_mode(self._location_id, location.mode.name, - True) + self._data.set_location_mode(self._location_id, location.mode.name, True) def alarm_arm_home(self, code=None): """Send arm home command.""" from canary.api import LOCATION_MODE_HOME + self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME) def alarm_arm_away(self, code=None): """Send arm away command.""" from canary.api import LOCATION_MODE_AWAY + self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY) def alarm_arm_night(self, code=None): """Send arm night command.""" from canary.api import LOCATION_MODE_NIGHT + self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 9411ab2a41c..8a6d27b8916 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -15,14 +15,14 @@ from . import DATA_CANARY, DEFAULT_TIMEOUT _LOGGER = logging.getLogger(__name__) -CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' -DEFAULT_ARGUMENTS = '-pred 1' +CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" +DEFAULT_ARGUMENTS = "-pred 1" MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -34,8 +34,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for device in location.devices: if device.is_online: devices.append( - CanaryCamera(hass, data, location, device, DEFAULT_TIMEOUT, - config.get(CONF_FFMPEG_ARGUMENTS))) + CanaryCamera( + hass, + data, + location, + device, + DEFAULT_TIMEOUT, + config.get(CONF_FFMPEG_ARGUMENTS), + ) + ) add_entities(devices, True) @@ -75,11 +82,15 @@ class CanaryCamera(Camera): self.renew_live_stream_session() from haffmpeg.tools import ImageFrame, IMAGE_JPEG + ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) - image = await asyncio.shield(ffmpeg.get_image( - self._live_stream_session.live_stream_url, - output_format=IMAGE_JPEG, - extra_cmd=self._ffmpeg_arguments)) + image = await asyncio.shield( + ffmpeg.get_image( + self._live_stream_session.live_stream_url, + output_format=IMAGE_JPEG, + extra_cmd=self._ffmpeg_arguments, + ) + ) return image async def handle_async_mjpeg_stream(self, request): @@ -88,21 +99,24 @@ class CanaryCamera(Camera): return from haffmpeg.camera import CameraMjpeg + stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) await stream.open_camera( - self._live_stream_session.live_stream_url, - extra_cmd=self._ffmpeg_arguments) + self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments + ) try: stream_reader = await stream.get_reader() return await async_aiohttp_proxy_stream( - self.hass, request, stream_reader, - self._ffmpeg.ffmpeg_stream_content_type) + self.hass, + request, + stream_reader, + self._ffmpeg.ffmpeg_stream_content_type, + ) finally: await stream.close() @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) def renew_live_stream_session(self): """Renew live stream session.""" - self._live_stream_session = self._data.get_live_stream_session( - self._device) + self._live_stream_session = self._data.get_live_stream_session(self._device) diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 220abc9b387..dcb54a772a3 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -35,8 +35,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_type = device.device_type for sensor_type in SENSOR_TYPES: if device_type.get("name") in sensor_type[3]: - devices.append(CanarySensor(data, sensor_type, - location, device)) + devices.append( + CanarySensor(data, sensor_type, location, device) + ) add_entities(devices, True) @@ -52,9 +53,7 @@ class CanarySensor(Entity): self._sensor_value = None sensor_type_name = sensor_type[0].replace("_", " ").title() - self._name = '{} {} {}'.format(location.name, - device.name, - sensor_type_name) + self._name = "{} {} {}".format(location.name, device.name, sensor_type_name) @property def name(self): @@ -87,19 +86,16 @@ class CanarySensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - if self._sensor_type[0] == "air_quality" \ - and self._sensor_value is not None: + if self._sensor_type[0] == "air_quality" and self._sensor_value is not None: air_quality = None - if self._sensor_value <= .4: + if self._sensor_value <= 0.4: air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL - elif self._sensor_value <= .59: + elif self._sensor_value <= 0.59: air_quality = STATE_AIR_QUALITY_ABNORMAL elif self._sensor_value <= 1.0: air_quality = STATE_AIR_QUALITY_NORMAL - return { - ATTR_AIR_QUALITY: air_quality - } + return {ATTR_AIR_QUALITY: air_quality} return None @@ -108,6 +104,7 @@ class CanarySensor(Entity): self._data.update() from canary.api import SensorType + canary_sensor_type = None if self._sensor_type[0] == "air_quality": canary_sensor_type = SensorType.AIR_QUALITY diff --git a/homeassistant/components/cast/.translations/bg.json b/homeassistant/components/cast/.translations/bg.json new file mode 100644 index 00000000000..c56bf118dc1 --- /dev/null +++ b/homeassistant/components/cast/.translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0412 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u043d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 Google Cast \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Google Cast." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/pt-BR.json b/homeassistant/components/cast/.translations/pt-BR.json index bd670d7c72f..e26a829480c 100644 --- a/homeassistant/components/cast/.translations/pt-BR.json +++ b/homeassistant/components/cast/.translations/pt-BR.json @@ -7,9 +7,9 @@ "step": { "confirm": { "description": "Deseja configurar o Google Cast?", - "title": "" + "title": "Google Cast" } }, - "title": "" + "title": "Google Cast" } } \ No newline at end of file diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index f91b90c1e08..cc112984f88 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -11,14 +11,18 @@ async def async_setup(hass, config): hass.data[DOMAIN] = conf or {} if conf is not None: - hass.async_create_task(hass.config_entries.flow.async_init( - DOMAIN, context={'source': config_entries.SOURCE_IMPORT})) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + ) return True async def async_setup_entry(hass, entry): """Set up Cast from a config entry.""" - hass.async_create_task(hass.config_entries.async_forward_entry_setup( - entry, 'media_player')) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "media_player") + ) return True diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index 0f8696cf29c..c3c21944d02 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -12,5 +12,5 @@ async def _async_has_devices(hass): config_entry_flow.register_discovery_flow( - DOMAIN, 'Google Cast', _async_has_devices, - config_entries.CONN_CLASS_LOCAL_PUSH) + DOMAIN, "Google Cast", _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH +) diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index 48bb87ca5d7..e9f9ba4c39d 100644 --- a/homeassistant/components/cast/const.py +++ b/homeassistant/components/cast/const.py @@ -1,3 +1,3 @@ """Consts for Cast integration.""" -DOMAIN = 'cast' +DOMAIN = "cast" diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 07818c03057..af9f39f8ed4 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -7,64 +7,86 @@ from typing import Optional, Tuple import attr import voluptuous as vol -from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, MediaPlayerDevice) +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_TVSHOW, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) from homeassistant.const import ( - CONF_HOST, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_PAUSED, - STATE_PLAYING) + CONF_HOST, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro from . import DOMAIN as CAST_DOMAIN -DEPENDENCIES = ('cast',) +DEPENDENCIES = ("cast",) _LOGGER = logging.getLogger(__name__) -CONF_IGNORE_CEC = 'ignore_cec' -CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' +CONF_IGNORE_CEC = "ignore_cec" +CAST_SPLASH = "https://home-assistant.io/images/cast/splash.png" DEFAULT_PORT = 8009 -SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA | \ - SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ - SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET +SUPPORT_CAST = ( + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET +) # Stores a threading.Lock that is held by the internal pychromecast discovery. -INTERNAL_DISCOVERY_RUNNING_KEY = 'cast_discovery_running' +INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running" # Stores all ChromecastInfo we encountered through discovery or config as a set # If we find a chromecast with a new host, the old one will be removed again. -KNOWN_CHROMECAST_INFO_KEY = 'cast_known_chromecasts' +KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts" # Stores UUIDs of cast devices that were added as entities. Doesn't store # None UUIDs. -ADDED_CAST_DEVICES_KEY = 'cast_added_cast_devices' +ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices" # Stores an audio group manager. -CAST_MULTIZONE_MANAGER_KEY = 'cast_multizone_manager' +CAST_MULTIZONE_MANAGER_KEY = "cast_multizone_manager" # Dispatcher signal fired with a ChromecastInfo every time we discover a new # Chromecast or receive it through configuration -SIGNAL_CAST_DISCOVERED = 'cast_discovered' +SIGNAL_CAST_DISCOVERED = "cast_discovered" # Dispatcher signal fired with a ChromecastInfo every time a Chromecast is # removed -SIGNAL_CAST_REMOVED = 'cast_removed' +SIGNAL_CAST_REMOVED = "cast_removed" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_IGNORE_CEC, default=[]): - vol.All(cv.ensure_list, [cv.string]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_IGNORE_CEC, default=[]): vol.All(cv.ensure_list, [cv.string]), + } +) @attr.s(slots=True, frozen=True) @@ -77,10 +99,11 @@ class ChromecastInfo: host = attr.ib(type=str) port = attr.ib(type=int) service = attr.ib(type=Optional[str], default=None) - uuid = attr.ib(type=Optional[str], converter=attr.converters.optional(str), - default=None) # always convert UUID to string if not None - manufacturer = attr.ib(type=str, default='') - model_name = attr.ib(type=str, default='') + uuid = attr.ib( + type=Optional[str], converter=attr.converters.optional(str), default=None + ) # always convert UUID to string if not None + manufacturer = attr.ib(type=str, default="") + model_name = attr.ib(type=str, default="") friendly_name = attr.ib(type=Optional[str], default=None) is_dynamic_group = attr.ib(type=Optional[bool], default=None) @@ -95,10 +118,16 @@ class ChromecastInfo: want_dynamic_group = self.is_audio_group have_dynamic_group = self.is_dynamic_group is not None have_all_except_dynamic_group = all( - attr.astuple(self, filter=attr.filters.exclude( - attr.fields(ChromecastInfo).is_dynamic_group))) - return (have_all_except_dynamic_group and - (not want_dynamic_group or have_dynamic_group)) + attr.astuple( + self, + filter=attr.filters.exclude( + attr.fields(ChromecastInfo).is_dynamic_group + ), + ) + ) + return have_all_except_dynamic_group and ( + not want_dynamic_group or have_dynamic_group + ) @property def host_port(self) -> Tuple[str, int]: @@ -106,11 +135,14 @@ class ChromecastInfo: return self.host, self.port -def _is_matching_dynamic_group(our_info: ChromecastInfo, - new_info: ChromecastInfo,) -> bool: - return (our_info.is_audio_group and - new_info.is_dynamic_group and - our_info.friendly_name == new_info.friendly_name) +def _is_matching_dynamic_group( + our_info: ChromecastInfo, new_info: ChromecastInfo +) -> bool: + return ( + our_info.is_audio_group + and new_info.is_dynamic_group + and our_info.friendly_name == new_info.friendly_name + ) def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: @@ -129,35 +161,40 @@ def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: dynamic_groups = [] if info.uuid: http_group_status = dial.get_multizone_status( - info.host, services=[info.service], - zconf=ChromeCastZeroconf.get_zeroconf()) + info.host, + services=[info.service], + zconf=ChromeCastZeroconf.get_zeroconf(), + ) if http_group_status is not None: - dynamic_groups = \ - [str(g.uuid) for g in http_group_status.dynamic_groups] + dynamic_groups = [str(g.uuid) for g in http_group_status.dynamic_groups] is_dynamic_group = info.uuid in dynamic_groups return ChromecastInfo( - service=info.service, host=info.host, port=info.port, + service=info.service, + host=info.host, + port=info.port, uuid=info.uuid, friendly_name=info.friendly_name, manufacturer=info.manufacturer, model_name=info.model_name, - is_dynamic_group=is_dynamic_group + is_dynamic_group=is_dynamic_group, ) http_device_status = dial.get_device_status( - info.host, services=[info.service], - zconf=ChromeCastZeroconf.get_zeroconf()) + info.host, services=[info.service], zconf=ChromeCastZeroconf.get_zeroconf() + ) if http_device_status is None: # HTTP dial didn't give us any new information. return info return ChromecastInfo( - service=info.service, host=info.host, port=info.port, + service=info.service, + host=info.host, + port=info.port, uuid=(info.uuid or http_device_status.uuid), friendly_name=(info.friendly_name or http_device_status.friendly_name), manufacturer=(info.manufacturer or http_device_status.manufacturer), - model_name=(info.model_name or http_device_status.model_name) + model_name=(info.model_name or http_device_status.model_name), ) @@ -171,8 +208,9 @@ def _discover_chromecast(hass: HomeAssistantType, info: ChromecastInfo): if info.uuid is not None: # Remove previous cast infos with same uuid from known chromecasts. - same_uuid = set(x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] - if info.uuid == x.uuid) + same_uuid = set( + x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] if info.uuid == x.uuid + ) hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) @@ -216,29 +254,36 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: def internal_add_callback(name): """Handle zeroconf discovery of a new chromecast.""" mdns = listener.services[name] - _discover_chromecast(hass, ChromecastInfo( - service=name, - host=mdns[0], - port=mdns[1], - uuid=mdns[2], - model_name=mdns[3], - friendly_name=mdns[4], - )) + _discover_chromecast( + hass, + ChromecastInfo( + service=name, + host=mdns[0], + port=mdns[1], + uuid=mdns[2], + model_name=mdns[3], + friendly_name=mdns[4], + ), + ) def internal_remove_callback(name, mdns): """Handle zeroconf discovery of a removed chromecast.""" - _remove_chromecast(hass, ChromecastInfo( - service=name, - host=mdns[0], - port=mdns[1], - uuid=mdns[2], - model_name=mdns[3], - friendly_name=mdns[4], - )) + _remove_chromecast( + hass, + ChromecastInfo( + service=name, + host=mdns[0], + port=mdns[1], + uuid=mdns[2], + model_name=mdns[3], + friendly_name=mdns[4], + ), + ) _LOGGER.debug("Starting internal pychromecast discovery.") - listener, browser = pychromecast.start_discovery(internal_add_callback, - internal_remove_callback) + listener, browser = pychromecast.start_discovery( + internal_add_callback, internal_remove_callback + ) ChromeCastZeroconf.set_zeroconf(browser.zc) def stop_discovery(event): @@ -251,8 +296,7 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: @callback -def _async_create_cast_device(hass: HomeAssistantType, - info: ChromecastInfo): +def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo): """Create a CastDevice Entity from the chromecast object. Returns None if the cast device has already been added. @@ -278,29 +322,30 @@ def _async_create_cast_device(hass: HomeAssistantType, return CastDevice(info) -async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, - async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +): """Set up thet Cast platform. Deprecated. """ _LOGGER.warning( - 'Setting configuration for Cast via platform is deprecated. ' - 'Configure via Cast integration instead.') - await _async_setup_platform( - hass, config, async_add_entities, discovery_info) + "Setting configuration for Cast via platform is deprecated. " + "Configure via Cast integration instead." + ) + await _async_setup_platform(hass, config, async_add_entities, discovery_info) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Cast from a config entry.""" - config = hass.data[CAST_DOMAIN].get('media_player', {}) + config = hass.data[CAST_DOMAIN].get("media_player", {}) if not isinstance(config, list): config = [config] # no pending task - done, _ = await asyncio.wait([ - _async_setup_platform(hass, cfg, async_add_entities, None) - for cfg in config]) + done, _ = await asyncio.wait( + [_async_setup_platform(hass, cfg, async_add_entities, None) for cfg in config] + ) if any([task.exception() for task in done]): exceptions = [task.exception() for task in done] for exception in exceptions: @@ -308,8 +353,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): raise PlatformNotReady -async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, - async_add_entities, discovery_info): +async def _async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info +): """Set up the cast platform.""" import pychromecast @@ -320,11 +366,9 @@ async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, info = None if discovery_info is not None: - info = ChromecastInfo(host=discovery_info['host'], - port=discovery_info['port']) + info = ChromecastInfo(host=discovery_info["host"], port=discovery_info["port"]) elif CONF_HOST in config: - info = ChromecastInfo(host=config[CONF_HOST], - port=DEFAULT_PORT) + info = ChromecastInfo(host=config[CONF_HOST], port=DEFAULT_PORT) @callback def async_cast_discovered(discover: ChromecastInfo) -> None: @@ -337,8 +381,7 @@ async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, if cast_device is not None: async_add_entities([cast_device]) - async_dispatcher_connect( - hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) + async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) # Re-play the callback for all past chromecasts, store the objects in # a list to avoid concurrent modification resulting in exception. for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]): @@ -349,11 +392,13 @@ async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, # b) have an audio group cast device, we need internal discovery. hass.async_add_job(_setup_internal_discovery, hass) else: - info = await hass.async_add_job(_fill_out_missing_chromecast_info, - info) + info = await hass.async_add_job(_fill_out_missing_chromecast_info, info) if info.friendly_name is None: - _LOGGER.debug("Cannot retrieve detail information for chromecast" - " %s, the device may not be online", info) + _LOGGER.debug( + "Cannot retrieve detail information for chromecast" + " %s, the device may not be online", + info, + ) hass.async_add_job(_discover_chromecast, hass, info) @@ -374,8 +419,7 @@ class CastStatusListener: self._mz_mgr = mz_mgr chromecast.register_status_listener(self) - chromecast.socket_client.media_controller.register_status_listener( - self) + chromecast.socket_client.media_controller.register_status_listener(self) chromecast.register_connection_listener(self) # pylint: disable=protected-access if cast_device._cast_info.is_audio_group: @@ -415,8 +459,7 @@ class CastStatusListener: def multizone_new_media_status(self, group_uuid, media_status): """Handle reception of a new MediaStatus for a group.""" if self._valid: - self._cast_device.multizone_new_media_status( - group_uuid, media_status) + self._cast_device.multizone_new_media_status(group_uuid, media_status) def invalidate(self): """Invalidate this status listener. @@ -447,8 +490,7 @@ class DynamicGroupCastStatusListener: self._mz_mgr = mz_mgr chromecast.register_status_listener(self) - chromecast.socket_client.media_controller.register_status_listener( - self) + chromecast.socket_client.media_controller.register_status_listener(self) chromecast.register_connection_listener(self) self._mz_mgr.add_multizone(chromecast) @@ -464,8 +506,7 @@ class DynamicGroupCastStatusListener: def new_connection_status(self, connection_status): """Handle reception of a new ConnectionStatus.""" if self._valid: - self._cast_device.new_dynamic_group_connection_status( - connection_status) + self._cast_device.new_dynamic_group_connection_status(connection_status) def invalidate(self): """Invalidate this status listener. @@ -487,6 +528,7 @@ class CastDevice(MediaPlayerDevice): def __init__(self, cast_info): """Initialize the cast device.""" import pychromecast # noqa: pylint: disable=unused-import + self._cast_info = cast_info # type: ChromecastInfo self.services = None if cast_info.service: @@ -497,8 +539,7 @@ class CastDevice(MediaPlayerDevice): self.media_status = None self.media_status_received = None self._dynamic_group_cast_info = None # type: ChromecastInfo - self._dynamic_group_cast = None \ - # type: Optional[pychromecast.Chromecast] + self._dynamic_group_cast = None # type: Optional[pychromecast.Chromecast] self.dynamic_group_media_status = None self.dynamic_group_media_status_received = None self.mz_media_status = {} @@ -507,13 +548,15 @@ class CastDevice(MediaPlayerDevice): self._available = False # type: bool self._dynamic_group_available = False # type: bool self._status_listener = None # type: Optional[CastStatusListener] - self._dynamic_group_status_listener = None \ - # type: Optional[DynamicGroupCastStatusListener] + self._dynamic_group_status_listener = ( + None + ) # type: Optional[DynamicGroupCastStatusListener] self._add_remove_handler = None self._del_remove_handler = None async def async_added_to_hass(self): """Create chromecast object when added to hass.""" + @callback def async_cast_discovered(discover: ChromecastInfo): """Handle discovery of new Chromecast.""" @@ -521,10 +564,10 @@ class CastDevice(MediaPlayerDevice): # We can't handle empty UUIDs return if _is_matching_dynamic_group(self._cast_info, discover): - _LOGGER.debug("Discovered matching dynamic group: %s", - discover) - self.hass.async_create_task(async_create_catching_coro( - self.async_set_dynamic_group(discover))) + _LOGGER.debug("Discovered matching dynamic group: %s", discover) + self.hass.async_create_task( + async_create_catching_coro(self.async_set_dynamic_group(discover)) + ) return if self._cast_info.uuid != discover.uuid: @@ -533,51 +576,66 @@ class CastDevice(MediaPlayerDevice): if self.services is None: _LOGGER.warning( "[%s %s (%s:%s)] Received update for manually added Cast", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port) + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + ) return _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) - self.hass.async_create_task(async_create_catching_coro( - self.async_set_cast_info(discover))) + self.hass.async_create_task( + async_create_catching_coro(self.async_set_cast_info(discover)) + ) def async_cast_removed(discover: ChromecastInfo): """Handle removal of Chromecast.""" if self._cast_info.uuid is None: # We can't handle empty UUIDs return - if (self._dynamic_group_cast_info is not None and - self._dynamic_group_cast_info.uuid == discover.uuid): + if ( + self._dynamic_group_cast_info is not None + and self._dynamic_group_cast_info.uuid == discover.uuid + ): _LOGGER.debug("Removed matching dynamic group: %s", discover) - self.hass.async_create_task(async_create_catching_coro( - self.async_del_dynamic_group())) + self.hass.async_create_task( + async_create_catching_coro(self.async_del_dynamic_group()) + ) return if self._cast_info.uuid != discover.uuid: # Removed is not our device. return _LOGGER.debug("Removed chromecast with same UUID: %s", discover) - self.hass.async_create_task(async_create_catching_coro( - self.async_del_cast_info(discover))) + self.hass.async_create_task( + async_create_catching_coro(self.async_del_cast_info(discover)) + ) async def async_stop(event): """Disconnect socket on Home Assistant stop.""" await self._async_disconnect() self._add_remove_handler = async_dispatcher_connect( - self.hass, SIGNAL_CAST_DISCOVERED, - async_cast_discovered) + self.hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered + ) self._del_remove_handler = async_dispatcher_connect( - self.hass, SIGNAL_CAST_REMOVED, - async_cast_removed) + self.hass, SIGNAL_CAST_REMOVED, async_cast_removed + ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) - self.hass.async_create_task(async_create_catching_coro( - self.async_set_cast_info(self._cast_info))) + self.hass.async_create_task( + async_create_catching_coro(self.async_set_cast_info(self._cast_info)) + ) for info in self.hass.data[KNOWN_CHROMECAST_INFO_KEY]: if _is_matching_dynamic_group(self._cast_info, info): - _LOGGER.debug("[%s %s (%s:%s)] Found dynamic group: %s", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, info) - self.hass.async_create_task(async_create_catching_coro( - self.async_set_dynamic_group(info))) + _LOGGER.debug( + "[%s %s (%s:%s)] Found dynamic group: %s", + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + info, + ) + self.hass.async_create_task( + async_create_catching_coro(self.async_set_dynamic_group(info)) + ) break async def async_will_remove_from_hass(self) -> None: @@ -595,14 +653,20 @@ class CastDevice(MediaPlayerDevice): async def async_set_cast_info(self, cast_info): """Set the cast information and set up the chromecast object.""" import pychromecast + self._cast_info = cast_info if self.services is not None: if cast_info.service not in self.services: - _LOGGER.debug("[%s %s (%s:%s)] Got new service: %s (%s)", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, - cast_info.service, self.services) + _LOGGER.debug( + "[%s %s (%s:%s)] Got new service: %s (%s)", + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + cast_info.service, + self.services, + ) self.services.add(cast_info.service) @@ -615,33 +679,50 @@ class CastDevice(MediaPlayerDevice): if self.services is None: _LOGGER.debug( "[%s %s (%s:%s)] Connecting to cast device by host %s", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, cast_info) + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + cast_info, + ) chromecast = await self.hass.async_add_job( - pychromecast._get_chromecast_from_host, ( - cast_info.host, cast_info.port, cast_info.uuid, - cast_info.model_name, cast_info.friendly_name - )) + pychromecast._get_chromecast_from_host, + ( + cast_info.host, + cast_info.port, + cast_info.uuid, + cast_info.model_name, + cast_info.friendly_name, + ), + ) else: _LOGGER.debug( "[%s %s (%s:%s)] Connecting to cast device by service %s", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, self.services) + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + self.services, + ) chromecast = await self.hass.async_add_job( - pychromecast._get_chromecast_from_service, ( - self.services, ChromeCastZeroconf.get_zeroconf(), - cast_info.uuid, cast_info.model_name, - cast_info.friendly_name - )) + pychromecast._get_chromecast_from_service, + ( + self.services, + ChromeCastZeroconf.get_zeroconf(), + cast_info.uuid, + cast_info.model_name, + cast_info.friendly_name, + ), + ) self._chromecast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: from pychromecast.controllers.multizone import MultizoneManager + self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager() self.mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] - self._status_listener = CastStatusListener( - self, chromecast, self.mz_mgr) + self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr) self._available = False self.cast_status = chromecast.status self.media_status = chromecast.media_controller.status @@ -651,38 +732,55 @@ class CastDevice(MediaPlayerDevice): async def async_del_cast_info(self, cast_info): """Remove the service.""" self.services.discard(cast_info.service) - _LOGGER.debug("[%s %s (%s:%s)] Remove service: %s (%s)", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, - cast_info.service, self.services) + _LOGGER.debug( + "[%s %s (%s:%s)] Remove service: %s (%s)", + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + cast_info.service, + self.services, + ) async def async_set_dynamic_group(self, cast_info): """Set the cast information and set up the chromecast object.""" import pychromecast + _LOGGER.debug( "[%s %s (%s:%s)] Connecting to dynamic group by host %s", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, cast_info) + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + cast_info, + ) await self.async_del_dynamic_group() self._dynamic_group_cast_info = cast_info # pylint: disable=protected-access chromecast = await self.hass.async_add_executor_job( - pychromecast._get_chromecast_from_host, ( - cast_info.host, cast_info.port, cast_info.uuid, - cast_info.model_name, cast_info.friendly_name - )) + pychromecast._get_chromecast_from_host, + ( + cast_info.host, + cast_info.port, + cast_info.uuid, + cast_info.model_name, + cast_info.friendly_name, + ), + ) self._dynamic_group_cast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: from pychromecast.controllers.multizone import MultizoneManager + self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager() mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] self._dynamic_group_status_listener = DynamicGroupCastStatusListener( - self, chromecast, mz_mgr) + self, chromecast, mz_mgr + ) self._dynamic_group_available = False self.dynamic_group_media_status = chromecast.media_controller.status self._dynamic_group_cast.start() @@ -691,16 +789,19 @@ class CastDevice(MediaPlayerDevice): async def async_del_dynamic_group(self): """Remove the dynamic group.""" cast_info = self._dynamic_group_cast_info - _LOGGER.debug("[%s %s (%s:%s)] Remove dynamic group: %s", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, - cast_info.service if cast_info else None) + _LOGGER.debug( + "[%s %s (%s:%s)] Remove dynamic group: %s", + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + cast_info.service if cast_info else None, + ) self._dynamic_group_available = False self._dynamic_group_cast_info = None if self._dynamic_group_cast is not None: - await self.hass.async_add_executor_job( - self._dynamic_group_cast.disconnect) + await self.hass.async_add_executor_job(self._dynamic_group_cast.disconnect) self._dynamic_group_invalidate() @@ -711,16 +812,19 @@ class CastDevice(MediaPlayerDevice): if self._chromecast is None: # Can't disconnect if not connected. return - _LOGGER.debug("[%s %s (%s:%s)] Disconnecting from chromecast socket.", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port) + _LOGGER.debug( + "[%s %s (%s:%s)] Disconnecting from chromecast socket.", + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + ) self._available = False self.async_schedule_update_ha_state() await self.hass.async_add_executor_job(self._chromecast.disconnect) if self._dynamic_group_cast is not None: - await self.hass.async_add_executor_job( - self._dynamic_group_cast.disconnect) + await self.hass.async_add_executor_job(self._dynamic_group_cast.disconnect) self._invalidate() @@ -762,14 +866,19 @@ class CastDevice(MediaPlayerDevice): def new_connection_status(self, connection_status): """Handle updates of connection status.""" - from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, \ - CONNECTION_STATUS_DISCONNECTED + from pychromecast.socket_client import ( + CONNECTION_STATUS_CONNECTED, + CONNECTION_STATUS_DISCONNECTED, + ) _LOGGER.debug( "[%s %s (%s:%s)] Received cast device connection status: %s", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, - connection_status.status) + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + connection_status.status, + ) if connection_status.status == CONNECTION_STATUS_DISCONNECTED: self._available = False self._invalidate() @@ -783,9 +892,12 @@ class CastDevice(MediaPlayerDevice): # on state machine. _LOGGER.debug( "[%s %s (%s:%s)] Cast device availability changed: %s", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, - connection_status.status) + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + connection_status.status, + ) info = self._cast_info if info.friendly_name is None and not info.is_audio_group: # We couldn't find friendly_name when the cast was added, retry @@ -801,14 +913,19 @@ class CastDevice(MediaPlayerDevice): def new_dynamic_group_connection_status(self, connection_status): """Handle updates of connection status.""" - from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, \ - CONNECTION_STATUS_DISCONNECTED + from pychromecast.socket_client import ( + CONNECTION_STATUS_CONNECTED, + CONNECTION_STATUS_DISCONNECTED, + ) _LOGGER.debug( "[%s %s (%s:%s)] Received dynamic group connection status: %s", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, - connection_status.status) + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + connection_status.status, + ) if connection_status.status == CONNECTION_STATUS_DISCONNECTED: self._dynamic_group_available = False self._dynamic_group_invalidate() @@ -822,9 +939,12 @@ class CastDevice(MediaPlayerDevice): # on state machine. _LOGGER.debug( "[%s %s (%s:%s)] Dynamic group availability changed: %s", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, - connection_status.status) + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + connection_status.status, + ) self._dynamic_group_available = new_available self.schedule_update_ha_state() @@ -832,9 +952,13 @@ class CastDevice(MediaPlayerDevice): """Handle updates of audio group media status.""" _LOGGER.debug( "[%s %s (%s:%s)] Multizone %s media status: %s", - self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, self._cast_info.port, - group_uuid, media_status) + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + group_uuid, + media_status, + ) self.mz_media_status[group_uuid] = media_status self.mz_media_status_received[group_uuid] = dt_util.utcnow() self.schedule_update_ha_state() @@ -850,18 +974,17 @@ class CastDevice(MediaPlayerDevice): media_status = self.media_status media_controller = self._chromecast.media_controller - if ((media_status is None or media_status.player_state == "UNKNOWN") - and self._dynamic_group_cast is not None): + if ( + media_status is None or media_status.player_state == "UNKNOWN" + ) and self._dynamic_group_cast is not None: media_status = self.dynamic_group_media_status - media_controller = \ - self._dynamic_group_cast.media_controller + media_controller = self._dynamic_group_cast.media_controller if media_status is None or media_status.player_state == "UNKNOWN": groups = self.mz_media_status for k, val in groups.items(): if val and val.player_state != "UNKNOWN": - media_controller = \ - self.mz_mgr.get_multizone_mediacontroller(k) + media_controller = self.mz_mgr.get_multizone_mediacontroller(k) break return media_controller @@ -879,8 +1002,7 @@ class CastDevice(MediaPlayerDevice): self._chromecast.quit_app() # The only way we can turn the Chromecast is on is by launching an app - self._chromecast.play_media(CAST_SPLASH, - pychromecast.STREAM_TYPE_BUFFERED) + self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) def turn_off(self): """Turn off the cast device.""" @@ -949,12 +1071,10 @@ class CastDevice(MediaPlayerDevice): return None return { - 'name': cast_info.friendly_name, - 'identifiers': { - (CAST_DOMAIN, cast_info.uuid.replace('-', '')) - }, - 'model': cast_info.model_name, - 'manufacturer': cast_info.manufacturer, + "name": cast_info.friendly_name, + "identifiers": {(CAST_DOMAIN, cast_info.uuid.replace("-", ""))}, + "model": cast_info.model_name, + "manufacturer": cast_info.manufacturer, } def _media_status(self): @@ -967,8 +1087,9 @@ class CastDevice(MediaPlayerDevice): media_status = self.media_status media_status_received = self.media_status_received - if ((media_status is None or media_status.player_state == "UNKNOWN") - and self._dynamic_group_cast is not None): + if ( + media_status is None or media_status.player_state == "UNKNOWN" + ) and self._dynamic_group_cast is not None: media_status = self.dynamic_group_media_status media_status_received = self.dynamic_group_media_status_received @@ -1134,10 +1255,11 @@ class CastDevice(MediaPlayerDevice): def media_position(self): """Position of current playing media in seconds.""" media_status, _ = self._media_status() - if media_status is None or \ - not (media_status.player_is_playing or - media_status.player_is_paused or - media_status.player_is_idle): + if media_status is None or not ( + media_status.player_is_playing + or media_status.player_is_paused + or media_status.player_is_idle + ): return None return media_status.current_time diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index f1fff08755f..b1e0d819358 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -8,28 +8,35 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, - EVENT_HOMEASSISTANT_START) +from homeassistant.const import ( + CONF_NAME, + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_START, +) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'SSL Certificate Expiry' +DEFAULT_NAME = "SSL Certificate Expiry" DEFAULT_PORT = 443 SCAN_INTERVAL = timedelta(hours=12) TIMEOUT = 10.0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up certificate expiry sensor.""" + def run_setup(event): """Wait until Home Assistant is fully initialized before creating. @@ -39,8 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): server_port = config.get(CONF_PORT) sensor_name = config.get(CONF_NAME) - add_entities([SSLCertificate(sensor_name, server_name, server_port)], - True) + add_entities([SSLCertificate(sensor_name, server_name, server_port)], True) # To allow checking of the HA certificate we must first be running. hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) @@ -65,7 +71,7 @@ class SSLCertificate(Entity): @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" - return 'days' + return "days" @property def state(self): @@ -75,7 +81,7 @@ class SSLCertificate(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return 'mdi:certificate' + return "mdi:certificate" @property def available(self): @@ -87,10 +93,8 @@ class SSLCertificate(Entity): ctx = ssl.create_default_context() try: address = (self.server_name, self.server_port) - with socket.create_connection( - address, timeout=TIMEOUT) as sock: - with ctx.wrap_socket( - sock, server_hostname=address[0]) as ssock: + with socket.create_connection(address, timeout=TIMEOUT) as sock: + with ctx.wrap_socket(sock, server_hostname=address[0]) as ssock: cert = ssock.getpeercert() except socket.gaierror: @@ -98,17 +102,17 @@ class SSLCertificate(Entity): self._available = False return except socket.timeout: - _LOGGER.error( - "Connection timeout with server: %s", self.server_name) + _LOGGER.error("Connection timeout with server: %s", self.server_name) self._available = False return except OSError: - _LOGGER.error("Cannot fetch certificate from %s", - self.server_name, exc_info=1) + _LOGGER.error( + "Cannot fetch certificate from %s", self.server_name, exc_info=1 + ) self._available = False return - ts_seconds = ssl.cert_time_to_seconds(cert['notAfter']) + ts_seconds = ssl.cert_time_to_seconds(cert["notAfter"]) timestamp = datetime.fromtimestamp(ts_seconds) expiry = timestamp - datetime.today() self._available = True diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index abd3281d11a..6c3e18cdb05 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -3,54 +3,77 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, - MEDIA_TYPE_TVSHOW, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_VOLUME_MUTE) + DOMAIN, + MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_TVSHOW, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, +) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, STATE_IDLE, STATE_PAUSED, - STATE_PLAYING) + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + CONF_PORT, + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DATA_CHANNELS = 'channels' -DEFAULT_NAME = 'Channels' +DATA_CHANNELS = "channels" +DEFAULT_NAME = "Channels" DEFAULT_PORT = 57000 -FEATURE_SUPPORT = SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | \ - SUPPORT_VOLUME_MUTE | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_PLAY_MEDIA | SUPPORT_SELECT_SOURCE +FEATURE_SUPPORT = ( + SUPPORT_PLAY + | SUPPORT_PAUSE + | SUPPORT_STOP + | SUPPORT_VOLUME_MUTE + | SUPPORT_NEXT_TRACK + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_PLAY_MEDIA + | SUPPORT_SELECT_SOURCE +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) -SERVICE_SEEK_FORWARD = 'channels_seek_forward' -SERVICE_SEEK_BACKWARD = 'channels_seek_backward' -SERVICE_SEEK_BY = 'channels_seek_by' +SERVICE_SEEK_FORWARD = "channels_seek_forward" +SERVICE_SEEK_BACKWARD = "channels_seek_backward" +SERVICE_SEEK_BY = "channels_seek_by" # Service call validation schemas -ATTR_SECONDS = 'seconds' +ATTR_SECONDS = "seconds" -CHANNELS_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_id, -}) +CHANNELS_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_id}) -CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend({ - vol.Required(ATTR_SECONDS): vol.Coerce(int), -}) +CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend( + {vol.Required(ATTR_SECONDS): vol.Coerce(int)} +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Channels platform.""" device = ChannelsPlayer( - config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT)) + config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT) + ) if DATA_CHANNELS not in hass.data: hass.data[DATA_CHANNELS] = [] @@ -62,12 +85,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Handle service.""" entity_id = service.data.get(ATTR_ENTITY_ID) - device = next((device for device in hass.data[DATA_CHANNELS] if - device.entity_id == entity_id), None) + device = next( + ( + device + for device in hass.data[DATA_CHANNELS] + if device.entity_id == entity_id + ), + None, + ) if device is None: - _LOGGER.warning( - "Unable to find Channels with entity_id: %s", entity_id) + _LOGGER.warning("Unable to find Channels with entity_id: %s", entity_id) return if service.service == SERVICE_SEEK_FORWARD: @@ -75,18 +103,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None): elif service.service == SERVICE_SEEK_BACKWARD: device.seek_backward() elif service.service == SERVICE_SEEK_BY: - seconds = service.data.get('seconds') + seconds = service.data.get("seconds") device.seek_by(seconds) hass.services.register( - DOMAIN, SERVICE_SEEK_FORWARD, service_handler, schema=CHANNELS_SCHEMA) + DOMAIN, SERVICE_SEEK_FORWARD, service_handler, schema=CHANNELS_SCHEMA + ) hass.services.register( - DOMAIN, SERVICE_SEEK_BACKWARD, service_handler, schema=CHANNELS_SCHEMA) + DOMAIN, SERVICE_SEEK_BACKWARD, service_handler, schema=CHANNELS_SCHEMA + ) hass.services.register( - DOMAIN, SERVICE_SEEK_BY, service_handler, - schema=CHANNELS_SEEK_BY_SCHEMA) + DOMAIN, SERVICE_SEEK_BY, service_handler, schema=CHANNELS_SEEK_BY_SCHEMA + ) class ChannelsPlayer(MediaPlayerDevice): @@ -124,28 +154,28 @@ class ChannelsPlayer(MediaPlayerDevice): def update_state(self, state_hash): """Update all the state properties with the passed in dictionary.""" - self.status = state_hash.get('status', "stopped") - self.muted = state_hash.get('muted', False) + self.status = state_hash.get("status", "stopped") + self.muted = state_hash.get("muted", False) - channel_hash = state_hash.get('channel') - np_hash = state_hash.get('now_playing') + channel_hash = state_hash.get("channel") + np_hash = state_hash.get("now_playing") if channel_hash: - self.channel_number = channel_hash.get('channel_number') - self.channel_name = channel_hash.get('channel_name') - self.channel_image_url = channel_hash.get('channel_image_url') + self.channel_number = channel_hash.get("channel_number") + self.channel_name = channel_hash.get("channel_name") + self.channel_image_url = channel_hash.get("channel_image_url") else: self.channel_number = None self.channel_name = None self.channel_image_url = None if np_hash: - self.now_playing_title = np_hash.get('title') - self.now_playing_episode_title = np_hash.get('episode_title') - self.now_playing_season_number = np_hash.get('season_number') - self.now_playing_episode_number = np_hash.get('episode_number') - self.now_playing_summary = np_hash.get('summary') - self.now_playing_image_url = np_hash.get('image_url') + self.now_playing_title = np_hash.get("title") + self.now_playing_episode_title = np_hash.get("episode_title") + self.now_playing_season_number = np_hash.get("season_number") + self.now_playing_episode_number = np_hash.get("episode_number") + self.now_playing_summary = np_hash.get("summary") + self.now_playing_image_url = np_hash.get("image_url") else: self.now_playing_title = None self.now_playing_episode_title = None @@ -162,13 +192,13 @@ class ChannelsPlayer(MediaPlayerDevice): @property def state(self): """Return the state of the player.""" - if self.status == 'stopped': + if self.status == "stopped": return STATE_IDLE - if self.status == 'paused': + if self.status == "paused": return STATE_PAUSED - if self.status == 'playing': + if self.status == "playing": return STATE_PLAYING return None @@ -181,7 +211,7 @@ class ChannelsPlayer(MediaPlayerDevice): @property def source_list(self): """List of favorite channels.""" - sources = [channel['name'] for channel in self.favorite_channels] + sources = [channel["name"] for channel in self.favorite_channels] return sources @property @@ -207,7 +237,7 @@ class ChannelsPlayer(MediaPlayerDevice): if self.channel_image_url: return self.channel_image_url - return 'https://getchannels.com/assets/img/icon-1024.png' + return "https://getchannels.com/assets/img/icon-1024.png" @property def media_title(self): @@ -267,8 +297,7 @@ class ChannelsPlayer(MediaPlayerDevice): if media_type == MEDIA_TYPE_CHANNEL: response = self.client.play_channel(media_id) self.update_state(response) - elif media_type in [MEDIA_TYPE_MOVIE, MEDIA_TYPE_EPISODE, - MEDIA_TYPE_TVSHOW]: + elif media_type in [MEDIA_TYPE_MOVIE, MEDIA_TYPE_EPISODE, MEDIA_TYPE_TVSHOW]: response = self.client.play_recording(media_id) self.update_state(response) diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index 5eb03970989..b442b24feb4 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -5,19 +5,23 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \ - CONF_PORT + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, default=''): cv.string, - vol.Optional(CONF_PORT): cv.port, - }) + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=""): cv.string, + vol.Optional(CONF_PORT): cv.port, + } + ) ) @@ -41,7 +45,7 @@ class CiscoDeviceScanner(DeviceScanner): self.last_results = {} self.success_init = self._update_info() - _LOGGER.info('cisco_ios scanner initialized') + _LOGGER.info("cisco_ios scanner initialized") def get_device_name(self, device): """Get the firmware doesn't save the name of the wireless device.""" @@ -101,15 +105,20 @@ class CiscoDeviceScanner(DeviceScanner): try: cisco_ssh = pxssh.pxssh() - cisco_ssh.login(self.host, self.username, self.password, - port=self.port, auto_prompt_reset=False) + cisco_ssh.login( + self.host, + self.username, + self.password, + port=self.port, + auto_prompt_reset=False, + ) # Find the hostname - initial_line = cisco_ssh.before.decode('utf-8').splitlines() + initial_line = cisco_ssh.before.decode("utf-8").splitlines() router_hostname = initial_line[len(initial_line) - 1] router_hostname += "#" # Set the discovered hostname as prompt - regex_expression = ('(?i)^%s' % router_hostname).encode() + regex_expression = ("(?i)^%s" % router_hostname).encode() cisco_ssh.PROMPT = re.compile(regex_expression, re.MULTILINE) # Allow full arp table to print at once cisco_ssh.sendline("terminal length 0") @@ -120,7 +129,7 @@ class CiscoDeviceScanner(DeviceScanner): devices_result = cisco_ssh.before - return devices_result.decode('utf-8') + return devices_result.decode("utf-8") except pxssh.ExceptionPxssh as px_e: _LOGGER.error("pxssh failed on login") _LOGGER.error(px_e) @@ -141,8 +150,9 @@ def _parse_cisco_mac_address(cisco_hardware_addr): Takes in cisco_hwaddr: HWAddr String from Cisco ARP table Returns a regular standard MAC address """ - cisco_hardware_addr = cisco_hardware_addr.replace('.', '') - blocks = [cisco_hardware_addr[x:x + 2] - for x in range(0, len(cisco_hardware_addr), 2)] + cisco_hardware_addr = cisco_hardware_addr.replace(".", "") + blocks = [ + cisco_hardware_addr[x : x + 2] for x in range(0, len(cisco_hardware_addr), 2) + ] - return ':'.join(blocks).upper() + return ":".join(blocks).upper() diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index 4af94588d3b..ca24fcb5c52 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -5,27 +5,38 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) from homeassistant.const import ( - CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SSL, CONF_VERIFY_SSL) + CONF_HOST, + CONF_USERNAME, + CONF_PASSWORD, + CONF_SSL, + CONF_VERIFY_SSL, +) _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + } +) def get_scanner(hass, config): """Validate the configuration and return a Cisco ME scanner.""" from ciscomobilityexpress.ciscome import CiscoMobilityExpress + config = config[DOMAIN] controller = CiscoMobilityExpress( @@ -33,7 +44,8 @@ def get_scanner(hass, config): config[CONF_USERNAME], config[CONF_PASSWORD], config.get(CONF_SSL), - config.get(CONF_VERIFY_SSL)) + config.get(CONF_VERIFY_SSL), + ) if not controller.is_logged_in(): return None return CiscoMEDeviceScanner(controller) @@ -55,9 +67,10 @@ class CiscoMEDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - name = next(( - result.clId for result in self.last_results - if result.macaddr == device), None) + name = next( + (result.clId for result in self.last_results if result.macaddr == device), + None, + ) return name def get_extra_attributes(self, device): @@ -67,13 +80,14 @@ class CiscoMEDeviceScanner(DeviceScanner): Some known extra attributes that may be returned in the device tuple include SSID, PT (eg 802.11ac), devtype (eg iPhone 7) among others. """ - device = next(( - result for result in self.last_results - if result.macaddr == device), None) + device = next( + (result for result in self.last_results if result.macaddr == device), None + ) return device._asdict() def _update_info(self): """Check the Cisco ME controller for devices.""" self.last_results = self.controller.get_associated_devices() - _LOGGER.debug("Cisco Mobility Express controller returned:" - " %s", self.last_results) + _LOGGER.debug( + "Cisco Mobility Express controller returned:" " %s", self.last_results + ) diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index d1b4687c2cd..1d80076793d 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -3,7 +3,7 @@ "name": "Cisco mobility express", "documentation": "https://www.home-assistant.io/components/cisco_mobility_express", "requirements": [ - "ciscomobilityexpress==0.1.5" + "ciscomobilityexpress==0.3.1" ], "dependencies": [], "codeowners": ["@fbradyirl"] diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index 22f8679f618..9feac3207ad 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -4,23 +4,26 @@ import logging import voluptuous as vol from homeassistant.components.notify import ( - PLATFORM_SCHEMA, BaseNotificationService, ATTR_TITLE) -from homeassistant.const import (CONF_TOKEN) + PLATFORM_SCHEMA, + BaseNotificationService, + ATTR_TITLE, +) +from homeassistant.const import CONF_TOKEN import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_ROOM_ID = 'room_id' +CONF_ROOM_ID = "room_id" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TOKEN): cv.string, - vol.Required(CONF_ROOM_ID): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_ROOM_ID): cv.string} +) def get_service(hass, config, discovery_info=None): """Get the CiscoWebexTeams notification service.""" from webexteamssdk import WebexTeamsAPI, exceptions + client = WebexTeamsAPI(access_token=config[CONF_TOKEN]) try: # Validate the token & room_id @@ -29,9 +32,7 @@ def get_service(hass, config, discovery_info=None): _LOGGER.error(error) return None - return CiscoWebexTeamsNotificationService( - client, - config[CONF_ROOM_ID]) + return CiscoWebexTeamsNotificationService(client, config[CONF_ROOM_ID]) class CiscoWebexTeamsNotificationService(BaseNotificationService): @@ -45,14 +46,16 @@ class CiscoWebexTeamsNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" from webexteamssdk import ApiError + title = "" if kwargs.get(ATTR_TITLE) is not None: title = "{}{}".format(kwargs.get(ATTR_TITLE), "
") try: - self.client.messages.create(roomId=self.room, - html="{}{}".format(title, message)) + self.client.messages.create( + roomId=self.room, html="{}{}".format(title, message) + ) except ApiError as api_error: - _LOGGER.error("Could not send CiscoWebexTeams notification. " - "Error: %s", - api_error) + _LOGGER.error( + "Could not send CiscoWebexTeams notification. " "Error: %s", api_error + ) diff --git a/homeassistant/components/ciscospark/notify.py b/homeassistant/components/ciscospark/notify.py index 320c342b143..67609766366 100644 --- a/homeassistant/components/ciscospark/notify.py +++ b/homeassistant/components/ciscospark/notify.py @@ -6,24 +6,26 @@ import voluptuous as vol from homeassistant.const import CONF_TOKEN import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (ATTR_TITLE, PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import ( + ATTR_TITLE, + PLATFORM_SCHEMA, + BaseNotificationService, +) _LOGGER = logging.getLogger(__name__) -CONF_ROOMID = 'roomid' +CONF_ROOMID = "roomid" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TOKEN): cv.string, - vol.Required(CONF_ROOMID): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_ROOMID): cv.string} +) def get_service(hass, config, discovery_info=None): """Get the CiscoSpark notification service.""" return CiscoSparkNotificationService( - config.get(CONF_TOKEN), - config.get(CONF_ROOMID)) + config.get(CONF_TOKEN), config.get(CONF_ROOMID) + ) class CiscoSparkNotificationService(BaseNotificationService): @@ -32,6 +34,7 @@ class CiscoSparkNotificationService(BaseNotificationService): def __init__(self, token, default_room): """Initialize the service.""" from ciscosparkapi import CiscoSparkAPI + self._default_room = default_room self._token = token self._spark = CiscoSparkAPI(access_token=self._token) @@ -39,12 +42,13 @@ class CiscoSparkNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" from ciscosparkapi import SparkApiError + try: title = "" if kwargs.get(ATTR_TITLE) is not None: title = kwargs.get(ATTR_TITLE) + ": " - self._spark.messages.create(roomId=self._default_room, - text=title + message) + self._spark.messages.create(roomId=self._default_room, text=title + message) except SparkApiError as api_error: - _LOGGER.error("Could not send CiscoSpark notification. Error: %s", - api_error) + _LOGGER.error( + "Could not send CiscoSpark notification. Error: %s", api_error + ) diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index fc751d96602..cb2647487ea 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -9,9 +9,19 @@ import voluptuous as vol from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_ID, ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, - ATTR_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS, - LENGTH_FEET, LENGTH_METERS) + ATTR_ATTRIBUTION, + ATTR_ID, + ATTR_LATITUDE, + ATTR_LOCATION, + ATTR_LONGITUDE, + ATTR_NAME, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_RADIUS, + LENGTH_FEET, + LENGTH_METERS, +) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -21,78 +31,95 @@ from homeassistant.util import distance, location _LOGGER = logging.getLogger(__name__) -ATTR_EMPTY_SLOTS = 'empty_slots' -ATTR_EXTRA = 'extra' -ATTR_FREE_BIKES = 'free_bikes' -ATTR_NETWORK = 'network' -ATTR_NETWORKS_LIST = 'networks' -ATTR_STATIONS_LIST = 'stations' -ATTR_TIMESTAMP = 'timestamp' -ATTR_UID = 'uid' +ATTR_EMPTY_SLOTS = "empty_slots" +ATTR_EXTRA = "extra" +ATTR_FREE_BIKES = "free_bikes" +ATTR_NETWORK = "network" +ATTR_NETWORKS_LIST = "networks" +ATTR_STATIONS_LIST = "stations" +ATTR_TIMESTAMP = "timestamp" +ATTR_UID = "uid" -CONF_NETWORK = 'network' -CONF_STATIONS_LIST = 'stations' +CONF_NETWORK = "network" +CONF_STATIONS_LIST = "stations" -DEFAULT_ENDPOINT = 'https://api.citybik.es/{uri}' -PLATFORM = 'citybikes' +DEFAULT_ENDPOINT = "https://api.citybik.es/{uri}" +PLATFORM = "citybikes" -MONITORED_NETWORKS = 'monitored-networks' +MONITORED_NETWORKS = "monitored-networks" -NETWORKS_URI = 'v2/networks' +NETWORKS_URI = "v2/networks" REQUEST_TIMEOUT = 5 # In seconds; argument to asyncio.timeout SCAN_INTERVAL = timedelta(minutes=5) # Timely, and doesn't suffocate the API -STATIONS_URI = 'v2/networks/{uid}?fields=network.stations' +STATIONS_URI = "v2/networks/{uid}?fields=network.stations" -CITYBIKES_ATTRIBUTION = "Information provided by the CityBikes Project "\ - "(https://citybik.es/#about)" +CITYBIKES_ATTRIBUTION = ( + "Information provided by the CityBikes Project " "(https://citybik.es/#about)" +) -CITYBIKES_NETWORKS = 'citybikes_networks' +CITYBIKES_NETWORKS = "citybikes_networks" PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_RADIUS, CONF_STATIONS_LIST), - PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=''): cv.string, - vol.Optional(CONF_NETWORK): cv.string, - vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude, - vol.Optional(CONF_RADIUS, 'station_filter'): cv.positive_int, - vol.Optional(CONF_STATIONS_LIST, 'station_filter'): - vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]) - })) + PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=""): cv.string, + vol.Optional(CONF_NETWORK): cv.string, + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Optional(CONF_RADIUS, "station_filter"): cv.positive_int, + vol.Optional(CONF_STATIONS_LIST, "station_filter"): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ), + } + ), +) -NETWORK_SCHEMA = vol.Schema({ - vol.Required(ATTR_ID): cv.string, - vol.Required(ATTR_NAME): cv.string, - vol.Required(ATTR_LOCATION): vol.Schema({ +NETWORK_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ID): cv.string, + vol.Required(ATTR_NAME): cv.string, + vol.Required(ATTR_LOCATION): vol.Schema( + { + vol.Required(ATTR_LATITUDE): cv.latitude, + vol.Required(ATTR_LONGITUDE): cv.longitude, + }, + extra=vol.REMOVE_EXTRA, + ), + }, + extra=vol.REMOVE_EXTRA, +) + +NETWORKS_RESPONSE_SCHEMA = vol.Schema( + {vol.Required(ATTR_NETWORKS_LIST): [NETWORK_SCHEMA]} +) + +STATION_SCHEMA = vol.Schema( + { + vol.Required(ATTR_FREE_BIKES): cv.positive_int, + vol.Required(ATTR_EMPTY_SLOTS): vol.Any(cv.positive_int, None), vol.Required(ATTR_LATITUDE): cv.latitude, vol.Required(ATTR_LONGITUDE): cv.longitude, - }, extra=vol.REMOVE_EXTRA), -}, extra=vol.REMOVE_EXTRA) + vol.Required(ATTR_ID): cv.string, + vol.Required(ATTR_NAME): cv.string, + vol.Required(ATTR_TIMESTAMP): cv.string, + vol.Optional(ATTR_EXTRA): vol.Schema( + {vol.Optional(ATTR_UID): cv.string}, extra=vol.REMOVE_EXTRA + ), + }, + extra=vol.REMOVE_EXTRA, +) -NETWORKS_RESPONSE_SCHEMA = vol.Schema({ - vol.Required(ATTR_NETWORKS_LIST): [NETWORK_SCHEMA], -}) - -STATION_SCHEMA = vol.Schema({ - vol.Required(ATTR_FREE_BIKES): cv.positive_int, - vol.Required(ATTR_EMPTY_SLOTS): vol.Any(cv.positive_int, None), - vol.Required(ATTR_LATITUDE): cv.latitude, - vol.Required(ATTR_LONGITUDE): cv.longitude, - vol.Required(ATTR_ID): cv.string, - vol.Required(ATTR_NAME): cv.string, - vol.Required(ATTR_TIMESTAMP): cv.string, - vol.Optional(ATTR_EXTRA): - vol.Schema({vol.Optional(ATTR_UID): cv.string}, extra=vol.REMOVE_EXTRA) -}, extra=vol.REMOVE_EXTRA) - -STATIONS_RESPONSE_SCHEMA = vol.Schema({ - vol.Required(ATTR_NETWORK): vol.Schema({ - vol.Required(ATTR_STATIONS_LIST): [STATION_SCHEMA] - }, extra=vol.REMOVE_EXTRA) -}) +STATIONS_RESPONSE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_NETWORK): vol.Schema( + {vol.Required(ATTR_STATIONS_LIST): [STATION_SCHEMA]}, extra=vol.REMOVE_EXTRA + ) + } +) class CityBikesRequestError(Exception): @@ -116,13 +143,13 @@ async def async_citybikes_request(hass, uri, schema): except ValueError: _LOGGER.error("Received non-JSON data from CityBikes API endpoint") except vol.Invalid as err: - _LOGGER.error("Received unexpected JSON from CityBikes" - " API endpoint: %s", err) + _LOGGER.error( + "Received unexpected JSON from CityBikes" " API endpoint: %s", err + ) raise CityBikesRequestError -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the CityBikes platform.""" if PLATFORM not in hass.data: hass.data[PLATFORM] = {MONITORED_NETWORKS: {}} @@ -137,8 +164,7 @@ async def async_setup_platform(hass, config, async_add_entities, radius = distance.convert(radius, LENGTH_FEET, LENGTH_METERS) # Create a single instance of CityBikesNetworks. - networks = hass.data.setdefault( - CITYBIKES_NETWORKS, CityBikesNetworks(hass)) + networks = hass.data.setdefault(CITYBIKES_NETWORKS, CityBikesNetworks(hass)) if not network_id: network_id = await networks.get_closest_network_id(latitude, longitude) @@ -156,19 +182,17 @@ async def async_setup_platform(hass, config, async_add_entities, devices = [] for station in network.stations: dist = location.distance( - latitude, longitude, station[ATTR_LATITUDE], - station[ATTR_LONGITUDE]) + latitude, longitude, station[ATTR_LATITUDE], station[ATTR_LONGITUDE] + ) station_id = station[ATTR_ID] - station_uid = str(station.get(ATTR_EXTRA, {}).get(ATTR_UID, '')) + station_uid = str(station.get(ATTR_EXTRA, {}).get(ATTR_UID, "")) - if radius > dist or stations_list.intersection( - (station_id, station_uid)): + if radius > dist or stations_list.intersection((station_id, station_uid)): if name: uid = "_".join([network.network_id, name, station_id]) else: uid = "_".join([network.network_id, station_id]) - entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, uid, hass=hass) + entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, uid, hass=hass) devices.append(CityBikesStation(network, station_id, entity_id)) async_add_entities(devices, True) @@ -189,7 +213,8 @@ class CityBikesNetworks: await self.networks_loading.acquire() if self.networks is None: networks = await async_citybikes_request( - self.hass, NETWORKS_URI, NETWORKS_RESPONSE_SCHEMA) + self.hass, NETWORKS_URI, NETWORKS_RESPONSE_SCHEMA + ) self.networks = networks[ATTR_NETWORKS_LIST] result = None minimum_dist = None @@ -197,7 +222,8 @@ class CityBikesNetworks: network_latitude = network[ATTR_LOCATION][ATTR_LATITUDE] network_longitude = network[ATTR_LOCATION][ATTR_LONGITUDE] dist = location.distance( - latitude, longitude, network_latitude, network_longitude) + latitude, longitude, network_latitude, network_longitude + ) if minimum_dist is None or dist < minimum_dist: minimum_dist = dist result = network[ATTR_ID] @@ -223,8 +249,10 @@ class CityBikesNetwork: """Refresh the state of the network.""" try: network = await async_citybikes_request( - self.hass, STATIONS_URI.format(uid=self.network_id), - STATIONS_RESPONSE_SCHEMA) + self.hass, + STATIONS_URI.format(uid=self.network_id), + STATIONS_RESPONSE_SCHEMA, + ) self.stations = network[ATTR_NETWORK][ATTR_STATIONS_LIST] self.ready.set() except CityBikesRequestError: @@ -278,9 +306,9 @@ class CityBikesStation(Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return 'bikes' + return "bikes" @property def icon(self): """Return the icon.""" - return 'mdi:bike' + return "mdi:bike" diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index fc6e27be1bd..37ed97915c7 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -5,40 +5,59 @@ import time import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, - SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) + MEDIA_TYPE_MUSIC, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) from homeassistant.const import ( - CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, - STATE_PAUSED, STATE_PLAYING) + CONF_ACCESS_TOKEN, + CONF_HOST, + CONF_NAME, + CONF_PORT, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Clementine Remote' +DEFAULT_NAME = "Clementine Remote" DEFAULT_PORT = 5500 SCAN_INTERVAL = timedelta(seconds=5) -SUPPORT_CLEMENTINE = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_VOLUME_SET | \ - SUPPORT_NEXT_TRACK | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY +SUPPORT_CLEMENTINE = ( + SUPPORT_PAUSE + | SUPPORT_VOLUME_STEP + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_VOLUME_SET + | SUPPORT_NEXT_TRACK + | SUPPORT_SELECT_SOURCE + | SUPPORT_PLAY +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_ACCESS_TOKEN): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_ACCESS_TOKEN): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Clementine platform.""" from clementineremote import ClementineRemote + host = config.get(CONF_HOST) port = config.get(CONF_PORT) token = config.get(CONF_ACCESS_TOKEN) @@ -59,9 +78,9 @@ class ClementineDevice(MediaPlayerDevice): self._volume = 0.0 self._track_id = 0 self._last_track_id = 0 - self._track_name = '' - self._track_artist = '' - self._track_album_name = '' + self._track_name = "" + self._track_artist = "" + self._track_album_name = "" self._state = None def update(self): @@ -69,11 +88,11 @@ class ClementineDevice(MediaPlayerDevice): try: client = self._client - if client.state == 'Playing': + if client.state == "Playing": self._state = STATE_PLAYING - elif client.state == 'Paused': + elif client.state == "Paused": self._state = STATE_PAUSED - elif client.state == 'Disconnected': + elif client.state == "Disconnected": self._state = STATE_OFF else: self._state = STATE_PAUSED @@ -84,10 +103,10 @@ class ClementineDevice(MediaPlayerDevice): self._volume = float(client.volume) if client.volume else 0.0 if client.current_track: - self._track_id = client.current_track['track_id'] - self._track_name = client.current_track['title'] - self._track_artist = client.current_track['track_artist'] - self._track_album_name = client.current_track['track_album'] + self._track_id = client.current_track["track_id"] + self._track_name = client.current_track["title"] + self._track_artist = client.current_track["track_artist"] + self._track_album_name = client.current_track["track_album"] except Exception: self._state = STATE_OFF @@ -114,7 +133,7 @@ class ClementineDevice(MediaPlayerDevice): source_name = "Unknown" client = self._client if client.active_playlist_id in client.playlists: - source_name = client.playlists[client.active_playlist_id]['name'] + source_name = client.playlists[client.active_playlist_id]["name"] return source_name @property @@ -126,9 +145,9 @@ class ClementineDevice(MediaPlayerDevice): def select_source(self, source): """Select input source.""" client = self._client - sources = [s for s in client.playlists.values() if s['name'] == source] + sources = [s for s in client.playlists.values() if s["name"] == source] if len(sources) == 1: - client.change_song(sources[0]['id'], 0) + client.change_song(sources[0]["id"], 0) @property def media_content_type(self): @@ -159,15 +178,15 @@ class ClementineDevice(MediaPlayerDevice): def media_image_hash(self): """Hash value for media image.""" if self._client.current_track: - return self._client.current_track['track_id'] + return self._client.current_track["track_id"] return None async def async_get_media_image(self): """Fetch media image of current playing image.""" if self._client.current_track: - image = bytes(self._client.current_track['art']) - return (image, 'image/png') + image = bytes(self._client.current_track["art"]) + return (image, "image/png") return None, None diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py index b512a288ed5..26f2f30aeff 100644 --- a/homeassistant/components/clickatell/notify.py +++ b/homeassistant/components/clickatell/notify.py @@ -7,19 +7,17 @@ import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'clickatell' +DEFAULT_NAME = "clickatell" -BASE_API_URL = 'https://platform.clickatell.com/messages/http/send' +BASE_API_URL = "https://platform.clickatell.com/messages/http/send" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_RECIPIENT): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_RECIPIENT): cv.string} +) def get_service(hass, config, discovery_info=None): @@ -37,11 +35,7 @@ class ClickatellNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - data = { - 'apiKey': self.api_key, - 'to': self.recipient, - 'content': message, - } + data = {"apiKey": self.api_key, "to": self.recipient, "content": message} resp = requests.get(BASE_API_URL, params=data, timeout=5) if (resp.status_code != 200) or (resp.status_code != 201): diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 111ae63601f..1ec828b4a28 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -7,30 +7,39 @@ import requests import voluptuous as vol from homeassistant.const import ( - CONF_API_KEY, CONF_RECIPIENT, CONF_SENDER, CONF_USERNAME, - CONTENT_TYPE_JSON) + CONF_API_KEY, + CONF_RECIPIENT, + CONF_SENDER, + CONF_USERNAME, + CONTENT_TYPE_JSON, +) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService _LOGGER = logging.getLogger(__name__) -BASE_API_URL = 'https://rest.clicksend.com/v3' -DEFAULT_SENDER = 'hass' +BASE_API_URL = "https://rest.clicksend.com/v3" +DEFAULT_SENDER = "hass" TIMEOUT = 5 HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} PLATFORM_SCHEMA = vol.Schema( - vol.All(PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_RECIPIENT, default=[]): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_SENDER, default=DEFAULT_SENDER): cv.string, - }),)) + vol.All( + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_RECIPIENT, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_SENDER, default=DEFAULT_SENDER): cv.string, + } + ) + ) +) def get_service(hass, config, discovery_info=None): @@ -55,37 +64,43 @@ class ClicksendNotificationService(BaseNotificationService): """Send a message to a user.""" data = {"messages": []} for recipient in self.recipients: - data["messages"].append({ - 'source': 'hass.notify', - 'from': self.sender, - 'to': recipient, - 'body': message, - }) + data["messages"].append( + { + "source": "hass.notify", + "from": self.sender, + "to": recipient, + "body": message, + } + ) api_url = "{}/sms/send".format(BASE_API_URL) - resp = requests.post(api_url, - data=json.dumps(data), - headers=HEADERS, - auth=(self.username, self.api_key), - timeout=TIMEOUT) + resp = requests.post( + api_url, + data=json.dumps(data), + headers=HEADERS, + auth=(self.username, self.api_key), + timeout=TIMEOUT, + ) if resp.status_code == 200: return obj = json.loads(resp.text) - response_msg = obj.get('response_msg') - response_code = obj.get('response_code') - _LOGGER.error("Error %s : %s (Code %s)", resp.status_code, - response_msg, response_code) + response_msg = obj.get("response_msg") + response_code = obj.get("response_code") + _LOGGER.error( + "Error %s : %s (Code %s)", resp.status_code, response_msg, response_code + ) def _authenticate(config): """Authenticate with ClickSend.""" - api_url = '{}/account'.format(BASE_API_URL) - resp = requests.get(api_url, - headers=HEADERS, - auth=(config[CONF_USERNAME], - config[CONF_API_KEY]), - timeout=TIMEOUT) + api_url = "{}/account".format(BASE_API_URL) + resp = requests.get( + api_url, + headers=HEADERS, + auth=(config[CONF_USERNAME], config[CONF_API_KEY]), + timeout=TIMEOUT, + ) if resp.status_code != 200: return False return True diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index feb4481fb56..7c73c346a33 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -7,34 +7,39 @@ import requests import voluptuous as vol from homeassistant.const import ( - CONF_API_KEY, CONF_RECIPIENT, CONF_USERNAME, CONTENT_TYPE_JSON) + CONF_API_KEY, + CONF_RECIPIENT, + CONF_USERNAME, + CONTENT_TYPE_JSON, +) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService _LOGGER = logging.getLogger(__name__) -BASE_API_URL = 'https://rest.clicksend.com/v3' +BASE_API_URL = "https://rest.clicksend.com/v3" HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} -CONF_LANGUAGE = 'language' -CONF_VOICE = 'voice' -CONF_CALLER = 'caller' +CONF_LANGUAGE = "language" +CONF_VOICE = "voice" +CONF_CALLER = "caller" -DEFAULT_LANGUAGE = 'en-us' -DEFAULT_VOICE = 'female' +DEFAULT_LANGUAGE = "en-us" +DEFAULT_VOICE = "female" TIMEOUT = 5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_RECIPIENT): cv.string, - vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): cv.string, - vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): cv.string, - vol.Optional(CONF_CALLER): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_RECIPIENT): cv.string, + vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): cv.string, + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): cv.string, + vol.Optional(CONF_CALLER): cv.string, + } +) def get_service(hass, config, discovery_info=None): @@ -62,31 +67,46 @@ class ClicksendNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a voice call to a user.""" - data = ({'messages': [{'source': 'hass.notify', 'from': self.caller, - 'to': self.recipient, 'body': message, - 'lang': self.language, 'voice': self.voice}]}) + data = { + "messages": [ + { + "source": "hass.notify", + "from": self.caller, + "to": self.recipient, + "body": message, + "lang": self.language, + "voice": self.voice, + } + ] + } api_url = "{}/voice/send".format(BASE_API_URL) - resp = requests.post(api_url, - data=json.dumps(data), - headers=HEADERS, - auth=(self.username, self.api_key), - timeout=TIMEOUT) + resp = requests.post( + api_url, + data=json.dumps(data), + headers=HEADERS, + auth=(self.username, self.api_key), + timeout=TIMEOUT, + ) if resp.status_code == 200: return obj = json.loads(resp.text) - response_msg = obj['response_msg'] - response_code = obj['response_code'] - _LOGGER.error("Error %s : %s (Code %s)", resp.status_code, - response_msg, response_code) + response_msg = obj["response_msg"] + response_code = obj["response_code"] + _LOGGER.error( + "Error %s : %s (Code %s)", resp.status_code, response_msg, response_code + ) def _authenticate(config): """Authenticate with ClickSend.""" - api_url = '{}/account'.format(BASE_API_URL) - resp = requests.get(api_url, headers=HEADERS, - auth=(config.get(CONF_USERNAME), - config.get(CONF_API_KEY)), timeout=TIMEOUT) + api_url = "{}/account".format(BASE_API_URL) + resp = requests.get( + api_url, + headers=HEADERS, + auth=(config.get(CONF_USERNAME), config.get(CONF_API_KEY)), + timeout=TIMEOUT, + ) if resp.status_code != 200: return False diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 347cb275e42..6c64d667254 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -7,31 +7,69 @@ from typing import Any, Dict, List, Optional import voluptuous as vol from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, PRECISION_TENTHS, PRECISION_WHOLE, - SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON, TEMP_CELSIUS) + ATTR_TEMPERATURE, + PRECISION_TENTHS, + PRECISION_WHOLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa - PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) + ENTITY_SERVICE_SCHEMA, + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.helpers.typing import ( - ConfigType, HomeAssistantType, ServiceDataType) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.util.temperature import convert as convert_temperature from .const import ( - ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, - ATTR_FAN_MODE, ATTR_FAN_MODES, ATTR_HUMIDITY, ATTR_HVAC_ACTIONS, - ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP, - ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_PRESET_MODES, - ATTR_SWING_MODE, ATTR_SWING_MODES, ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_STEP, DOMAIN, HVAC_MODE_COOL, - HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, HVAC_MODES, - SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, - SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, - SERVICE_SET_TEMPERATURE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY, - SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_TARGET_TEMPERATURE) + ATTR_AUX_HEAT, + ATTR_CURRENT_HUMIDITY, + ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, + ATTR_FAN_MODES, + ATTR_HUMIDITY, + ATTR_HVAC_ACTIONS, + ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_MAX_HUMIDITY, + ATTR_MAX_TEMP, + ATTR_MIN_HUMIDITY, + ATTR_MIN_TEMP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + ATTR_SWING_MODE, + ATTR_SWING_MODES, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_STEP, + DOMAIN, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + HVAC_MODES, + SERVICE_SET_AUX_HEAT, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HUMIDITY, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, + SUPPORT_AUX_HEAT, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE_RANGE, + SUPPORT_TARGET_TEMPERATURE, +) from .reproduce_state import async_reproduce_states # noqa DEFAULT_MIN_TEMP = 7 @@ -39,98 +77,81 @@ DEFAULT_MAX_TEMP = 35 DEFAULT_MIN_HUMIDITY = 30 DEFAULT_MAX_HUMIDITY = 99 -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = timedelta(seconds=60) -CONVERTIBLE_ATTRIBUTE = [ - ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_HIGH, -] +CONVERTIBLE_ATTRIBUTE = [ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH] _LOGGER = logging.getLogger(__name__) -TURN_ON_OFF_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, -}) -SET_AUX_HEAT_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_AUX_HEAT): cv.boolean, -}) -SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All( - cv.has_at_least_one_key( - ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW), - { - vol.Exclusive(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float), - vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float), - vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float), - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Optional(ATTR_HVAC_MODE): vol.In(HVAC_MODES), - } -)) -SET_FAN_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_FAN_MODE): cv.string, -}) -SET_PRESET_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_PRESET_MODE): vol.Maybe(cv.string), -}) -SET_HVAC_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_HVAC_MODE): vol.In(HVAC_MODES), -}) -SET_HUMIDITY_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_HUMIDITY): vol.Coerce(float), -}) -SET_SWING_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_SWING_MODE): cv.string, -}) +SET_AUX_HEAT_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_AUX_HEAT): cv.boolean} +) +SET_TEMPERATURE_SCHEMA = vol.Schema( + vol.All( + cv.has_at_least_one_key( + ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW + ), + ENTITY_SERVICE_SCHEMA.extend( + { + vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_HIGH, "temperature"): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_LOW, "temperature"): vol.Coerce(float), + vol.Optional(ATTR_HVAC_MODE): vol.In(HVAC_MODES), + } + ), + ) +) +SET_FAN_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_FAN_MODE): cv.string} +) +SET_PRESET_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_PRESET_MODE): cv.string} +) +SET_HVAC_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_HVAC_MODE): vol.In(HVAC_MODES)} +) +SET_HUMIDITY_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_HUMIDITY): vol.Coerce(float)} +) +SET_SWING_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_SWING_MODE): cv.string} +) async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up climate devices.""" - component = hass.data[DOMAIN] = \ - EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + component = hass.data[DOMAIN] = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) await component.async_setup(config) component.async_register_entity_service( - SERVICE_TURN_ON, TURN_ON_OFF_SCHEMA, - 'async_turn_on' + SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" ) component.async_register_entity_service( - SERVICE_TURN_OFF, TURN_ON_OFF_SCHEMA, - 'async_turn_off' + SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" ) component.async_register_entity_service( - SERVICE_SET_HVAC_MODE, SET_HVAC_MODE_SCHEMA, - 'async_set_hvac_mode' + SERVICE_SET_HVAC_MODE, SET_HVAC_MODE_SCHEMA, "async_set_hvac_mode" ) component.async_register_entity_service( - SERVICE_SET_PRESET_MODE, SET_PRESET_MODE_SCHEMA, - 'async_set_preset_mode' + SERVICE_SET_PRESET_MODE, SET_PRESET_MODE_SCHEMA, "async_set_preset_mode" ) component.async_register_entity_service( - SERVICE_SET_AUX_HEAT, SET_AUX_HEAT_SCHEMA, - async_service_aux_heat + SERVICE_SET_AUX_HEAT, SET_AUX_HEAT_SCHEMA, async_service_aux_heat ) component.async_register_entity_service( - SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, - async_service_temperature_set + SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, async_service_temperature_set ) component.async_register_entity_service( - SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, - 'async_set_humidity' + SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, "async_set_humidity" ) component.async_register_entity_service( - SERVICE_SET_FAN_MODE, SET_FAN_MODE_SCHEMA, - 'async_set_fan_mode' + SERVICE_SET_FAN_MODE, SET_FAN_MODE_SCHEMA, "async_set_fan_mode" ) component.async_register_entity_service( - SERVICE_SET_SWING_MODE, SET_SWING_MODE_SCHEMA, - 'async_set_swing_mode' + SERVICE_SET_SWING_MODE, SET_SWING_MODE_SCHEMA, "async_set_swing_mode" ) return True @@ -168,14 +189,17 @@ class ClimateDevice(Entity): data = { ATTR_HVAC_MODES: self.hvac_modes, ATTR_CURRENT_TEMPERATURE: show_temp( - self.hass, self.current_temperature, self.temperature_unit, - self.precision), + self.hass, + self.current_temperature, + self.temperature_unit, + self.precision, + ), ATTR_MIN_TEMP: show_temp( - self.hass, self.min_temp, self.temperature_unit, - self.precision), + self.hass, self.min_temp, self.temperature_unit, self.precision + ), ATTR_MAX_TEMP: show_temp( - self.hass, self.max_temp, self.temperature_unit, - self.precision), + self.hass, self.max_temp, self.temperature_unit, self.precision + ), } if self.target_temperature_step: @@ -183,16 +207,25 @@ class ClimateDevice(Entity): if supported_features & SUPPORT_TARGET_TEMPERATURE: data[ATTR_TEMPERATURE] = show_temp( - self.hass, self.target_temperature, self.temperature_unit, - self.precision) + self.hass, + self.target_temperature, + self.temperature_unit, + self.precision, + ) if supported_features & SUPPORT_TARGET_TEMPERATURE_RANGE: data[ATTR_TARGET_TEMP_HIGH] = show_temp( - self.hass, self.target_temperature_high, self.temperature_unit, - self.precision) + self.hass, + self.target_temperature_high, + self.temperature_unit, + self.precision, + ) data[ATTR_TARGET_TEMP_LOW] = show_temp( - self.hass, self.target_temperature_low, self.temperature_unit, - self.precision) + self.hass, + self.target_temperature_low, + self.temperature_unit, + self.precision, + ) if self.current_humidity is not None: data[ATTR_CURRENT_HUMIDITY] = self.current_humidity @@ -355,7 +388,8 @@ class ClimateDevice(Entity): async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" await self.hass.async_add_executor_job( - ft.partial(self.set_temperature, **kwargs)) + ft.partial(self.set_temperature, **kwargs) + ) def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" @@ -395,8 +429,7 @@ class ClimateDevice(Entity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - await self.hass.async_add_executor_job( - self.set_preset_mode, preset_mode) + await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode) def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" @@ -416,7 +449,7 @@ class ClimateDevice(Entity): async def async_turn_on(self) -> None: """Turn the entity on.""" - if hasattr(self, 'turn_on'): + if hasattr(self, "turn_on"): # pylint: disable=no-member await self.hass.async_add_executor_job(self.turn_on) return @@ -430,7 +463,7 @@ class ClimateDevice(Entity): async def async_turn_off(self) -> None: """Turn the entity off.""" - if hasattr(self, 'turn_off'): + if hasattr(self, "turn_off"): # pylint: disable=no-member await self.hass.async_add_executor_job(self.turn_off) return @@ -447,14 +480,16 @@ class ClimateDevice(Entity): @property def min_temp(self) -> float: """Return the minimum temperature.""" - return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS, - self.temperature_unit) + return convert_temperature( + DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit + ) @property def max_temp(self) -> float: """Return the maximum temperature.""" - return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS, - self.temperature_unit) + return convert_temperature( + DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit + ) @property def min_humidity(self) -> int: @@ -468,7 +503,7 @@ class ClimateDevice(Entity): async def async_service_aux_heat( - entity: ClimateDevice, service: ServiceDataType + entity: ClimateDevice, service: ServiceDataType ) -> None: """Handle aux heat service.""" if service.data[ATTR_AUX_HEAT]: @@ -478,7 +513,7 @@ async def async_service_aux_heat( async def async_service_temperature_set( - entity: ClimateDevice, service: ServiceDataType + entity: ClimateDevice, service: ServiceDataType ) -> None: """Handle set temperature service.""" hass = entity.hass @@ -487,9 +522,7 @@ async def async_service_temperature_set( for value, temp in service.data.items(): if value in CONVERTIBLE_ATTRIBUTE: kwargs[value] = convert_temperature( - temp, - hass.config.units.temperature_unit, - entity.temperature_unit + temp, hass.config.units.temperature_unit, entity.temperature_unit ) else: kwargs[value] = temp diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 13f8e3b616a..4012aa8be1b 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -1,26 +1,26 @@ """Provides the constants needed for component.""" # All activity disabled / Device is off/standby -HVAC_MODE_OFF = 'off' +HVAC_MODE_OFF = "off" # Heating -HVAC_MODE_HEAT = 'heat' +HVAC_MODE_HEAT = "heat" # Cooling -HVAC_MODE_COOL = 'cool' +HVAC_MODE_COOL = "cool" # The device supports heating/cooling to a range -HVAC_MODE_HEAT_COOL = 'heat_cool' +HVAC_MODE_HEAT_COOL = "heat_cool" # The temperature is set based on a schedule, learned behavior, AI or some # other related mechanism. User is not able to adjust the temperature -HVAC_MODE_AUTO = 'auto' +HVAC_MODE_AUTO = "auto" # Device is in Dry/Humidity mode -HVAC_MODE_DRY = 'dry' +HVAC_MODE_DRY = "dry" # Only the fan is on, not fan and another mode like cool -HVAC_MODE_FAN_ONLY = 'fan_only' +HVAC_MODE_FAN_ONLY = "fan_only" HVAC_MODES = [ HVAC_MODE_OFF, @@ -32,27 +32,29 @@ HVAC_MODES = [ HVAC_MODE_FAN_ONLY, ] +# No preset is active +PRESET_NONE = "none" # Device is running an energy-saving mode -PRESET_ECO = 'eco' +PRESET_ECO = "eco" # Device is in away mode -PRESET_AWAY = 'away' +PRESET_AWAY = "away" # Device turn all valve full up -PRESET_BOOST = 'boost' +PRESET_BOOST = "boost" # Device is in comfort mode -PRESET_COMFORT = 'comfort' +PRESET_COMFORT = "comfort" # Device is in home mode -PRESET_HOME = 'home' +PRESET_HOME = "home" # Device is prepared for sleep -PRESET_SLEEP = 'sleep' +PRESET_SLEEP = "sleep" # Device is reacting to activity (e.g. movement sensors) -PRESET_ACTIVITY = 'activity' +PRESET_ACTIVITY = "activity" # Possible fan state @@ -75,49 +77,49 @@ SWING_HORIZONTAL = "horizontal" # This are support current states of HVAC -CURRENT_HVAC_OFF = 'off' -CURRENT_HVAC_HEAT = 'heating' -CURRENT_HVAC_COOL = 'cooling' -CURRENT_HVAC_DRY = 'drying' -CURRENT_HVAC_IDLE = 'idle' -CURRENT_HVAC_FAN = 'fan' +CURRENT_HVAC_OFF = "off" +CURRENT_HVAC_HEAT = "heating" +CURRENT_HVAC_COOL = "cooling" +CURRENT_HVAC_DRY = "drying" +CURRENT_HVAC_IDLE = "idle" +CURRENT_HVAC_FAN = "fan" -ATTR_AUX_HEAT = 'aux_heat' -ATTR_CURRENT_HUMIDITY = 'current_humidity' -ATTR_CURRENT_TEMPERATURE = 'current_temperature' -ATTR_FAN_MODES = 'fan_modes' -ATTR_FAN_MODE = 'fan_mode' -ATTR_PRESET_MODE = 'preset_mode' -ATTR_PRESET_MODES = 'preset_modes' -ATTR_HUMIDITY = 'humidity' -ATTR_MAX_HUMIDITY = 'max_humidity' -ATTR_MIN_HUMIDITY = 'min_humidity' -ATTR_MAX_TEMP = 'max_temp' -ATTR_MIN_TEMP = 'min_temp' -ATTR_HVAC_ACTIONS = 'hvac_action' -ATTR_HVAC_MODES = 'hvac_modes' -ATTR_HVAC_MODE = 'hvac_mode' -ATTR_SWING_MODES = 'swing_modes' -ATTR_SWING_MODE = 'swing_mode' -ATTR_TARGET_TEMP_HIGH = 'target_temp_high' -ATTR_TARGET_TEMP_LOW = 'target_temp_low' -ATTR_TARGET_TEMP_STEP = 'target_temp_step' +ATTR_AUX_HEAT = "aux_heat" +ATTR_CURRENT_HUMIDITY = "current_humidity" +ATTR_CURRENT_TEMPERATURE = "current_temperature" +ATTR_FAN_MODES = "fan_modes" +ATTR_FAN_MODE = "fan_mode" +ATTR_PRESET_MODE = "preset_mode" +ATTR_PRESET_MODES = "preset_modes" +ATTR_HUMIDITY = "humidity" +ATTR_MAX_HUMIDITY = "max_humidity" +ATTR_MIN_HUMIDITY = "min_humidity" +ATTR_MAX_TEMP = "max_temp" +ATTR_MIN_TEMP = "min_temp" +ATTR_HVAC_ACTIONS = "hvac_action" +ATTR_HVAC_MODES = "hvac_modes" +ATTR_HVAC_MODE = "hvac_mode" +ATTR_SWING_MODES = "swing_modes" +ATTR_SWING_MODE = "swing_mode" +ATTR_TARGET_TEMP_HIGH = "target_temp_high" +ATTR_TARGET_TEMP_LOW = "target_temp_low" +ATTR_TARGET_TEMP_STEP = "target_temp_step" DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 -DEFAULT_MIN_HUMITIDY = 30 +DEFAULT_MIN_HUMIDITY = 30 DEFAULT_MAX_HUMIDITY = 99 -DOMAIN = 'climate' +DOMAIN = "climate" -SERVICE_SET_AUX_HEAT = 'set_aux_heat' -SERVICE_SET_FAN_MODE = 'set_fan_mode' -SERVICE_SET_PRESET_MODE = 'set_preset_mode' -SERVICE_SET_HUMIDITY = 'set_humidity' -SERVICE_SET_HVAC_MODE = 'set_hvac_mode' -SERVICE_SET_SWING_MODE = 'set_swing_mode' -SERVICE_SET_TEMPERATURE = 'set_temperature' +SERVICE_SET_AUX_HEAT = "set_aux_heat" +SERVICE_SET_FAN_MODE = "set_fan_mode" +SERVICE_SET_PRESET_MODE = "set_preset_mode" +SERVICE_SET_HUMIDITY = "set_humidity" +SERVICE_SET_HVAC_MODE = "set_hvac_mode" +SERVICE_SET_SWING_MODE = "set_swing_mode" +SERVICE_SET_TEMPERATURE = "set_temperature" SUPPORT_TARGET_TEMPERATURE = 1 SUPPORT_TARGET_TEMPERATURE_RANGE = 2 diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index 261dfe93a40..98f085c1e8d 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -26,36 +26,38 @@ from .const import ( ) -async def _async_reproduce_states(hass: HomeAssistantType, - state: State, - context: Optional[Context] = None) -> None: +async def _async_reproduce_states( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: """Reproduce component states.""" + async def call_service(service: str, keys: Iterable, data=None): """Call service with set of attributes given.""" data = data or {} - data['entity_id'] = state.entity_id + data["entity_id"] = state.entity_id for key in keys: if key in state.attributes: data[key] = state.attributes[key] await hass.services.async_call( - DOMAIN, service, data, - blocking=True, context=context) + DOMAIN, service, data, blocking=True, context=context + ) if state.state in HVAC_MODES: - await call_service( - SERVICE_SET_HVAC_MODE, [], {ATTR_HVAC_MODE: state.state}) + await call_service(SERVICE_SET_HVAC_MODE, [], {ATTR_HVAC_MODE: state.state}) if ATTR_AUX_HEAT in state.attributes: await call_service(SERVICE_SET_AUX_HEAT, [ATTR_AUX_HEAT]) - if (ATTR_TEMPERATURE in state.attributes) or \ - (ATTR_TARGET_TEMP_HIGH in state.attributes) or \ - (ATTR_TARGET_TEMP_LOW in state.attributes): - await call_service(SERVICE_SET_TEMPERATURE, - [ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW]) + if ( + (ATTR_TEMPERATURE in state.attributes) + or (ATTR_TARGET_TEMP_HIGH in state.attributes) + or (ATTR_TARGET_TEMP_LOW in state.attributes) + ): + await call_service( + SERVICE_SET_TEMPERATURE, + [ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW], + ) if ATTR_PRESET_MODE in state.attributes: await call_service(SERVICE_SET_PRESET_MODE, [ATTR_PRESET_MODE]) @@ -68,10 +70,10 @@ async def _async_reproduce_states(hass: HomeAssistantType, @bind_hass -async def async_reproduce_states(hass: HomeAssistantType, - states: Iterable[State], - context: Optional[Context] = None) -> None: +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: """Reproduce component states.""" - await asyncio.gather(*[ - _async_reproduce_states(hass, state, context) - for state in states]) + await asyncio.gather( + *(_async_reproduce_states(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 3e17dd70841..8b295634c99 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -7,8 +7,12 @@ from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.alexa import const as alexa_const from homeassistant.components.google_assistant import const as ga_c from homeassistant.const import ( - CONF_MODE, CONF_NAME, CONF_REGION, EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP) + CONF_MODE, + CONF_NAME, + CONF_REGION, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entityfilter @@ -17,11 +21,23 @@ from homeassistant.util.aiohttp import MockRequest from . import http_api from .const import ( - CONF_ACME_DIRECTORY_SERVER, CONF_ALEXA, CONF_ALIASES, - CONF_CLOUDHOOK_CREATE_URL, CONF_COGNITO_CLIENT_ID, CONF_ENTITY_CONFIG, - CONF_FILTER, CONF_GOOGLE_ACTIONS, CONF_GOOGLE_ACTIONS_SYNC_URL, - CONF_RELAYER, CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, - CONF_USER_POOL_ID, DOMAIN, MODE_DEV, MODE_PROD, CONF_ALEXA_ACCESS_TOKEN_URL + CONF_ACME_DIRECTORY_SERVER, + CONF_ALEXA, + CONF_ALIASES, + CONF_CLOUDHOOK_CREATE_URL, + CONF_COGNITO_CLIENT_ID, + CONF_ENTITY_CONFIG, + CONF_FILTER, + CONF_GOOGLE_ACTIONS, + CONF_GOOGLE_ACTIONS_SYNC_URL, + CONF_RELAYER, + CONF_REMOTE_API_URL, + CONF_SUBSCRIPTION_INFO_URL, + CONF_USER_POOL_ID, + DOMAIN, + MODE_DEV, + MODE_PROD, + CONF_ALEXA_ACCESS_TOKEN_URL, ) from .prefs import CloudPreferences @@ -29,53 +45,63 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_MODE = MODE_PROD -SERVICE_REMOTE_CONNECT = 'remote_connect' -SERVICE_REMOTE_DISCONNECT = 'remote_disconnect' +SERVICE_REMOTE_CONNECT = "remote_connect" +SERVICE_REMOTE_DISCONNECT = "remote_disconnect" -ALEXA_ENTITY_SCHEMA = vol.Schema({ - vol.Optional(alexa_const.CONF_DESCRIPTION): cv.string, - vol.Optional(alexa_const.CONF_DISPLAY_CATEGORIES): cv.string, - vol.Optional(CONF_NAME): cv.string, -}) +ALEXA_ENTITY_SCHEMA = vol.Schema( + { + vol.Optional(alexa_const.CONF_DESCRIPTION): cv.string, + vol.Optional(alexa_const.CONF_DISPLAY_CATEGORIES): cv.string, + vol.Optional(CONF_NAME): cv.string, + } +) -GOOGLE_ENTITY_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ga_c.CONF_ROOM_HINT): cv.string, -}) +GOOGLE_ENTITY_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ga_c.CONF_ROOM_HINT): cv.string, + } +) -ASSISTANT_SCHEMA = vol.Schema({ - vol.Optional(CONF_FILTER, default=dict): entityfilter.FILTER_SCHEMA, -}) +ASSISTANT_SCHEMA = vol.Schema( + {vol.Optional(CONF_FILTER, default=dict): entityfilter.FILTER_SCHEMA} +) -ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({ - vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA} -}) +ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend( + {vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}} +) -GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend({ - vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA}, -}) +GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend( + {vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA}} +) # pylint: disable=no-value-for-parameter -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_MODE, default=DEFAULT_MODE): - vol.In([MODE_DEV, MODE_PROD]), - vol.Optional(CONF_COGNITO_CLIENT_ID): str, - vol.Optional(CONF_USER_POOL_ID): str, - vol.Optional(CONF_REGION): str, - vol.Optional(CONF_RELAYER): str, - vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): vol.Url(), - vol.Optional(CONF_SUBSCRIPTION_INFO_URL): vol.Url(), - vol.Optional(CONF_CLOUDHOOK_CREATE_URL): vol.Url(), - vol.Optional(CONF_REMOTE_API_URL): vol.Url(), - vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(), - vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, - vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, - vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): str, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In( + [MODE_DEV, MODE_PROD] + ), + vol.Optional(CONF_COGNITO_CLIENT_ID): str, + vol.Optional(CONF_USER_POOL_ID): str, + vol.Optional(CONF_REGION): str, + vol.Optional(CONF_RELAYER): str, + vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): vol.Url(), + vol.Optional(CONF_SUBSCRIPTION_INFO_URL): vol.Url(), + vol.Optional(CONF_CLOUDHOOK_CREATE_URL): vol.Url(), + vol.Optional(CONF_REMOTE_API_URL): vol.Url(), + vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(), + vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, + vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, + vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): str, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) class CloudNotAvailable(HomeAssistantError): @@ -93,8 +119,7 @@ def async_is_logged_in(hass) -> bool: @callback def async_active_subscription(hass) -> bool: """Test if user has an active subscription.""" - return \ - async_is_logged_in(hass) and not hass.data[DOMAIN].subscription_expired + return async_is_logged_in(hass) and not hass.data[DOMAIN].subscription_expired @bind_hass @@ -104,7 +129,7 @@ async def async_create_cloudhook(hass, webhook_id: str) -> str: raise CloudNotAvailable hook = await hass.data[DOMAIN].cloudhooks.async_create(webhook_id, True) - return hook['cloudhook_url'] + return hook["cloudhook_url"] @bind_hass @@ -165,7 +190,8 @@ async def async_setup(hass, config): if user is None: user = await hass.auth.async_create_system_user( - 'Home Assistant Cloud', [GROUP_ID_ADMIN]) + "Home Assistant Cloud", [GROUP_ID_ADMIN] + ) await prefs.async_update(cloud_user=user.id) # Initialize Cloud @@ -195,9 +221,11 @@ async def async_setup(hass, config): await prefs.async_update(remote_enabled=False) hass.helpers.service.async_register_admin_service( - DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler) + DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler + ) hass.helpers.service.async_register_admin_service( - DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler) + DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler + ) loaded_binary_sensor = False @@ -209,8 +237,11 @@ async def async_setup(hass, config): return loaded_binary_sensor = True - hass.async_create_task(hass.helpers.discovery.async_load_platform( - 'binary_sensor', DOMAIN, {}, config)) + hass.async_create_task( + hass.helpers.discovery.async_load_platform( + "binary_sensor", DOMAIN, {}, config + ) + ) cloud.iot.register_on_connect(_on_connect) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index a6aced474d6..d31bcfdfc40 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -20,8 +20,11 @@ from homeassistant.components.alexa import ( from .const import ( - CONF_ENTITY_CONFIG, CONF_FILTER, PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, - RequireRelink + CONF_ENTITY_CONFIG, + CONF_FILTER, + PREF_SHOULD_EXPOSE, + DEFAULT_SHOULD_EXPOSE, + RequireRelink, ) _LOGGER = logging.getLogger(__name__) @@ -49,7 +52,7 @@ class AlexaConfig(alexa_config.AbstractConfig): prefs.async_listen_updates(self._async_prefs_updated) hass.bus.async_listen( entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, - self._handle_entity_registry_updated + self._handle_entity_registry_updated, ) @property @@ -90,8 +93,7 @@ class AlexaConfig(alexa_config.AbstractConfig): entity_configs = self._prefs.alexa_entity_configs entity_config = entity_configs.get(entity_id, {}) - return entity_config.get( - PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) + return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) async def async_get_access_token(self): """Get an access token.""" @@ -102,13 +104,13 @@ class AlexaConfig(alexa_config.AbstractConfig): body = await resp.json() if resp.status == 400: - if body['reason'] in ('RefreshTokenNotFound', 'UnknownRegion'): + if body["reason"] in ("RefreshTokenNotFound", "UnknownRegion"): if self.should_report_state: await self._prefs.async_update(alexa_report_state=False) self.hass.components.persistent_notification.async_create( "There was an error reporting state to Alexa ({}). " "Please re-link your Alexa skill via the Alexa app to " - "continue using it.".format(body['reason']), + "continue using it.".format(body["reason"]), "Alexa state reporting disabled", "cloud_alexa_report", ) @@ -116,9 +118,9 @@ class AlexaConfig(alexa_config.AbstractConfig): raise alexa_errors.NoTokenAvailable - self._token = body['access_token'] - self._endpoint = body['event_endpoint'] - self._token_valid = utcnow() + timedelta(seconds=body['expires_in']) + self._token = body["access_token"] + self._endpoint = body["event_endpoint"] + self._token_valid = utcnow() + timedelta(seconds=body["expires_in"]) return self._token async def _async_prefs_updated(self, prefs): @@ -136,15 +138,18 @@ class AlexaConfig(alexa_config.AbstractConfig): # If entity prefs are the same or we have filter in config.yaml, # don't sync. - if (self._cur_entity_prefs is prefs.alexa_entity_configs or - not self._config[CONF_FILTER].empty_filter): + if ( + self._cur_entity_prefs is prefs.alexa_entity_configs + or not self._config[CONF_FILTER].empty_filter + ): return if self._alexa_sync_unsub: self._alexa_sync_unsub() self._alexa_sync_unsub = async_call_later( - self.hass, SYNC_DELAY, self._sync_prefs) + self.hass, SYNC_DELAY, self._sync_prefs + ) async def _sync_prefs(self, _now): """Sync the updated preferences to Alexa.""" @@ -225,14 +230,16 @@ class AlexaConfig(alexa_config.AbstractConfig): tasks = [] if to_update: - tasks.append(alexa_state_report.async_send_add_or_update_message( - self.hass, self, to_update - )) + tasks.append( + alexa_state_report.async_send_add_or_update_message( + self.hass, self, to_update + ) + ) if to_remove: - tasks.append(alexa_state_report.async_send_delete_message( - self.hass, self, to_remove - )) + tasks.append( + alexa_state_report.async_send_delete_message(self.hass, self, to_remove) + ) try: with async_timeout.timeout(10): @@ -253,14 +260,14 @@ class AlexaConfig(alexa_config.AbstractConfig): if not self.enabled or not self._cloud.is_logged_in: return - action = event.data['action'] - entity_id = event.data['entity_id'] + action = event.data["action"] + entity_id = event.data["entity_id"] to_update = [] to_remove = [] - if action == 'create' and self.should_expose(entity_id): + if action == "create" and self.should_expose(entity_id): to_update.append(entity_id) - elif action == 'remove' and self.should_expose(entity_id): + elif action == "remove" and self.should_expose(entity_id): to_remove.append(entity_id) try: diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 3e4aaf9cc84..2192eec8923 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -10,8 +10,7 @@ from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN WAIT_UNTIL_CHANGE = 3 -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the cloud binary sensors.""" if discovery_info is None: return @@ -46,7 +45,7 @@ class CloudRemoteBinary(BinarySensorDevice): @property def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" - return 'connectivity' + return "connectivity" @property def available(self) -> bool: @@ -60,13 +59,15 @@ class CloudRemoteBinary(BinarySensorDevice): async def async_added_to_hass(self): """Register update dispatcher.""" + async def async_state_update(data): """Update callback.""" await asyncio.sleep(WAIT_UNTIL_CHANGE) self.async_schedule_update_ha_state() self._unsub_dispatcher = async_dispatcher_connect( - self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update) + self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update + ) async def async_will_remove_from_hass(self): """Register update dispatcher.""" diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index d22e5bf37ba..07882d8dac2 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -28,10 +28,14 @@ _LOGGER = logging.getLogger(__name__) class CloudClient(Interface): """Interface class for Home Assistant Cloud.""" - def __init__(self, hass: HomeAssistantType, prefs: CloudPreferences, - websession: aiohttp.ClientSession, - alexa_user_config: Dict[str, Any], - google_user_config: Dict[str, Any]): + def __init__( + self, + hass: HomeAssistantType, + prefs: CloudPreferences, + websession: aiohttp.ClientSession, + alexa_user_config: Dict[str, Any], + google_user_config: Dict[str, Any], + ): """Initialize client interface to Cloud.""" self._hass = hass self._prefs = prefs @@ -83,7 +87,8 @@ class CloudClient(Interface): if self._alexa_config is None: assert self.cloud is not None self._alexa_config = alexa_config.AlexaConfig( - self._hass, self.alexa_user_config, self._prefs, self.cloud) + self._hass, self.alexa_user_config, self._prefs, self.cloud + ) return self._alexa_config @@ -93,7 +98,8 @@ class CloudClient(Interface): if not self._google_config: assert self.cloud is not None self._google_config = google_config.CloudGoogleConfig( - self.google_user_config, self._prefs, self.cloud) + self.google_user_config, self._prefs, self.cloud + ) return self._google_config @@ -101,8 +107,7 @@ class CloudClient(Interface): """Initialize the client.""" self.cloud = cloud - if (not self.alexa_config.should_report_state or - not self.cloud.is_logged_in): + if not self.alexa_config.should_report_state or not self.cloud.is_logged_in: return try: @@ -127,16 +132,13 @@ class CloudClient(Interface): if identifier.startswith("remote_"): async_dispatcher_send(self._hass, DISPATCHER_REMOTE_UPDATE, data) - async def async_alexa_message( - self, payload: Dict[Any, Any]) -> Dict[Any, Any]: + async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud alexa message to client.""" return await alexa_sh.async_handle_message( - self._hass, self.alexa_config, payload, - enabled=self._prefs.alexa_enabled + self._hass, self.alexa_config, payload, enabled=self._prefs.alexa_enabled ) - async def async_google_message( - self, payload: Dict[Any, Any]) -> Dict[Any, Any]: + async def async_google_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud google message to client.""" if not self._prefs.google_enabled: return ga.turned_off_response(payload) @@ -145,44 +147,39 @@ class CloudClient(Interface): self._hass, self.google_config, self.prefs.cloud_user, payload ) - async def async_webhook_message( - self, payload: Dict[Any, Any]) -> Dict[Any, Any]: + async def async_webhook_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud webhook message to client.""" - cloudhook_id = payload['cloudhook_id'] + cloudhook_id = payload["cloudhook_id"] found = None for cloudhook in self._prefs.cloudhooks.values(): - if cloudhook['cloudhook_id'] == cloudhook_id: + if cloudhook["cloudhook_id"] == cloudhook_id: found = cloudhook break if found is None: - return { - 'status': 200 - } + return {"status": 200} request = MockRequest( - content=payload['body'].encode('utf-8'), - headers=payload['headers'], - method=payload['method'], - query_string=payload['query'], + content=payload["body"].encode("utf-8"), + headers=payload["headers"], + method=payload["method"], + query_string=payload["query"], ) response = await self._hass.components.webhook.async_handle_webhook( - found['webhook_id'], request) + found["webhook_id"], request + ) response_dict = utils.aiohttp_serialize_response(response) - body = response_dict.get('body') + body = response_dict.get("body") return { - 'body': body, - 'status': response_dict['status'], - 'headers': { - 'Content-Type': response.content_type - } + "body": body, + "status": response_dict["status"], + "headers": {"Content-Type": response.content_type}, } - async def async_cloudhooks_update( - self, data: Dict[str, Dict[str, str]]) -> None: + async def async_cloudhooks_update(self, data: Dict[str, Dict[str, str]]) -> None: """Update local list of cloudhooks.""" await self._prefs.async_update(cloudhooks=data) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index fdb36723fdb..df1b8ef165d 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -1,43 +1,43 @@ """Constants for the cloud component.""" -DOMAIN = 'cloud' +DOMAIN = "cloud" REQUEST_TIMEOUT = 10 -PREF_ENABLE_ALEXA = 'alexa_enabled' -PREF_ENABLE_GOOGLE = 'google_enabled' -PREF_ENABLE_REMOTE = 'remote_enabled' -PREF_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin' -PREF_CLOUDHOOKS = 'cloudhooks' -PREF_CLOUD_USER = 'cloud_user' -PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs' -PREF_ALEXA_ENTITY_CONFIGS = 'alexa_entity_configs' -PREF_ALEXA_REPORT_STATE = 'alexa_report_state' -PREF_OVERRIDE_NAME = 'override_name' -PREF_DISABLE_2FA = 'disable_2fa' -PREF_ALIASES = 'aliases' -PREF_SHOULD_EXPOSE = 'should_expose' +PREF_ENABLE_ALEXA = "alexa_enabled" +PREF_ENABLE_GOOGLE = "google_enabled" +PREF_ENABLE_REMOTE = "remote_enabled" +PREF_GOOGLE_SECURE_DEVICES_PIN = "google_secure_devices_pin" +PREF_CLOUDHOOKS = "cloudhooks" +PREF_CLOUD_USER = "cloud_user" +PREF_GOOGLE_ENTITY_CONFIGS = "google_entity_configs" +PREF_ALEXA_ENTITY_CONFIGS = "alexa_entity_configs" +PREF_ALEXA_REPORT_STATE = "alexa_report_state" +PREF_OVERRIDE_NAME = "override_name" +PREF_DISABLE_2FA = "disable_2fa" +PREF_ALIASES = "aliases" +PREF_SHOULD_EXPOSE = "should_expose" DEFAULT_SHOULD_EXPOSE = True DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False -CONF_ALEXA = 'alexa' -CONF_ALIASES = 'aliases' -CONF_COGNITO_CLIENT_ID = 'cognito_client_id' -CONF_ENTITY_CONFIG = 'entity_config' -CONF_FILTER = 'filter' -CONF_GOOGLE_ACTIONS = 'google_actions' -CONF_RELAYER = 'relayer' -CONF_USER_POOL_ID = 'user_pool_id' -CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url' -CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url' -CONF_CLOUDHOOK_CREATE_URL = 'cloudhook_create_url' -CONF_REMOTE_API_URL = 'remote_api_url' -CONF_ACME_DIRECTORY_SERVER = 'acme_directory_server' -CONF_ALEXA_ACCESS_TOKEN_URL = 'alexa_access_token_url' +CONF_ALEXA = "alexa" +CONF_ALIASES = "aliases" +CONF_COGNITO_CLIENT_ID = "cognito_client_id" +CONF_ENTITY_CONFIG = "entity_config" +CONF_FILTER = "filter" +CONF_GOOGLE_ACTIONS = "google_actions" +CONF_RELAYER = "relayer" +CONF_USER_POOL_ID = "user_pool_id" +CONF_GOOGLE_ACTIONS_SYNC_URL = "google_actions_sync_url" +CONF_SUBSCRIPTION_INFO_URL = "subscription_info_url" +CONF_CLOUDHOOK_CREATE_URL = "cloudhook_create_url" +CONF_REMOTE_API_URL = "remote_api_url" +CONF_ACME_DIRECTORY_SERVER = "acme_directory_server" +CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url" MODE_DEV = "development" MODE_PROD = "production" -DISPATCHER_REMOTE_UPDATE = 'cloud_remote_update' +DISPATCHER_REMOTE_UPDATE = "cloud_remote_update" class InvalidTrustedNetworks(Exception): diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 5e95417cd33..8986f8f3995 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -3,8 +3,12 @@ from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.components.google_assistant.helpers import AbstractConfig from .const import ( - PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, CONF_ENTITY_CONFIG, - PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) + PREF_SHOULD_EXPOSE, + DEFAULT_SHOULD_EXPOSE, + CONF_ENTITY_CONFIG, + PREF_DISABLE_2FA, + DEFAULT_DISABLE_2FA, +) class CloudGoogleConfig(AbstractConfig): @@ -36,17 +40,15 @@ class CloudGoogleConfig(AbstractConfig): if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False - if not self._config['filter'].empty_filter: - return self._config['filter'](state.entity_id) + if not self._config["filter"].empty_filter: + return self._config["filter"](state.entity_id) entity_configs = self._prefs.google_entity_configs entity_config = entity_configs.get(state.entity_id, {}) - return entity_config.get( - PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) + return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) def should_2fa(self, state): """If an entity should be checked for 2FA.""" entity_configs = self._prefs.google_entity_configs entity_config = entity_configs.get(state.entity_id, {}) - return not entity_config.get( - PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) + return not entity_config.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index e5f00873aab..d261c9e494c 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -10,8 +10,7 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.data_validator import ( - RequestDataValidator) +from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components import websocket_api from homeassistant.components.websocket_api import const as ws_const from homeassistant.components.alexa import ( @@ -21,78 +20,76 @@ from homeassistant.components.alexa import ( from homeassistant.components.google_assistant import helpers as google_helpers from .const import ( - DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, - PREF_GOOGLE_SECURE_DEVICES_PIN, InvalidTrustedNetworks, - InvalidTrustedProxies, PREF_ALEXA_REPORT_STATE, RequireRelink) + DOMAIN, + REQUEST_TIMEOUT, + PREF_ENABLE_ALEXA, + PREF_ENABLE_GOOGLE, + PREF_GOOGLE_SECURE_DEVICES_PIN, + InvalidTrustedNetworks, + InvalidTrustedProxies, + PREF_ALEXA_REPORT_STATE, + RequireRelink, +) _LOGGER = logging.getLogger(__name__) -WS_TYPE_STATUS = 'cloud/status' -SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_STATUS, -}) +WS_TYPE_STATUS = "cloud/status" +SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_STATUS} +) -WS_TYPE_SUBSCRIPTION = 'cloud/subscription' -SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_SUBSCRIPTION, -}) +WS_TYPE_SUBSCRIPTION = "cloud/subscription" +SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_SUBSCRIPTION} +) -WS_TYPE_HOOK_CREATE = 'cloud/cloudhook/create' -SCHEMA_WS_HOOK_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_HOOK_CREATE, - vol.Required('webhook_id'): str -}) +WS_TYPE_HOOK_CREATE = "cloud/cloudhook/create" +SCHEMA_WS_HOOK_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_HOOK_CREATE, vol.Required("webhook_id"): str} +) -WS_TYPE_HOOK_DELETE = 'cloud/cloudhook/delete' -SCHEMA_WS_HOOK_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_HOOK_DELETE, - vol.Required('webhook_id'): str -}) +WS_TYPE_HOOK_DELETE = "cloud/cloudhook/delete" +SCHEMA_WS_HOOK_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_HOOK_DELETE, vol.Required("webhook_id"): str} +) _CLOUD_ERRORS = { - InvalidTrustedNetworks: - (500, 'Remote UI not compatible with 127.0.0.1/::1' - ' as a trusted network.'), - InvalidTrustedProxies: - (500, 'Remote UI not compatible with 127.0.0.1/::1' - ' as trusted proxies.'), + InvalidTrustedNetworks: ( + 500, + "Remote UI not compatible with 127.0.0.1/::1 as a trusted network.", + ), + InvalidTrustedProxies: ( + 500, + "Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.", + ), } async def async_setup(hass): """Initialize the HTTP API.""" hass.components.websocket_api.async_register_command( - WS_TYPE_STATUS, websocket_cloud_status, - SCHEMA_WS_STATUS + WS_TYPE_STATUS, websocket_cloud_status, SCHEMA_WS_STATUS ) hass.components.websocket_api.async_register_command( - WS_TYPE_SUBSCRIPTION, websocket_subscription, - SCHEMA_WS_SUBSCRIPTION + WS_TYPE_SUBSCRIPTION, websocket_subscription, SCHEMA_WS_SUBSCRIPTION + ) + hass.components.websocket_api.async_register_command(websocket_update_prefs) + hass.components.websocket_api.async_register_command( + WS_TYPE_HOOK_CREATE, websocket_hook_create, SCHEMA_WS_HOOK_CREATE ) hass.components.websocket_api.async_register_command( - websocket_update_prefs) - hass.components.websocket_api.async_register_command( - WS_TYPE_HOOK_CREATE, websocket_hook_create, - SCHEMA_WS_HOOK_CREATE + WS_TYPE_HOOK_DELETE, websocket_hook_delete, SCHEMA_WS_HOOK_DELETE ) - hass.components.websocket_api.async_register_command( - WS_TYPE_HOOK_DELETE, websocket_hook_delete, - SCHEMA_WS_HOOK_DELETE - ) - hass.components.websocket_api.async_register_command( - websocket_remote_connect) - hass.components.websocket_api.async_register_command( - websocket_remote_disconnect) + hass.components.websocket_api.async_register_command(websocket_remote_connect) + hass.components.websocket_api.async_register_command(websocket_remote_disconnect) - hass.components.websocket_api.async_register_command( - google_assistant_list) - hass.components.websocket_api.async_register_command( - google_assistant_update) + hass.components.websocket_api.async_register_command(google_assistant_list) + hass.components.websocket_api.async_register_command(google_assistant_update) hass.components.websocket_api.async_register_command(alexa_list) hass.components.websocket_api.async_register_command(alexa_update) @@ -107,26 +104,22 @@ async def async_setup(hass): from hass_nabucasa import auth - _CLOUD_ERRORS.update({ - auth.UserNotFound: - (400, "User does not exist."), - auth.UserNotConfirmed: - (400, 'Email not confirmed.'), - auth.UserExists: - (400, 'An account with the given email already exists.'), - auth.Unauthenticated: - (401, 'Authentication failed.'), - auth.PasswordChangeRequired: - (400, 'Password change required.'), - asyncio.TimeoutError: - (502, 'Unable to reach the Home Assistant cloud.'), - aiohttp.ClientError: - (500, 'Error making internal request'), - }) + _CLOUD_ERRORS.update( + { + auth.UserNotFound: (400, "User does not exist."), + auth.UserNotConfirmed: (400, "Email not confirmed."), + auth.UserExists: (400, "An account with the given email already exists."), + auth.Unauthenticated: (401, "Authentication failed."), + auth.PasswordChangeRequired: (400, "Password change required."), + asyncio.TimeoutError: (502, "Unable to reach the Home Assistant cloud."), + aiohttp.ClientError: (500, "Error making internal request"), + } + ) def _handle_cloud_errors(handler): """Webview decorator to handle auth errors.""" + @wraps(handler) async def error_handler(view, request, *args, **kwargs): """Handle exceptions that raise from the wrapped request handler.""" @@ -137,14 +130,15 @@ def _handle_cloud_errors(handler): except Exception as err: # pylint: disable=broad-except status, msg = _process_cloud_exception(err, request.path) return view.json_message( - msg, status_code=status, - message_code=err.__class__.__name__.lower()) + msg, status_code=status, message_code=err.__class__.__name__.lower() + ) return error_handler def _ws_handle_cloud_errors(handler): """Websocket decorator to handle auth errors.""" + @wraps(handler) async def error_handler(hass, connection, msg): """Handle exceptions that raise from the wrapped handler.""" @@ -152,8 +146,8 @@ def _ws_handle_cloud_errors(handler): return await handler(hass, connection, msg) except Exception as err: # pylint: disable=broad-except - err_status, err_msg = _process_cloud_exception(err, msg['type']) - connection.send_error(msg['id'], err_status, err_msg) + err_status, err_msg = _process_cloud_exception(err, msg["type"]) + connection.send_error(msg["id"], err_status, err_msg) return error_handler @@ -162,22 +156,21 @@ def _process_cloud_exception(exc, where): """Process a cloud exception.""" err_info = _CLOUD_ERRORS.get(exc.__class__) if err_info is None: - _LOGGER.exception( - "Unexpected error processing request for %s", where) - err_info = (502, 'Unexpected error: {}'.format(exc)) + _LOGGER.exception("Unexpected error processing request for %s", where) + err_info = (502, "Unexpected error: {}".format(exc)) return err_info class GoogleActionsSyncView(HomeAssistantView): """Trigger a Google Actions Smart Home Sync.""" - url = '/api/cloud/google_actions/sync' - name = 'api:cloud:google_actions/sync' + url = "/api/cloud/google_actions/sync" + name = "api:cloud:google_actions/sync" @_handle_cloud_errors async def post(self, request): """Trigger a Google Actions sync.""" - hass = request.app['hass'] + hass = request.app["hass"] cloud = hass.data[DOMAIN] websession = hass.helpers.aiohttp_client.async_get_clientsession() @@ -186,9 +179,8 @@ class GoogleActionsSyncView(HomeAssistantView): with async_timeout.timeout(REQUEST_TIMEOUT): req = await websession.post( - cloud.google_actions_sync_url, headers={ - 'authorization': cloud.id_token - }) + cloud.google_actions_sync_url, headers={"authorization": cloud.id_token} + ) return self.json({}, status_code=req.status) @@ -196,110 +188,107 @@ class GoogleActionsSyncView(HomeAssistantView): class CloudLoginView(HomeAssistantView): """Login to Home Assistant cloud.""" - url = '/api/cloud/login' - name = 'api:cloud:login' + url = "/api/cloud/login" + name = "api:cloud:login" @_handle_cloud_errors - @RequestDataValidator(vol.Schema({ - vol.Required('email'): str, - vol.Required('password'): str, - })) + @RequestDataValidator( + vol.Schema({vol.Required("email"): str, vol.Required("password"): str}) + ) async def post(self, request, data): """Handle login request.""" - hass = request.app['hass'] + hass = request.app["hass"] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT): - await hass.async_add_job(cloud.auth.login, data['email'], - data['password']) + await hass.async_add_job(cloud.auth.login, data["email"], data["password"]) hass.async_add_job(cloud.iot.connect) - return self.json({'success': True}) + return self.json({"success": True}) class CloudLogoutView(HomeAssistantView): """Log out of the Home Assistant cloud.""" - url = '/api/cloud/logout' - name = 'api:cloud:logout' + url = "/api/cloud/logout" + name = "api:cloud:logout" @_handle_cloud_errors async def post(self, request): """Handle logout request.""" - hass = request.app['hass'] + hass = request.app["hass"] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT): await cloud.logout() - return self.json_message('ok') + return self.json_message("ok") class CloudRegisterView(HomeAssistantView): """Register on the Home Assistant cloud.""" - url = '/api/cloud/register' - name = 'api:cloud:register' + url = "/api/cloud/register" + name = "api:cloud:register" @_handle_cloud_errors - @RequestDataValidator(vol.Schema({ - vol.Required('email'): str, - vol.Required('password'): vol.All(str, vol.Length(min=6)), - })) + @RequestDataValidator( + vol.Schema( + { + vol.Required("email"): str, + vol.Required("password"): vol.All(str, vol.Length(min=6)), + } + ) + ) async def post(self, request, data): """Handle registration request.""" - hass = request.app['hass'] + hass = request.app["hass"] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT): await hass.async_add_job( - cloud.auth.register, data['email'], data['password']) + cloud.auth.register, data["email"], data["password"] + ) - return self.json_message('ok') + return self.json_message("ok") class CloudResendConfirmView(HomeAssistantView): """Resend email confirmation code.""" - url = '/api/cloud/resend_confirm' - name = 'api:cloud:resend_confirm' + url = "/api/cloud/resend_confirm" + name = "api:cloud:resend_confirm" @_handle_cloud_errors - @RequestDataValidator(vol.Schema({ - vol.Required('email'): str, - })) + @RequestDataValidator(vol.Schema({vol.Required("email"): str})) async def post(self, request, data): """Handle resending confirm email code request.""" - hass = request.app['hass'] + hass = request.app["hass"] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT): - await hass.async_add_job( - cloud.auth.resend_email_confirm, data['email']) + await hass.async_add_job(cloud.auth.resend_email_confirm, data["email"]) - return self.json_message('ok') + return self.json_message("ok") class CloudForgotPasswordView(HomeAssistantView): """View to start Forgot Password flow..""" - url = '/api/cloud/forgot_password' - name = 'api:cloud:forgot_password' + url = "/api/cloud/forgot_password" + name = "api:cloud:forgot_password" @_handle_cloud_errors - @RequestDataValidator(vol.Schema({ - vol.Required('email'): str, - })) + @RequestDataValidator(vol.Schema({vol.Required("email"): str})) async def post(self, request, data): """Handle forgot password request.""" - hass = request.app['hass'] + hass = request.app["hass"] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT): - await hass.async_add_job( - cloud.auth.forgot_password, data['email']) + await hass.async_add_job(cloud.auth.forgot_password, data["email"]) - return self.json_message('ok') + return self.json_message("ok") @callback @@ -310,19 +299,23 @@ def websocket_cloud_status(hass, connection, msg): """ cloud = hass.data[DOMAIN] connection.send_message( - websocket_api.result_message(msg['id'], _account_data(cloud))) + websocket_api.result_message(msg["id"], _account_data(cloud)) + ) def _require_cloud_login(handler): """Websocket decorator that requires cloud to be logged in.""" + @wraps(handler) def with_cloud_auth(hass, connection, msg): """Require to be logged into the cloud.""" cloud = hass.data[DOMAIN] if not cloud.is_logged_in: - connection.send_message(websocket_api.error_message( - msg['id'], 'not_logged_in', - 'You need to be logged in to the cloud.')) + connection.send_message( + websocket_api.error_message( + msg["id"], "not_logged_in", "You need to be logged in to the cloud." + ) + ) return handler(hass, connection, msg) @@ -335,22 +328,25 @@ def _require_cloud_login(handler): async def websocket_subscription(hass, connection, msg): """Handle request for account info.""" from hass_nabucasa.const import STATE_DISCONNECTED + cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT): response = await cloud.fetch_subscription_info() if response.status != 200: - connection.send_message(websocket_api.error_message( - msg['id'], 'request_failed', 'Failed to request subscription')) + connection.send_message( + websocket_api.error_message( + msg["id"], "request_failed", "Failed to request subscription" + ) + ) data = await response.json() # Check if a user is subscribed but local info is outdated # In that case, let's refresh and reconnect - if data.get('provider') and not cloud.is_connected: - _LOGGER.debug( - "Found disconnected account with valid subscriotion, connecting") + if data.get("provider") and not cloud.is_connected: + _LOGGER.debug("Found disconnected account with valid subscriotion, connecting") await hass.async_add_executor_job(cloud.auth.renew_access_token) # Cancel reconnect in progress @@ -359,25 +355,27 @@ async def websocket_subscription(hass, connection, msg): hass.async_create_task(cloud.iot.connect()) - connection.send_message(websocket_api.result_message(msg['id'], data)) + connection.send_message(websocket_api.result_message(msg["id"], data)) @_require_cloud_login @websocket_api.async_response -@websocket_api.websocket_command({ - vol.Required('type'): 'cloud/update_prefs', - vol.Optional(PREF_ENABLE_GOOGLE): bool, - vol.Optional(PREF_ENABLE_ALEXA): bool, - vol.Optional(PREF_ALEXA_REPORT_STATE): bool, - vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str), -}) +@websocket_api.websocket_command( + { + vol.Required("type"): "cloud/update_prefs", + vol.Optional(PREF_ENABLE_GOOGLE): bool, + vol.Optional(PREF_ENABLE_ALEXA): bool, + vol.Optional(PREF_ALEXA_REPORT_STATE): bool, + vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str), + } +) async def websocket_update_prefs(hass, connection, msg): """Handle request for account info.""" cloud = hass.data[DOMAIN] changes = dict(msg) - changes.pop('id') - changes.pop('type') + changes.pop("id") + changes.pop("type") # If we turn alexa linking on, validate that we can fetch access token if changes.get(PREF_ALEXA_REPORT_STATE): @@ -385,20 +383,22 @@ async def websocket_update_prefs(hass, connection, msg): with async_timeout.timeout(10): await cloud.client.alexa_config.async_get_access_token() except asyncio.TimeoutError: - connection.send_error(msg['id'], 'alexa_timeout', - 'Timeout validating Alexa access token.') + connection.send_error( + msg["id"], "alexa_timeout", "Timeout validating Alexa access token." + ) return except (alexa_errors.NoTokenAvailable, RequireRelink): connection.send_error( - msg['id'], 'alexa_relink', - 'Please go to the Alexa app and re-link the Home Assistant ' - 'skill and then try to enable state reporting.' + msg["id"], + "alexa_relink", + "Please go to the Alexa app and re-link the Home Assistant " + "skill and then try to enable state reporting.", ) return await cloud.client.prefs.async_update(**changes) - connection.send_message(websocket_api.result_message(msg['id'])) + connection.send_message(websocket_api.result_message(msg["id"])) @_require_cloud_login @@ -407,8 +407,8 @@ async def websocket_update_prefs(hass, connection, msg): async def websocket_hook_create(hass, connection, msg): """Handle request for account info.""" cloud = hass.data[DOMAIN] - hook = await cloud.cloudhooks.async_create(msg['webhook_id'], False) - connection.send_message(websocket_api.result_message(msg['id'], hook)) + hook = await cloud.cloudhooks.async_create(msg["webhook_id"], False) + connection.send_message(websocket_api.result_message(msg["id"], hook)) @_require_cloud_login @@ -417,8 +417,8 @@ async def websocket_hook_create(hass, connection, msg): async def websocket_hook_delete(hass, connection, msg): """Handle request for account info.""" cloud = hass.data[DOMAIN] - await cloud.cloudhooks.async_delete(msg['webhook_id']) - connection.send_message(websocket_api.result_message(msg['id'])) + await cloud.cloudhooks.async_delete(msg["webhook_id"]) + connection.send_message(websocket_api.result_message(msg["id"])) def _account_data(cloud): @@ -426,10 +426,7 @@ def _account_data(cloud): from hass_nabucasa.const import STATE_DISCONNECTED if not cloud.is_logged_in: - return { - 'logged_in': False, - 'cloud': STATE_DISCONNECTED, - } + return {"logged_in": False, "cloud": STATE_DISCONNECTED} claims = cloud.claims client = cloud.client @@ -442,15 +439,15 @@ def _account_data(cloud): certificate = None return { - 'logged_in': True, - 'email': claims['email'], - 'cloud': cloud.iot.state, - 'prefs': client.prefs.as_dict(), - 'google_entities': client.google_user_config['filter'].config, - 'alexa_entities': client.alexa_user_config['filter'].config, - 'remote_domain': remote.instance_domain, - 'remote_connected': remote.is_connected, - 'remote_certificate': certificate, + "logged_in": True, + "email": claims["email"], + "cloud": cloud.iot.state, + "prefs": client.prefs.as_dict(), + "google_entities": client.google_user_config["filter"].config, + "alexa_entities": client.alexa_user_config["filter"].config, + "remote_domain": remote.instance_domain, + "remote_connected": remote.is_connected, + "remote_certificate": certificate, } @@ -458,139 +455,133 @@ def _account_data(cloud): @_require_cloud_login @websocket_api.async_response @_ws_handle_cloud_errors -@websocket_api.websocket_command({ - 'type': 'cloud/remote/connect' -}) +@websocket_api.websocket_command({"type": "cloud/remote/connect"}) async def websocket_remote_connect(hass, connection, msg): """Handle request for connect remote.""" cloud = hass.data[DOMAIN] await cloud.client.prefs.async_update(remote_enabled=True) await cloud.remote.connect() - connection.send_result(msg['id'], _account_data(cloud)) + connection.send_result(msg["id"], _account_data(cloud)) @websocket_api.require_admin @_require_cloud_login @websocket_api.async_response @_ws_handle_cloud_errors -@websocket_api.websocket_command({ - 'type': 'cloud/remote/disconnect' -}) +@websocket_api.websocket_command({"type": "cloud/remote/disconnect"}) async def websocket_remote_disconnect(hass, connection, msg): """Handle request for disconnect remote.""" cloud = hass.data[DOMAIN] await cloud.client.prefs.async_update(remote_enabled=False) await cloud.remote.disconnect() - connection.send_result(msg['id'], _account_data(cloud)) + connection.send_result(msg["id"], _account_data(cloud)) @websocket_api.require_admin @_require_cloud_login @websocket_api.async_response @_ws_handle_cloud_errors -@websocket_api.websocket_command({ - 'type': 'cloud/google_assistant/entities' -}) +@websocket_api.websocket_command({"type": "cloud/google_assistant/entities"}) async def google_assistant_list(hass, connection, msg): """List all google assistant entities.""" cloud = hass.data[DOMAIN] - entities = google_helpers.async_get_entities( - hass, cloud.client.google_config - ) + entities = google_helpers.async_get_entities(hass, cloud.client.google_config) result = [] for entity in entities: - result.append({ - 'entity_id': entity.entity_id, - 'traits': [trait.name for trait in entity.traits()], - 'might_2fa': entity.might_2fa(), - }) + result.append( + { + "entity_id": entity.entity_id, + "traits": [trait.name for trait in entity.traits()], + "might_2fa": entity.might_2fa(), + } + ) - connection.send_result(msg['id'], result) + connection.send_result(msg["id"], result) @websocket_api.require_admin @_require_cloud_login @websocket_api.async_response @_ws_handle_cloud_errors -@websocket_api.websocket_command({ - 'type': 'cloud/google_assistant/entities/update', - 'entity_id': str, - vol.Optional('should_expose'): bool, - vol.Optional('override_name'): str, - vol.Optional('aliases'): [str], - vol.Optional('disable_2fa'): bool, -}) +@websocket_api.websocket_command( + { + "type": "cloud/google_assistant/entities/update", + "entity_id": str, + vol.Optional("should_expose"): bool, + vol.Optional("override_name"): str, + vol.Optional("aliases"): [str], + vol.Optional("disable_2fa"): bool, + } +) async def google_assistant_update(hass, connection, msg): """Update google assistant config.""" cloud = hass.data[DOMAIN] changes = dict(msg) - changes.pop('type') - changes.pop('id') + changes.pop("type") + changes.pop("id") await cloud.client.prefs.async_update_google_entity_config(**changes) connection.send_result( - msg['id'], - cloud.client.prefs.google_entity_configs.get(msg['entity_id'])) + msg["id"], cloud.client.prefs.google_entity_configs.get(msg["entity_id"]) + ) @websocket_api.require_admin @_require_cloud_login @websocket_api.async_response @_ws_handle_cloud_errors -@websocket_api.websocket_command({ - 'type': 'cloud/alexa/entities' -}) +@websocket_api.websocket_command({"type": "cloud/alexa/entities"}) async def alexa_list(hass, connection, msg): """List all alexa entities.""" cloud = hass.data[DOMAIN] - entities = alexa_entities.async_get_entities( - hass, cloud.client.alexa_config - ) + entities = alexa_entities.async_get_entities(hass, cloud.client.alexa_config) result = [] for entity in entities: - result.append({ - 'entity_id': entity.entity_id, - 'display_categories': entity.default_display_categories(), - 'interfaces': [ifc.name() for ifc in entity.interfaces()], - }) + result.append( + { + "entity_id": entity.entity_id, + "display_categories": entity.default_display_categories(), + "interfaces": [ifc.name() for ifc in entity.interfaces()], + } + ) - connection.send_result(msg['id'], result) + connection.send_result(msg["id"], result) @websocket_api.require_admin @_require_cloud_login @websocket_api.async_response @_ws_handle_cloud_errors -@websocket_api.websocket_command({ - 'type': 'cloud/alexa/entities/update', - 'entity_id': str, - vol.Optional('should_expose'): bool, -}) +@websocket_api.websocket_command( + { + "type": "cloud/alexa/entities/update", + "entity_id": str, + vol.Optional("should_expose"): bool, + } +) async def alexa_update(hass, connection, msg): """Update alexa entity config.""" cloud = hass.data[DOMAIN] changes = dict(msg) - changes.pop('type') - changes.pop('id') + changes.pop("type") + changes.pop("id") await cloud.client.prefs.async_update_alexa_entity_config(**changes) connection.send_result( - msg['id'], - cloud.client.prefs.alexa_entity_configs.get(msg['entity_id'])) + msg["id"], cloud.client.prefs.alexa_entity_configs.get(msg["entity_id"]) + ) @websocket_api.require_admin @_require_cloud_login @websocket_api.async_response -@websocket_api.websocket_command({ - 'type': 'cloud/alexa/sync', -}) +@websocket_api.websocket_command({"type": "cloud/alexa/sync"}) async def alexa_sync(hass, connection, msg): """Sync with Alexa.""" cloud = hass.data[DOMAIN] @@ -600,14 +591,13 @@ async def alexa_sync(hass, connection, msg): success = await cloud.client.alexa_config.async_sync_entities() except alexa_errors.NoTokenAvailable: connection.send_error( - msg['id'], 'alexa_relink', - 'Please go to the Alexa app and re-link the Home Assistant ' - 'skill.' + msg["id"], + "alexa_relink", + "Please go to the Alexa app and re-link the Home Assistant " "skill.", ) return if success: - connection.send_result(msg['id']) + connection.send_result(msg["id"]) else: - connection.send_error( - msg['id'], ws_const.ERR_UNKNOWN_ERROR, 'Unknown error') + connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, "Unknown error") diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index e848f54425b..58739bededc 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,14 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/components/cloud", - "requirements": [ - "hass-nabucasa==0.15" - ], - "dependencies": [ - "http", - "webhook" - ], - "codeowners": [ - "@home-assistant/cloud" - ] + "requirements": ["hass-nabucasa==0.16"], + "dependencies": ["http", "webhook"], + "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index a01a6dd4cb5..d6e78e87e25 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -5,12 +5,24 @@ from homeassistant.core import callback from homeassistant.util.logging import async_create_catching_coro from .const import ( - DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, - PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER, - PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA, - PREF_ALIASES, PREF_SHOULD_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS, - PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE, - InvalidTrustedNetworks, InvalidTrustedProxies) + DOMAIN, + PREF_ENABLE_ALEXA, + PREF_ENABLE_GOOGLE, + PREF_ENABLE_REMOTE, + PREF_GOOGLE_SECURE_DEVICES_PIN, + PREF_CLOUDHOOKS, + PREF_CLOUD_USER, + PREF_GOOGLE_ENTITY_CONFIGS, + PREF_OVERRIDE_NAME, + PREF_DISABLE_2FA, + PREF_ALIASES, + PREF_SHOULD_EXPOSE, + PREF_ALEXA_ENTITY_CONFIGS, + PREF_ALEXA_REPORT_STATE, + DEFAULT_ALEXA_REPORT_STATE, + InvalidTrustedNetworks, + InvalidTrustedProxies, +) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -50,23 +62,30 @@ class CloudPreferences: """Listen for updates to the preferences.""" self._listeners.append(listener) - async def async_update(self, *, google_enabled=_UNDEF, - alexa_enabled=_UNDEF, remote_enabled=_UNDEF, - google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF, - cloud_user=_UNDEF, google_entity_configs=_UNDEF, - alexa_entity_configs=_UNDEF, - alexa_report_state=_UNDEF): + async def async_update( + self, + *, + google_enabled=_UNDEF, + alexa_enabled=_UNDEF, + remote_enabled=_UNDEF, + google_secure_devices_pin=_UNDEF, + cloudhooks=_UNDEF, + cloud_user=_UNDEF, + google_entity_configs=_UNDEF, + alexa_entity_configs=_UNDEF, + alexa_report_state=_UNDEF, + ): """Update user preferences.""" for key, value in ( - (PREF_ENABLE_GOOGLE, google_enabled), - (PREF_ENABLE_ALEXA, alexa_enabled), - (PREF_ENABLE_REMOTE, remote_enabled), - (PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin), - (PREF_CLOUDHOOKS, cloudhooks), - (PREF_CLOUD_USER, cloud_user), - (PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs), - (PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs), - (PREF_ALEXA_REPORT_STATE, alexa_report_state), + (PREF_ENABLE_GOOGLE, google_enabled), + (PREF_ENABLE_ALEXA, alexa_enabled), + (PREF_ENABLE_REMOTE, remote_enabled), + (PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin), + (PREF_CLOUDHOOKS, cloudhooks), + (PREF_CLOUD_USER, cloud_user), + (PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs), + (PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs), + (PREF_ALEXA_REPORT_STATE, alexa_report_state), ): if value is not _UNDEF: self._prefs[key] = value @@ -82,23 +101,27 @@ class CloudPreferences: await self._store.async_save(self._prefs) for listener in self._listeners: - self._hass.async_create_task( - async_create_catching_coro(listener(self)) - ) + self._hass.async_create_task(async_create_catching_coro(listener(self))) async def async_update_google_entity_config( - self, *, entity_id, override_name=_UNDEF, disable_2fa=_UNDEF, - aliases=_UNDEF, should_expose=_UNDEF): + self, + *, + entity_id, + override_name=_UNDEF, + disable_2fa=_UNDEF, + aliases=_UNDEF, + should_expose=_UNDEF, + ): """Update config for a Google entity.""" entities = self.google_entity_configs entity = entities.get(entity_id, {}) changes = {} for key, value in ( - (PREF_OVERRIDE_NAME, override_name), - (PREF_DISABLE_2FA, disable_2fa), - (PREF_ALIASES, aliases), - (PREF_SHOULD_EXPOSE, should_expose), + (PREF_OVERRIDE_NAME, override_name), + (PREF_DISABLE_2FA, disable_2fa), + (PREF_ALIASES, aliases), + (PREF_SHOULD_EXPOSE, should_expose), ): if value is not _UNDEF: changes[key] = value @@ -106,42 +129,29 @@ class CloudPreferences: if not changes: return - updated_entity = { - **entity, - **changes, - } + updated_entity = {**entity, **changes} - updated_entities = { - **entities, - entity_id: updated_entity, - } + updated_entities = {**entities, entity_id: updated_entity} await self.async_update(google_entity_configs=updated_entities) async def async_update_alexa_entity_config( - self, *, entity_id, should_expose=_UNDEF): + self, *, entity_id, should_expose=_UNDEF + ): """Update config for an Alexa entity.""" entities = self.alexa_entity_configs entity = entities.get(entity_id, {}) changes = {} - for key, value in ( - (PREF_SHOULD_EXPOSE, should_expose), - ): + for key, value in ((PREF_SHOULD_EXPOSE, should_expose),): if value is not _UNDEF: changes[key] = value if not changes: return - updated_entity = { - **entity, - **changes, - } + updated_entity = {**entity, **changes} - updated_entities = { - **entities, - entity_id: updated_entity, - } + updated_entities = {**entities, entity_id: updated_entity} await self.async_update(alexa_entity_configs=updated_entities) def as_dict(self): @@ -179,8 +189,7 @@ class CloudPreferences: @property def alexa_report_state(self): """Return if Alexa report state is enabled.""" - return self._prefs.get(PREF_ALEXA_REPORT_STATE, - DEFAULT_ALEXA_REPORT_STATE) + return self._prefs.get(PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE) @property def google_enabled(self): @@ -215,11 +224,11 @@ class CloudPreferences: @property def _has_local_trusted_network(self) -> bool: """Return if we allow localhost to bypass auth.""" - local4 = ip_address('127.0.0.1') - local6 = ip_address('::1') + local4 = ip_address("127.0.0.1") + local6 = ip_address("::1") for prv in self._hass.auth.auth_providers: - if prv.type != 'trusted_networks': + if prv.type != "trusted_networks": continue for network in prv.trusted_networks: @@ -231,14 +240,15 @@ class CloudPreferences: @property def _has_local_trusted_proxies(self) -> bool: """Return if we allow localhost to be a proxy and use its data.""" - if not hasattr(self._hass, 'http'): + if not hasattr(self._hass, "http"): return False - local4 = ip_address('127.0.0.1') - local6 = ip_address('::1') + local4 = ip_address("127.0.0.1") + local6 = ip_address("::1") - if any(local4 in nwk or local6 in nwk - for nwk in self._hass.http.trusted_proxies): + if any( + local4 in nwk or local6 in nwk for nwk in self._hass.http.trusted_proxies + ): return True return False diff --git a/homeassistant/components/cloud/utils.py b/homeassistant/components/cloud/utils.py index 1d53681cbea..5040baada9a 100644 --- a/homeassistant/components/cloud/utils.py +++ b/homeassistant/components/cloud/utils.py @@ -14,12 +14,8 @@ def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]: # pylint: disable=protected-access body = body._value.decode(body.encoding) elif isinstance(body, bytes): - body = body.decode(response.charset or 'utf-8') + body = body.decode(response.charset or "utf-8") else: raise ValueError("Unknown payload encoding") - return { - 'status': response.status, - 'body': body, - 'headers': dict(response.headers), - } + return {"status": response.status, "body": body, "headers": dict(response.headers)} diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index ce88f820fe3..26feff069da 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -10,20 +10,25 @@ from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) -CONF_RECORDS = 'records' +CONF_RECORDS = "records" -DOMAIN = 'cloudflare' +DOMAIN = "cloudflare" INTERVAL = timedelta(minutes=60) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_ZONE): cv.string, - vol.Required(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]), - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_ZONE): cv.string, + vol.Required(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -45,8 +50,7 @@ def setup(hass, config): _update_cloudflare(cfupdate, email, key, zone, records) track_time_interval(hass, update_records_interval, INTERVAL) - hass.services.register( - DOMAIN, 'update_records', update_records_service) + hass.services.register(DOMAIN, "update_records", update_records_service) return True diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index 4f1dfc50536..a491bcc09ee 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -3,32 +3,56 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_SET) + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_PLAYLIST, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_SET, +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, STATE_OFF, STATE_PAUSED, - STATE_PLAYING) + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'cmus' +DEFAULT_NAME = "cmus" DEFAULT_PORT = 3000 -SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ - SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_PLAY +SUPPORT_CMUS = ( + SUPPORT_PAUSE + | SUPPORT_VOLUME_SET + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_PLAY_MEDIA + | SUPPORT_SEEK + | SUPPORT_PLAY +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Inclusive(CONF_HOST, 'remote'): cv.string, - vol.Inclusive(CONF_PASSWORD, 'remote'): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Inclusive(CONF_HOST, "remote"): cv.string, + vol.Inclusive(CONF_PASSWORD, "remote"): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discover_info=None): @@ -57,12 +81,11 @@ class CmusDevice(MediaPlayerDevice): from pycmus import remote if server: - self.cmus = remote.PyCmus( - server=server, password=password, port=port) - auto_name = 'cmus-{}'.format(server) + self.cmus = remote.PyCmus(server=server, password=password, port=port) + auto_name = "cmus-{}".format(server) else: self.cmus = remote.PyCmus() - auto_name = 'cmus-local' + auto_name = "cmus-local" self._name = name or auto_name self.status = {} @@ -82,16 +105,16 @@ class CmusDevice(MediaPlayerDevice): @property def state(self): """Return the media state.""" - if self.status.get('status') == 'playing': + if self.status.get("status") == "playing": return STATE_PLAYING - if self.status.get('status') == 'paused': + if self.status.get("status") == "paused": return STATE_PAUSED return STATE_OFF @property def media_content_id(self): """Content ID of current playing media.""" - return self.status.get('file') + return self.status.get("file") @property def content_type(self): @@ -101,43 +124,43 @@ class CmusDevice(MediaPlayerDevice): @property def media_duration(self): """Duration of current playing media in seconds.""" - return self.status.get('duration') + return self.status.get("duration") @property def media_title(self): """Title of current playing media.""" - return self.status['tag'].get('title') + return self.status["tag"].get("title") @property def media_artist(self): """Artist of current playing media, music track only.""" - return self.status['tag'].get('artist') + return self.status["tag"].get("artist") @property def media_track(self): """Track number of current playing media, music track only.""" - return self.status['tag'].get('tracknumber') + return self.status["tag"].get("tracknumber") @property def media_album_name(self): """Album name of current playing media, music track only.""" - return self.status['tag'].get('album') + return self.status["tag"].get("album") @property def media_album_artist(self): """Album artist of current playing media, music track only.""" - return self.status['tag'].get('albumartist') + return self.status["tag"].get("albumartist") @property def volume_level(self): """Return the volume level.""" - left = self.status['set'].get('vol_left')[0] - right = self.status['set'].get('vol_right')[0] + left = self.status["set"].get("vol_left")[0] + right = self.status["set"].get("vol_right")[0] if left != right: volume = float(left + right) / 2 else: volume = left - return int(volume)/100 + return int(volume) / 100 @property def supported_features(self): @@ -158,8 +181,8 @@ class CmusDevice(MediaPlayerDevice): def volume_up(self): """Set the volume up.""" - left = self.status['set'].get('vol_left') - right = self.status['set'].get('vol_right') + left = self.status["set"].get("vol_left") + right = self.status["set"].get("vol_right") if left != right: current_volume = float(left + right) / 2 else: @@ -170,8 +193,8 @@ class CmusDevice(MediaPlayerDevice): def volume_down(self): """Set the volume down.""" - left = self.status['set'].get('vol_left') - right = self.status['set'].get('vol_right') + left = self.status["set"].get("vol_left") + right = self.status["set"].get("vol_right") if left != right: current_volume = float(left + right) / 2 else: @@ -187,7 +210,10 @@ class CmusDevice(MediaPlayerDevice): else: _LOGGER.error( "Invalid media type %s. Only %s and %s are supported", - media_type, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST) + media_type, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_PLAYLIST, + ) def media_pause(self): """Send the pause command.""" diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 990521d0418..d881482ed1a 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -5,7 +5,11 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_TOKEN, CONF_LATITUDE, CONF_LONGITUDE) + ATTR_ATTRIBUTION, + CONF_TOKEN, + CONF_LATITUDE, + CONF_LONGITUDE, +) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity @@ -13,18 +17,22 @@ CONF_COUNTRY_CODE = "country_code" _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = 'Data provided by CO2signal' +ATTRIBUTION = "Data provided by CO2signal" -MSG_LOCATION = "Please use either coordinates or the country code. " \ - "For the coordinates, " \ - "you need to use both latitude and longitude." +MSG_LOCATION = ( + "Please use either coordinates or the country code. " + "For the coordinates, " + "you need to use both latitude and longitude." +) CO2_INTENSITY_UNIT = "CO2eq/kWh" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TOKEN): cv.string, - vol.Inclusive(CONF_LATITUDE, 'coords', msg=MSG_LOCATION): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, 'coords', msg=MSG_LOCATION): cv.longitude, - vol.Optional(CONF_COUNTRY_CODE): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_TOKEN): cv.string, + vol.Inclusive(CONF_LATITUDE, "coords", msg=MSG_LOCATION): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coords", msg=MSG_LOCATION): cv.longitude, + vol.Optional(CONF_COUNTRY_CODE): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -38,10 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devs = [] - devs.append(CO2Sensor(token, - country_code, - lat, - lon)) + devs.append(CO2Sensor(token, country_code, lat, lon)) add_entities(devs, True) @@ -59,11 +64,11 @@ class CO2Sensor(Entity): if country_code is not None: device_name = country_code else: - device_name = '{lat}/{lon}'\ - .format(lat=round(self._latitude, 2), - lon=round(self._longitude, 2)) + device_name = "{lat}/{lon}".format( + lat=round(self._latitude, 2), lon=round(self._longitude, 2) + ) - self._friendly_name = 'CO2 intensity - {}'.format(device_name) + self._friendly_name = "CO2 intensity - {}".format(device_name) @property def name(self): @@ -73,7 +78,7 @@ class CO2Sensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return 'mdi:periodic-table-co2' + return "mdi:periodic-table-co2" @property def state(self): @@ -98,10 +103,11 @@ class CO2Sensor(Entity): if self._country_code is not None: self._data = CO2Signal.get_latest_carbon_intensity( - self._token, country_code=self._country_code) + self._token, country_code=self._country_code + ) else: self._data = CO2Signal.get_latest_carbon_intensity( - self._token, - latitude=self._latitude, longitude=self._longitude) + self._token, latitude=self._latitude, longitude=self._longitude + ) self._data = round(self._data, 2) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 21efd5f9b8e..6eca0616ca8 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -11,26 +11,33 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -DOMAIN = 'coinbase' +DOMAIN = "coinbase" -CONF_API_SECRET = 'api_secret' -CONF_ACCOUNT_CURRENCIES = 'account_balance_currencies' -CONF_EXCHANGE_CURRENCIES = 'exchange_rate_currencies' +CONF_API_SECRET = "api_secret" +CONF_ACCOUNT_CURRENCIES = "account_balance_currencies" +CONF_EXCHANGE_CURRENCIES = "exchange_rate_currencies" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -DATA_COINBASE = 'coinbase_cache' +DATA_COINBASE = "coinbase_cache" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_API_SECRET): cv.string, - vol.Optional(CONF_ACCOUNT_CURRENCIES): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]): - vol.All(cv.ensure_list, [cv.string]) - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_API_SECRET): cv.string, + vol.Optional(CONF_ACCOUNT_CURRENCIES): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -44,30 +51,25 @@ def setup(hass, config): account_currencies = config[DOMAIN].get(CONF_ACCOUNT_CURRENCIES) exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES) - hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData( - api_key, api_secret) + hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(api_key, api_secret) - if not hasattr(coinbase_data, 'accounts'): + if not hasattr(coinbase_data, "accounts"): return False for account in coinbase_data.accounts.data: - if (account_currencies is None or - account.currency in account_currencies): - load_platform(hass, - 'sensor', - DOMAIN, - {'account': account}, - config) + if account_currencies is None or account.currency in account_currencies: + load_platform(hass, "sensor", DOMAIN, {"account": account}, config) for currency in exchange_currencies: if currency not in coinbase_data.exchange_rates.rates: _LOGGER.warning("Currency %s not found", currency) continue native = coinbase_data.exchange_rates.currency - load_platform(hass, - 'sensor', - DOMAIN, - {'native_currency': native, - 'exchange_currency': currency}, - config) + load_platform( + hass, + "sensor", + DOMAIN, + {"native_currency": native, "exchange_currency": currency}, + config, + ) return True @@ -78,6 +80,7 @@ class CoinbaseData: def __init__(self, api_key, api_secret): """Init the coinbase data object.""" from coinbase.wallet.client import Client + self.client = Client(api_key, api_secret) self.update() @@ -85,9 +88,11 @@ class CoinbaseData: def update(self): """Get the latest data from coinbase.""" from coinbase.wallet.error import AuthenticationError + try: self.accounts = self.client.get_accounts() self.exchange_rates = self.client.get_exchange_rates() except AuthenticationError as coinbase_error: - _LOGGER.error("Authentication error connecting" - " to coinbase: %s", coinbase_error) + _LOGGER.error( + "Authentication error connecting" " to coinbase: %s", coinbase_error + ) diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 9470999efbb..c5f53ef609d 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -5,33 +5,35 @@ from homeassistant.helpers.entity import Entity ATTR_NATIVE_BALANCE = "Balance in native currency" CURRENCY_ICONS = { - 'BTC': 'mdi:currency-btc', - 'ETH': 'mdi:currency-eth', - 'EUR': 'mdi:currency-eur', - 'LTC': 'mdi:litecoin', - 'USD': 'mdi:currency-usd' + "BTC": "mdi:currency-btc", + "ETH": "mdi:currency-eth", + "EUR": "mdi:currency-eur", + "LTC": "mdi:litecoin", + "USD": "mdi:currency-usd", } -DEFAULT_COIN_ICON = 'mdi:coin' +DEFAULT_COIN_ICON = "mdi:coin" ATTRIBUTION = "Data provided by coinbase.com" -DATA_COINBASE = 'coinbase_cache' +DATA_COINBASE = "coinbase_cache" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Coinbase sensors.""" if discovery_info is None: return - if 'account' in discovery_info: - account = discovery_info['account'] + if "account" in discovery_info: + account = discovery_info["account"] sensor = AccountSensor( - hass.data[DATA_COINBASE], account['name'], - account['balance']['currency']) - if 'exchange_currency' in discovery_info: + hass.data[DATA_COINBASE], account["name"], account["balance"]["currency"] + ) + if "exchange_currency" in discovery_info: sensor = ExchangeRateSensor( - hass.data[DATA_COINBASE], discovery_info['exchange_currency'], - discovery_info['native_currency']) + hass.data[DATA_COINBASE], + discovery_info["exchange_currency"], + discovery_info["native_currency"], + ) add_entities([sensor], True) @@ -74,17 +76,18 @@ class AccountSensor(Entity): return { ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_NATIVE_BALANCE: "{} {}".format( - self._native_balance, self._native_currency), + self._native_balance, self._native_currency + ), } def update(self): """Get the latest state of the sensor.""" self._coinbase_data.update() - for account in self._coinbase_data.accounts['data']: - if self._name == "Coinbase {}".format(account['name']): - self._state = account['balance']['amount'] - self._native_balance = account['native_balance']['amount'] - self._native_currency = account['native_balance']['currency'] + for account in self._coinbase_data.accounts["data"]: + if self._name == "Coinbase {}".format(account["name"]): + self._state = account["balance"]["amount"] + self._native_balance = account["native_balance"]["amount"] + self._native_currency = account["native_balance"]["currency"] class ExchangeRateSensor(Entity): @@ -121,9 +124,7 @@ class ExchangeRateSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION - } + return {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Get the latest state of the sensor.""" diff --git a/homeassistant/components/coinmarketcap/sensor.py b/homeassistant/components/coinmarketcap/sensor.py index 4d8af5a721d..fbe05187684 100644 --- a/homeassistant/components/coinmarketcap/sensor.py +++ b/homeassistant/components/coinmarketcap/sensor.py @@ -7,47 +7,48 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_DISPLAY_CURRENCY) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_DISPLAY_CURRENCY from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_VOLUME_24H = 'volume_24h' -ATTR_AVAILABLE_SUPPLY = 'available_supply' -ATTR_CIRCULATING_SUPPLY = 'circulating_supply' -ATTR_MARKET_CAP = 'market_cap' -ATTR_PERCENT_CHANGE_24H = 'percent_change_24h' -ATTR_PERCENT_CHANGE_7D = 'percent_change_7d' -ATTR_PERCENT_CHANGE_1H = 'percent_change_1h' -ATTR_PRICE = 'price' -ATTR_RANK = 'rank' -ATTR_SYMBOL = 'symbol' -ATTR_TOTAL_SUPPLY = 'total_supply' +ATTR_VOLUME_24H = "volume_24h" +ATTR_AVAILABLE_SUPPLY = "available_supply" +ATTR_CIRCULATING_SUPPLY = "circulating_supply" +ATTR_MARKET_CAP = "market_cap" +ATTR_PERCENT_CHANGE_24H = "percent_change_24h" +ATTR_PERCENT_CHANGE_7D = "percent_change_7d" +ATTR_PERCENT_CHANGE_1H = "percent_change_1h" +ATTR_PRICE = "price" +ATTR_RANK = "rank" +ATTR_SYMBOL = "symbol" +ATTR_TOTAL_SUPPLY = "total_supply" ATTRIBUTION = "Data provided by CoinMarketCap" -CONF_CURRENCY_ID = 'currency_id' -CONF_DISPLAY_CURRENCY_DECIMALS = 'display_currency_decimals' +CONF_CURRENCY_ID = "currency_id" +CONF_DISPLAY_CURRENCY_DECIMALS = "display_currency_decimals" DEFAULT_CURRENCY_ID = 1 -DEFAULT_DISPLAY_CURRENCY = 'USD' +DEFAULT_DISPLAY_CURRENCY = "USD" DEFAULT_DISPLAY_CURRENCY_DECIMALS = 2 -ICON = 'mdi:currency-usd' +ICON = "mdi:currency-usd" SCAN_INTERVAL = timedelta(minutes=15) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_CURRENCY_ID, default=DEFAULT_CURRENCY_ID): - cv.positive_int, - vol.Optional(CONF_DISPLAY_CURRENCY, default=DEFAULT_DISPLAY_CURRENCY): - cv.string, - vol.Optional(CONF_DISPLAY_CURRENCY_DECIMALS, - default=DEFAULT_DISPLAY_CURRENCY_DECIMALS): - vol.All(vol.Coerce(int), vol.Range(min=1)), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_CURRENCY_ID, default=DEFAULT_CURRENCY_ID): cv.positive_int, + vol.Optional( + CONF_DISPLAY_CURRENCY, default=DEFAULT_DISPLAY_CURRENCY + ): cv.string, + vol.Optional( + CONF_DISPLAY_CURRENCY_DECIMALS, default=DEFAULT_DISPLAY_CURRENCY_DECIMALS + ): vol.All(vol.Coerce(int), vol.Range(min=1)), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -59,15 +60,25 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: CoinMarketCapData(currency_id, display_currency).update() except HTTPError: - _LOGGER.warning("Currency ID %s or display currency %s " - "is not available. Using 1 (bitcoin) " - "and USD.", currency_id, display_currency) + _LOGGER.warning( + "Currency ID %s or display currency %s " + "is not available. Using 1 (bitcoin) " + "and USD.", + currency_id, + display_currency, + ) currency_id = DEFAULT_CURRENCY_ID display_currency = DEFAULT_DISPLAY_CURRENCY - add_entities([CoinMarketCapSensor( - CoinMarketCapData( - currency_id, display_currency), display_currency_decimals)], True) + add_entities( + [ + CoinMarketCapSensor( + CoinMarketCapData(currency_id, display_currency), + display_currency_decimals, + ) + ], + True, + ) class CoinMarketCapSensor(Entity): @@ -83,14 +94,17 @@ class CoinMarketCapSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return self._ticker.get('name') + return self._ticker.get("name") @property def state(self): """Return the state of the sensor.""" - return round(float( - self._ticker.get('quotes').get(self.data.display_currency) - .get('price')), self.display_currency_decimals) + return round( + float( + self._ticker.get("quotes").get(self.data.display_currency).get("price") + ), + self.display_currency_decimals, + ) @property def unit_of_measurement(self): @@ -106,32 +120,32 @@ class CoinMarketCapSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_VOLUME_24H: - self._ticker.get('quotes').get(self.data.display_currency) - .get('volume_24h'), + ATTR_VOLUME_24H: self._ticker.get("quotes") + .get(self.data.display_currency) + .get("volume_24h"), ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_CIRCULATING_SUPPLY: self._ticker.get('circulating_supply'), - ATTR_MARKET_CAP: - self._ticker.get('quotes').get(self.data.display_currency) - .get('market_cap'), - ATTR_PERCENT_CHANGE_24H: - self._ticker.get('quotes').get(self.data.display_currency) - .get('percent_change_24h'), - ATTR_PERCENT_CHANGE_7D: - self._ticker.get('quotes').get(self.data.display_currency) - .get('percent_change_7d'), - ATTR_PERCENT_CHANGE_1H: - self._ticker.get('quotes').get(self.data.display_currency) - .get('percent_change_1h'), - ATTR_RANK: self._ticker.get('rank'), - ATTR_SYMBOL: self._ticker.get('symbol'), - ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'), + ATTR_CIRCULATING_SUPPLY: self._ticker.get("circulating_supply"), + ATTR_MARKET_CAP: self._ticker.get("quotes") + .get(self.data.display_currency) + .get("market_cap"), + ATTR_PERCENT_CHANGE_24H: self._ticker.get("quotes") + .get(self.data.display_currency) + .get("percent_change_24h"), + ATTR_PERCENT_CHANGE_7D: self._ticker.get("quotes") + .get(self.data.display_currency) + .get("percent_change_7d"), + ATTR_PERCENT_CHANGE_1H: self._ticker.get("quotes") + .get(self.data.display_currency) + .get("percent_change_1h"), + ATTR_RANK: self._ticker.get("rank"), + ATTR_SYMBOL: self._ticker.get("symbol"), + ATTR_TOTAL_SUPPLY: self._ticker.get("total_supply"), } def update(self): """Get the latest data and updates the states.""" self.data.update() - self._ticker = self.data.ticker.get('data') + self._ticker = self.data.ticker.get("data") class CoinMarketCapData: @@ -146,5 +160,5 @@ class CoinMarketCapData: def update(self): """Get the latest data from coinmarketcap.com.""" from coinmarketcap import Market - self.ticker = Market().ticker( - self.currency_id, convert=self.display_currency) + + self.ticker = Market().ticker(self.currency_id, convert=self.display_currency) diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 3c06bc0c2d7..90830d52236 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -9,52 +9,58 @@ import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -_RESOURCE = 'https://hourlypricing.comed.com/api' +_RESOURCE = "https://hourlypricing.comed.com/api" SCAN_INTERVAL = timedelta(minutes=5) ATTRIBUTION = "Data provided by ComEd Hourly Pricing service" -CONF_CURRENT_HOUR_AVERAGE = 'current_hour_average' -CONF_FIVE_MINUTE = 'five_minute' -CONF_MONITORED_FEEDS = 'monitored_feeds' -CONF_SENSOR_TYPE = 'type' +CONF_CURRENT_HOUR_AVERAGE = "current_hour_average" +CONF_FIVE_MINUTE = "five_minute" +CONF_MONITORED_FEEDS = "monitored_feeds" +CONF_SENSOR_TYPE = "type" SENSOR_TYPES = { - CONF_FIVE_MINUTE: ['ComEd 5 Minute Price', 'c'], - CONF_CURRENT_HOUR_AVERAGE: ['ComEd Current Hour Average Price', 'c'], + CONF_FIVE_MINUTE: ["ComEd 5 Minute Price", "c"], + CONF_CURRENT_HOUR_AVERAGE: ["ComEd Current Hour Average Price", "c"], } TYPES_SCHEMA = vol.In(SENSOR_TYPES) -SENSORS_SCHEMA = vol.Schema({ - vol.Required(CONF_SENSOR_TYPE): TYPES_SCHEMA, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OFFSET, default=0.0): vol.Coerce(float), -}) +SENSORS_SCHEMA = vol.Schema( + { + vol.Required(CONF_SENSOR_TYPE): TYPES_SCHEMA, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_OFFSET, default=0.0): vol.Coerce(float), + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_FEEDS): [SENSORS_SCHEMA], -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_MONITORED_FEEDS): [SENSORS_SCHEMA]} +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the ComEd Hourly Pricing sensor.""" websession = async_get_clientsession(hass) dev = [] for variable in config[CONF_MONITORED_FEEDS]: - dev.append(ComedHourlyPricingSensor( - hass.loop, websession, variable[CONF_SENSOR_TYPE], - variable[CONF_OFFSET], variable.get(CONF_NAME))) + dev.append( + ComedHourlyPricingSensor( + hass.loop, + websession, + variable[CONF_SENSOR_TYPE], + variable[CONF_OFFSET], + variable.get(CONF_NAME), + ) + ) async_add_entities(dev, True) @@ -98,21 +104,19 @@ class ComedHourlyPricingSensor(Entity): async def async_update(self): """Get the ComEd Hourly Pricing data from the web service.""" try: - if self.type == CONF_FIVE_MINUTE or \ - self.type == CONF_CURRENT_HOUR_AVERAGE: + if self.type == CONF_FIVE_MINUTE or self.type == CONF_CURRENT_HOUR_AVERAGE: url_string = _RESOURCE if self.type == CONF_FIVE_MINUTE: - url_string += '?type=5minutefeed' + url_string += "?type=5minutefeed" else: - url_string += '?type=currenthouraverage' + url_string += "?type=currenthouraverage" with async_timeout.timeout(60): response = await self.websession.get(url_string) # The API responds with MIME type 'text/html' text = await response.text() data = json.loads(text) - self._state = round( - float(data[0]['price']) + self.offset, 2) + self._state = round(float(data[0]["price"]) + self.offset, 2) else: self._state = None diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index 3c50f3fb723..22e9d95bbd8 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -4,48 +4,59 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PIN, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP) + CONF_HOST, + CONF_NAME, + CONF_PIN, + CONF_TOKEN, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send _LOGGER = logging.getLogger(__name__) -DOMAIN = 'comfoconnect' +DOMAIN = "comfoconnect" -SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = 'comfoconnect_update_received' +SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = "comfoconnect_update_received" -ATTR_CURRENT_TEMPERATURE = 'current_temperature' -ATTR_CURRENT_HUMIDITY = 'current_humidity' -ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature' -ATTR_OUTSIDE_HUMIDITY = 'outside_humidity' -ATTR_AIR_FLOW_SUPPLY = 'air_flow_supply' -ATTR_AIR_FLOW_EXHAUST = 'air_flow_exhaust' +ATTR_CURRENT_TEMPERATURE = "current_temperature" +ATTR_CURRENT_HUMIDITY = "current_humidity" +ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" +ATTR_OUTSIDE_HUMIDITY = "outside_humidity" +ATTR_AIR_FLOW_SUPPLY = "air_flow_supply" +ATTR_AIR_FLOW_EXHAUST = "air_flow_exhaust" -CONF_USER_AGENT = 'user_agent' +CONF_USER_AGENT = "user_agent" -DEFAULT_NAME = 'ComfoAirQ' +DEFAULT_NAME = "ComfoAirQ" DEFAULT_PIN = 0 -DEFAULT_TOKEN = '00000000000000000000000000000001' -DEFAULT_USER_AGENT = 'Home Assistant' +DEFAULT_TOKEN = "00000000000000000000000000000001" +DEFAULT_USER_AGENT = "Home Assistant" DEVICE = None -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TOKEN, default=DEFAULT_TOKEN): - vol.Length(min=32, max=32, msg='invalid token'), - vol.Optional(CONF_USER_AGENT, default=DEFAULT_USER_AGENT): cv.string, - vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TOKEN, default=DEFAULT_TOKEN): vol.Length( + min=32, max=32, msg="invalid token" + ), + vol.Optional(CONF_USER_AGENT, default=DEFAULT_USER_AGENT): cv.string, + vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): """Set up the ComfoConnect bridge.""" - from pycomfoconnect import (Bridge) + from pycomfoconnect import Bridge conf = config[DOMAIN] host = conf.get(CONF_HOST) @@ -76,7 +87,7 @@ def setup(hass, config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) # Load platforms - discovery.load_platform(hass, 'fan', DOMAIN, {}, config) + discovery.load_platform(hass, "fan", DOMAIN, {}, config) return True @@ -86,15 +97,18 @@ class ComfoConnectBridge: def __init__(self, hass, bridge, name, token, friendly_name, pin): """Initialize the ComfoConnect bridge.""" - from pycomfoconnect import (ComfoConnect) + from pycomfoconnect import ComfoConnect self.data = {} self.name = name self.hass = hass self.comfoconnect = ComfoConnect( - bridge=bridge, local_uuid=bytes.fromhex(token), - local_devicename=friendly_name, pin=pin) + bridge=bridge, + local_uuid=bytes.fromhex(token), + local_devicename=friendly_name, + pin=pin, + ) self.comfoconnect.callback_sensor = self.sensor_callback def connect(self): @@ -112,7 +126,9 @@ class ComfoConnectBridge: _LOGGER.debug("Got value from bridge: %d = %d", var, value) from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR) + SENSOR_TEMPERATURE_EXTRACT, + SENSOR_TEMPERATURE_OUTDOOR, + ) if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]: self.data[var] = value / 10 diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 56175f0bca0..6c90ab8cba1 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -2,20 +2,20 @@ import logging from homeassistant.components.fan import ( - SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, - FanEntity) + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_SET_SPEED, + FanEntity, +) from homeassistant.helpers.dispatcher import dispatcher_connect from . import DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge _LOGGER = logging.getLogger(__name__) -SPEED_MAPPING = { - 0: SPEED_OFF, - 1: SPEED_LOW, - 2: SPEED_MEDIUM, - 3: SPEED_HIGH -} +SPEED_MAPPING = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -44,8 +44,7 @@ class ComfoConnectFan(FanEntity): self.schedule_update_ha_state() # Register for dispatcher updates - dispatcher_connect( - hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) + dispatcher_connect(hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) @property def name(self): @@ -55,7 +54,7 @@ class ComfoConnectFan(FanEntity): @property def icon(self): """Return the icon to use in the frontend.""" - return 'mdi:air-conditioner' + return "mdi:air-conditioner" @property def supported_features(self) -> int: @@ -65,7 +64,7 @@ class ComfoConnectFan(FanEntity): @property def speed(self): """Return the current fan mode.""" - from pycomfoconnect import (SENSOR_FAN_SPEED_MODE) + from pycomfoconnect import SENSOR_FAN_SPEED_MODE try: speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] @@ -90,11 +89,14 @@ class ComfoConnectFan(FanEntity): def set_speed(self, speed: str): """Set fan speed.""" - _LOGGER.debug('Changing fan speed to %s.', speed) + _LOGGER.debug("Changing fan speed to %s.", speed) from pycomfoconnect import ( - CMD_FAN_MODE_AWAY, CMD_FAN_MODE_LOW, CMD_FAN_MODE_MEDIUM, - CMD_FAN_MODE_HIGH) + CMD_FAN_MODE_AWAY, + CMD_FAN_MODE_LOW, + CMD_FAN_MODE_MEDIUM, + CMD_FAN_MODE_HIGH, + ) if speed == SPEED_OFF: self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY) diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index db2a9393e2b..4099804d413 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -6,9 +6,16 @@ from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.helpers.entity import Entity from . import ( - ATTR_AIR_FLOW_EXHAUST, ATTR_AIR_FLOW_SUPPLY, ATTR_CURRENT_HUMIDITY, - ATTR_CURRENT_TEMPERATURE, ATTR_OUTSIDE_HUMIDITY, ATTR_OUTSIDE_TEMPERATURE, - DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge) + ATTR_AIR_FLOW_EXHAUST, + ATTR_AIR_FLOW_SUPPLY, + ATTR_CURRENT_HUMIDITY, + ATTR_CURRENT_TEMPERATURE, + ATTR_OUTSIDE_HUMIDITY, + ATTR_OUTSIDE_TEMPERATURE, + DOMAIN, + SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, + ComfoConnectBridge, +) _LOGGER = logging.getLogger(__name__) @@ -18,47 +25,51 @@ SENSOR_TYPES = {} def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ComfoConnect fan platform.""" from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, SENSOR_HUMIDITY_EXTRACT, - SENSOR_TEMPERATURE_OUTDOOR, SENSOR_HUMIDITY_OUTDOOR, - SENSOR_FAN_SUPPLY_FLOW, SENSOR_FAN_EXHAUST_FLOW) + SENSOR_TEMPERATURE_EXTRACT, + SENSOR_HUMIDITY_EXTRACT, + SENSOR_TEMPERATURE_OUTDOOR, + SENSOR_HUMIDITY_OUTDOOR, + SENSOR_FAN_SUPPLY_FLOW, + SENSOR_FAN_EXHAUST_FLOW, + ) global SENSOR_TYPES SENSOR_TYPES = { ATTR_CURRENT_TEMPERATURE: [ - 'Inside Temperature', + "Inside Temperature", TEMP_CELSIUS, - 'mdi:thermometer', - SENSOR_TEMPERATURE_EXTRACT + "mdi:thermometer", + SENSOR_TEMPERATURE_EXTRACT, ], ATTR_CURRENT_HUMIDITY: [ - 'Inside Humidity', - '%', - 'mdi:water-percent', - SENSOR_HUMIDITY_EXTRACT + "Inside Humidity", + "%", + "mdi:water-percent", + SENSOR_HUMIDITY_EXTRACT, ], ATTR_OUTSIDE_TEMPERATURE: [ - 'Outside Temperature', + "Outside Temperature", TEMP_CELSIUS, - 'mdi:thermometer', - SENSOR_TEMPERATURE_OUTDOOR + "mdi:thermometer", + SENSOR_TEMPERATURE_OUTDOOR, ], ATTR_OUTSIDE_HUMIDITY: [ - 'Outside Humidity', - '%', - 'mdi:water-percent', - SENSOR_HUMIDITY_OUTDOOR + "Outside Humidity", + "%", + "mdi:water-percent", + SENSOR_HUMIDITY_OUTDOOR, ], ATTR_AIR_FLOW_SUPPLY: [ - 'Supply airflow', - 'm³/h', - 'mdi:air-conditioner', - SENSOR_FAN_SUPPLY_FLOW + "Supply airflow", + "m³/h", + "mdi:air-conditioner", + SENSOR_FAN_SUPPLY_FLOW, ], ATTR_AIR_FLOW_EXHAUST: [ - 'Exhaust airflow', - 'm³/h', - 'mdi:air-conditioner', - SENSOR_FAN_EXHAUST_FLOW + "Exhaust airflow", + "m³/h", + "mdi:air-conditioner", + SENSOR_FAN_EXHAUST_FLOW, ], } @@ -69,8 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_type = resource.lower() if sensor_type not in SENSOR_TYPES: - _LOGGER.warning("Sensor type: %s is not a valid sensor.", - sensor_type) + _LOGGER.warning("Sensor type: %s is not a valid sensor.", sensor_type) continue sensors.append( @@ -78,7 +88,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass, name="%s %s" % (ccb.name, SENSOR_TYPES[sensor_type][0]), ccb=ccb, - sensor_type=sensor_type + sensor_type=sensor_type, ) ) @@ -88,8 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ComfoConnectSensor(Entity): """Representation of a ComfoConnect sensor.""" - def __init__(self, hass, name, ccb: ComfoConnectBridge, - sensor_type) -> None: + def __init__(self, hass, name, ccb: ComfoConnectBridge, sensor_type) -> None: """Initialize the ComfoConnect sensor.""" self._ccb = ccb self._sensor_type = sensor_type @@ -101,12 +110,11 @@ class ComfoConnectSensor(Entity): def _handle_update(var): if var == self._sensor_id: - _LOGGER.debug('Dispatcher update for %s.', var) + _LOGGER.debug("Dispatcher update for %s.", var) self.schedule_update_ha_state() # Register for dispatcher updates - dispatcher_connect( - hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) + dispatcher_connect(hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) @property def state(self): diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 860367d8091..eaa371be1a3 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -5,35 +5,44 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) + DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorDevice, +) from homeassistant.const import ( - CONF_COMMAND, CONF_DEVICE_CLASS, CONF_NAME, CONF_PAYLOAD_OFF, - CONF_PAYLOAD_ON, CONF_VALUE_TEMPLATE) + CONF_COMMAND, + CONF_DEVICE_CLASS, + CONF_NAME, + CONF_PAYLOAD_OFF, + CONF_PAYLOAD_ON, + CONF_VALUE_TEMPLATE, +) import homeassistant.helpers.config_validation as cv from .sensor import CommandSensorData _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Binary Command Sensor' -DEFAULT_PAYLOAD_ON = 'ON' -DEFAULT_PAYLOAD_OFF = 'OFF' +DEFAULT_NAME = "Binary Command Sensor" +DEFAULT_PAYLOAD_ON = "ON" +DEFAULT_PAYLOAD_OFF = "OFF" SCAN_INTERVAL = timedelta(seconds=60) -CONF_COMMAND_TIMEOUT = 'command_timeout' +CONF_COMMAND_TIMEOUT = "command_timeout" DEFAULT_TIMEOUT = 15 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COMMAND): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, - vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional( - CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_COMMAND): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -49,16 +58,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None): value_template.hass = hass data = CommandSensorData(hass, command, command_timeout) - add_entities([CommandBinarySensor( - hass, data, name, device_class, payload_on, payload_off, - value_template)], True) + add_entities( + [ + CommandBinarySensor( + hass, data, name, device_class, payload_on, payload_off, value_template + ) + ], + True, + ) class CommandBinarySensor(BinarySensorDevice): """Representation of a command line binary sensor.""" - def __init__(self, hass, data, name, device_class, payload_on, - payload_off, value_template): + def __init__( + self, hass, data, name, device_class, payload_on, payload_off, value_template + ): """Initialize the Command line binary sensor.""" self._hass = hass self.data = data @@ -79,7 +94,7 @@ class CommandBinarySensor(BinarySensorDevice): """Return true if the binary sensor is on.""" return self._state - @ property + @property def device_class(self): """Return the class of the binary sensor.""" return self._device_class @@ -90,8 +105,7 @@ class CommandBinarySensor(BinarySensorDevice): value = self.data.value if self._value_template is not None: - value = self._value_template.render_with_possible_json_value( - value, False) + value = self._value_template.render_with_possible_json_value(value, False) if value == self._payload_on: self._state = True elif value == self._payload_off: diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 7f3c5279905..c4413e78a00 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -4,26 +4,34 @@ import subprocess import voluptuous as vol -from homeassistant.components.cover import (CoverDevice, PLATFORM_SCHEMA) +from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA from homeassistant.const import ( - CONF_COMMAND_CLOSE, CONF_COMMAND_OPEN, CONF_COMMAND_STATE, - CONF_COMMAND_STOP, CONF_COVERS, CONF_VALUE_TEMPLATE, CONF_FRIENDLY_NAME) + CONF_COMMAND_CLOSE, + CONF_COMMAND_OPEN, + CONF_COMMAND_STATE, + CONF_COMMAND_STOP, + CONF_COVERS, + CONF_VALUE_TEMPLATE, + CONF_FRIENDLY_NAME, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -COVER_SCHEMA = vol.Schema({ - vol.Optional(CONF_COMMAND_CLOSE, default='true'): cv.string, - vol.Optional(CONF_COMMAND_OPEN, default='true'): cv.string, - vol.Optional(CONF_COMMAND_STATE): cv.string, - vol.Optional(CONF_COMMAND_STOP, default='true'): cv.string, - vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, -}) +COVER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_COMMAND_CLOSE, default="true"): cv.string, + vol.Optional(CONF_COMMAND_OPEN, default="true"): cv.string, + vol.Optional(CONF_COMMAND_STATE): cv.string, + vol.Optional(CONF_COMMAND_STOP, default="true"): cv.string, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA)} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -58,8 +66,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class CommandCover(CoverDevice): """Representation a command line cover.""" - def __init__(self, hass, name, command_open, command_close, command_stop, - command_state, value_template): + def __init__( + self, + hass, + name, + command_open, + command_close, + command_stop, + command_state, + value_template, + ): """Initialize the cover.""" self._hass = hass self._name = name @@ -75,7 +91,7 @@ class CommandCover(CoverDevice): """Execute the actual commands.""" _LOGGER.info("Running command: %s", command) - success = (subprocess.call(command, shell=True) == 0) + success = subprocess.call(command, shell=True) == 0 if not success: _LOGGER.error("Command failed: %s", command) @@ -89,7 +105,7 @@ class CommandCover(CoverDevice): try: return_value = subprocess.check_output(command, shell=True) - return return_value.strip().decode('utf-8') + return return_value.strip().decode("utf-8") except subprocess.CalledProcessError: _LOGGER.error("Command failed: %s", command) @@ -129,8 +145,7 @@ class CommandCover(CoverDevice): if self._command_state: payload = str(self._query_state()) if self._value_template: - payload = self._value_template.render_with_possible_json_value( - payload) + payload = self._value_template.render_with_possible_json_value(payload) self._state = int(payload) def open_cover(self, **kwargs): diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 941be72aa81..e2581c8f065 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -7,15 +7,13 @@ import voluptuous as vol from homeassistant.const import CONF_COMMAND, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COMMAND): cv.string, - vol.Optional(CONF_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_COMMAND): cv.string, vol.Optional(CONF_NAME): cv.string} +) def get_service(hass, config, discovery_info=None): @@ -35,8 +33,9 @@ class CommandLineNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a command line.""" try: - proc = subprocess.Popen(self.command, universal_newlines=True, - stdin=subprocess.PIPE, shell=True) + proc = subprocess.Popen( + self.command, universal_newlines=True, stdin=subprocess.PIPE, shell=True + ) proc.communicate(input=message) if proc.returncode != 0: _LOGGER.error("Command failed: %s", self.command) diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 587cfe53d3c..85ba78ecd98 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -10,8 +10,12 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_COMMAND, CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, - STATE_UNKNOWN) + CONF_COMMAND, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, + STATE_UNKNOWN, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv @@ -19,23 +23,24 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_COMMAND_TIMEOUT = 'command_timeout' -CONF_JSON_ATTRIBUTES = 'json_attributes' +CONF_COMMAND_TIMEOUT = "command_timeout" +CONF_JSON_ATTRIBUTES = "json_attributes" -DEFAULT_NAME = 'Command Sensor' +DEFAULT_NAME = "Command Sensor" DEFAULT_TIMEOUT = 15 SCAN_INTERVAL = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COMMAND): cv.string, - vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): - cv.positive_int, - vol.Optional(CONF_JSON_ATTRIBUTES): cv.ensure_list_csv, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_COMMAND): cv.string, + vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_JSON_ATTRIBUTES): cv.ensure_list_csv, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -50,15 +55,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): json_attributes = config.get(CONF_JSON_ATTRIBUTES) data = CommandSensorData(hass, command, command_timeout) - add_entities([CommandSensor( - hass, data, name, unit, value_template, json_attributes)], True) + add_entities( + [CommandSensor(hass, data, name, unit, value_template, json_attributes)], True + ) class CommandSensor(Entity): """Representation of a sensor that is using shell commands.""" - def __init__(self, hass, data, name, unit_of_measurement, value_template, - json_attributes): + def __init__( + self, hass, data, name, unit_of_measurement, value_template, json_attributes + ): """Initialize the sensor.""" self._hass = hass self.data = data @@ -100,14 +107,15 @@ class CommandSensor(Entity): try: json_dict = json.loads(value) if isinstance(json_dict, collections.Mapping): - self._attributes = {k: json_dict[k] for k in - self._json_attributes - if k in json_dict} + self._attributes = { + k: json_dict[k] + for k in self._json_attributes + if k in json_dict + } else: _LOGGER.warning("JSON result was not a dictionary") except ValueError: - _LOGGER.warning( - "Unable to parse output as JSON: %s", value) + _LOGGER.warning("Unable to parse output as JSON: %s", value) else: _LOGGER.warning("Empty reply found when expecting JSON data") @@ -115,7 +123,8 @@ class CommandSensor(Entity): value = STATE_UNKNOWN elif self._value_template is not None: self._state = self._value_template.render_with_possible_json_value( - value, STATE_UNKNOWN) + value, STATE_UNKNOWN + ) else: self._state = value @@ -137,13 +146,13 @@ class CommandSensorData: if command in cache: prog, args, args_compiled = cache[command] - elif ' ' not in command: + elif " " not in command: prog = command args = None args_compiled = None cache[command] = (prog, args, args_compiled) else: - prog, args = command.split(' ', 1) + prog, args = command.split(" ", 1) args_compiled = template.Template(args, self.hass) cache[command] = (prog, args, args_compiled) @@ -162,13 +171,14 @@ class CommandSensorData: shell = True else: # Template used. Construct the string used in the shell - command = str(' '.join([prog] + shlex.split(rendered_args))) + command = str(" ".join([prog] + shlex.split(rendered_args))) shell = True try: _LOGGER.debug("Running command: %s", command) return_value = subprocess.check_output( - command, shell=shell, timeout=self.timeout) - self.value = return_value.strip().decode('utf-8') + command, shell=shell, timeout=self.timeout + ) + self.value = return_value.strip().decode("utf-8") except subprocess.CalledProcessError: _LOGGER.error("Command failed: %s", command) except subprocess.TimeoutExpired: diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 8d97198ad66..937e859197a 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -7,24 +7,34 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.switch import ( - SwitchDevice, PLATFORM_SCHEMA, ENTITY_ID_FORMAT) + SwitchDevice, + PLATFORM_SCHEMA, + ENTITY_ID_FORMAT, +) from homeassistant.const import ( - CONF_FRIENDLY_NAME, CONF_SWITCHES, CONF_VALUE_TEMPLATE, CONF_COMMAND_OFF, - CONF_COMMAND_ON, CONF_COMMAND_STATE) + CONF_FRIENDLY_NAME, + CONF_SWITCHES, + CONF_VALUE_TEMPLATE, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_COMMAND_STATE, +) _LOGGER = logging.getLogger(__name__) -SWITCH_SCHEMA = vol.Schema({ - vol.Optional(CONF_COMMAND_OFF, default='true'): cv.string, - vol.Optional(CONF_COMMAND_ON, default='true'): cv.string, - vol.Optional(CONF_COMMAND_STATE): cv.string, - vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, -}) +SWITCH_SCHEMA = vol.Schema( + { + vol.Optional(CONF_COMMAND_OFF, default="true"): cv.string, + vol.Optional(CONF_COMMAND_ON, default="true"): cv.string, + vol.Optional(CONF_COMMAND_STATE): cv.string, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA)} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -46,7 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_config.get(CONF_COMMAND_ON), device_config.get(CONF_COMMAND_OFF), device_config.get(CONF_COMMAND_STATE), - value_template + value_template, ) ) @@ -60,8 +70,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class CommandSwitch(SwitchDevice): """Representation a switch that can be toggled using shell commands.""" - def __init__(self, hass, object_id, friendly_name, command_on, - command_off, command_state, value_template): + def __init__( + self, + hass, + object_id, + friendly_name, + command_on, + command_off, + command_state, + value_template, + ): """Initialize the switch.""" self._hass = hass self.entity_id = ENTITY_ID_FORMAT.format(object_id) @@ -77,7 +95,7 @@ class CommandSwitch(SwitchDevice): """Execute the actual commands.""" _LOGGER.info("Running command: %s", command) - success = (subprocess.call(command, shell=True) == 0) + success = subprocess.call(command, shell=True) == 0 if not success: _LOGGER.error("Command failed: %s", command) @@ -91,7 +109,7 @@ class CommandSwitch(SwitchDevice): try: return_value = subprocess.check_output(command, shell=True) - return return_value.strip().decode('utf-8') + return return_value.strip().decode("utf-8") except subprocess.CalledProcessError: _LOGGER.error("Command failed: %s", command) @@ -135,20 +153,17 @@ class CommandSwitch(SwitchDevice): if self._command_state: payload = str(self._query_state()) if self._value_template: - payload = self._value_template.render_with_possible_json_value( - payload) - self._state = (payload.lower() == 'true') + payload = self._value_template.render_with_possible_json_value(payload) + self._state = payload.lower() == "true" def turn_on(self, **kwargs): """Turn the device on.""" - if (CommandSwitch._switch(self._command_on) and - not self._command_state): + if CommandSwitch._switch(self._command_on) and not self._command_state: self._state = True self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" - if (CommandSwitch._switch(self._command_off) and - not self._command_state): + if CommandSwitch._switch(self._command_off) and not self._command_state: self._state = False self.schedule_update_ha_state() diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index c56e7e71531..2383700f42a 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -9,25 +9,34 @@ import homeassistant.components.alarm_control_panel as alarm import homeassistant.helpers.config_validation as cv from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, CONF_CODE, CONF_MODE, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_CODE, + CONF_MODE, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, +) _LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = 'localhost' -DEFAULT_NAME = 'CONCORD232' +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "CONCORD232" DEFAULT_PORT = 5007 -DEFAULT_MODE = 'audible' +DEFAULT_MODE = "audible" SCAN_INTERVAL = datetime.timedelta(seconds=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_CODE): cv.string, - vol.Optional(CONF_MODE, default=DEFAULT_MODE): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_MODE, default=DEFAULT_MODE): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -38,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = 'http://{}:{}'.format(host, port) + url = "http://{}:{}".format(host, port) try: add_entities([Concord232Alarm(url, name, code, mode)], True) @@ -82,16 +91,18 @@ class Concord232Alarm(alarm.AlarmControlPanel): try: part = self._alarm.list_partitions()[0] except requests.exceptions.ConnectionError as ex: - _LOGGER.error("Unable to connect to %(host)s: %(reason)s", - dict(host=self._url, reason=ex)) + _LOGGER.error( + "Unable to connect to %(host)s: %(reason)s", + dict(host=self._url, reason=ex), + ) return except IndexError: _LOGGER.error("Concord232 reports no partitions") return - if part['arming_level'] == 'Off': + if part["arming_level"] == "Off": self._state = STATE_ALARM_DISARMED - elif 'Home' in part['arming_level']: + elif "Home" in part["arming_level"]: self._state = STATE_ALARM_ARMED_HOME else: self._state = STATE_ALARM_ARMED_AWAY @@ -106,16 +117,16 @@ class Concord232Alarm(alarm.AlarmControlPanel): """Send arm home command.""" if not self._validate_code(code, STATE_ALARM_ARMED_HOME): return - if self._mode == 'silent': - self._alarm.arm('stay', 'silent') + if self._mode == "silent": + self._alarm.arm("stay", "silent") else: - self._alarm.arm('stay') + self._alarm.arm("stay") def alarm_arm_away(self, code=None): """Send arm away command.""" if not self._validate_code(code, STATE_ALARM_ARMED_AWAY): return - self._alarm.arm('away') + self._alarm.arm("away") def _validate_code(self, code, state): """Validate given code.""" @@ -124,8 +135,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): if isinstance(self._code, str): alarm_code = self._code else: - alarm_code = self._code.render(from_state=self._state, - to_state=state) + alarm_code = self._code.render(from_state=self._state, to_state=state) check = not alarm_code or code == alarm_code if not check: _LOGGER.warning("Invalid code given for %s", state) diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index ae464da9798..89b6ab6af97 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -6,33 +6,37 @@ import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES) -from homeassistant.const import (CONF_HOST, CONF_PORT) + BinarySensorDevice, + PLATFORM_SCHEMA, + DEVICE_CLASSES, +) +from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_EXCLUDE_ZONES = 'exclude_zones' -CONF_ZONE_TYPES = 'zone_types' +CONF_EXCLUDE_ZONES = "exclude_zones" +CONF_ZONE_TYPES = "zone_types" -DEFAULT_HOST = 'localhost' -DEFAULT_NAME = 'Alarm' -DEFAULT_PORT = '5007' +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "Alarm" +DEFAULT_PORT = "5007" DEFAULT_SSL = False SCAN_INTERVAL = datetime.timedelta(seconds=10) -ZONE_TYPES_SCHEMA = vol.Schema({ - cv.positive_int: vol.In(DEVICE_CLASSES), -}) +ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: vol.In(DEVICE_CLASSES)}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_EXCLUDE_ZONES, default=[]): - vol.All(cv.ensure_list, [cv.positive_int]), - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_EXCLUDE_ZONES, default=[]): vol.All( + cv.ensure_list, [cv.positive_int] + ), + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -47,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: _LOGGER.debug("Initializing client") - client = concord232_client.Client('http://{}:{}'.format(host, port)) + client = concord232_client.Client("http://{}:{}".format(host, port)) client.zones = client.list_zones() client.last_zone_update = datetime.datetime.now() @@ -60,15 +64,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # name mapping to different sensors in an unpredictable way. Sort # the zones by zone number to prevent this. - client.zones.sort(key=lambda zone: zone['number']) + client.zones.sort(key=lambda zone: zone["number"]) for zone in client.zones: - _LOGGER.info("Loading Zone found: %s", zone['name']) - if zone['number'] not in exclude: + _LOGGER.info("Loading Zone found: %s", zone["name"]) + if zone["number"] not in exclude: sensors.append( Concord232ZoneSensor( - hass, client, zone, zone_types.get( - zone['number'], get_opening_type(zone)) + hass, + client, + zone, + zone_types.get(zone["number"], get_opening_type(zone)), ) ) @@ -77,15 +83,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def get_opening_type(zone): """Return the result of the type guessing from name.""" - if 'MOTION' in zone['name']: - return 'motion' - if 'KEY' in zone['name']: - return 'safety' - if 'SMOKE' in zone['name']: - return 'smoke' - if 'WATER' in zone['name']: - return 'water' - return 'opening' + if "MOTION" in zone["name"]: + return "motion" + if "KEY" in zone["name"]: + return "safety" + if "SMOKE" in zone["name"]: + return "smoke" + if "WATER" in zone["name"]: + return "water" + return "opening" class Concord232ZoneSensor(BinarySensorDevice): @@ -96,7 +102,7 @@ class Concord232ZoneSensor(BinarySensorDevice): self._hass = hass self._client = client self._zone = zone - self._number = zone['number'] + self._number = zone["number"] self._zone_type = zone_type @property @@ -112,13 +118,13 @@ class Concord232ZoneSensor(BinarySensorDevice): @property def name(self): """Return the name of the binary sensor.""" - return self._zone['name'] + return self._zone["name"] @property def is_on(self): """Return true if the binary sensor is on.""" # True means "faulted" or "open" or "abnormal state" - return bool(self._zone['state'] != 'Normal') + return bool(self._zone["state"] != "Normal") def update(self): """Get updated stats from API.""" @@ -127,8 +133,9 @@ class Concord232ZoneSensor(BinarySensorDevice): if last_update > datetime.timedelta(seconds=1): self._client.zones = self._client.list_zones() self._client.last_zone_update = datetime.datetime.now() - _LOGGER.debug("Updated from zone: %s", self._zone['name']) + _LOGGER.debug("Updated from zone: %s", self._zone["name"]) - if hasattr(self._client, 'zones'): - self._zone = next((x for x in self._client.zones - if x['number'] == self._number), None) + if hasattr(self._client, "zones"): + self._zone = next( + (x for x in self._client.zones if x["number"] == self._number), None + ) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 0cb76cc8c3b..5de11a032c5 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -11,31 +11,32 @@ from homeassistant.setup import ATTR_COMPONENT from homeassistant.components.http import HomeAssistantView from homeassistant.util.yaml import load_yaml, dump -DOMAIN = 'config' +DOMAIN = "config" SECTIONS = ( - 'area_registry', - 'auth', - 'auth_provider_homeassistant', - 'automation', - 'config_entries', - 'core', - 'customize', - 'device_registry', - 'entity_registry', - 'group', - 'script', + "area_registry", + "auth", + "auth_provider_homeassistant", + "automation", + "config_entries", + "core", + "customize", + "device_registry", + "entity_registry", + "group", + "script", ) -ON_DEMAND = ('zwave',) +ON_DEMAND = ("zwave",) async def async_setup(hass, config): """Set up the config component.""" hass.components.frontend.async_register_built_in_panel( - 'config', 'config', 'hass:settings', require_admin=True) + "config", "config", "hass:settings", require_admin=True + ) async def setup_panel(panel_name): """Set up a panel.""" - panel = importlib.import_module('.{}'.format(panel_name), __name__) + panel = importlib.import_module(".{}".format(panel_name), __name__) if not panel: return @@ -43,7 +44,7 @@ async def async_setup(hass, config): success = await panel.async_setup(hass) if success: - key = '{}.{}'.format(DOMAIN, panel_name) + key = "{}.{}".format(DOMAIN, panel_name) hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key}) @callback @@ -70,11 +71,19 @@ async def async_setup(hass, config): class BaseEditConfigView(HomeAssistantView): """Configure a Group endpoint.""" - def __init__(self, component, config_type, path, key_schema, data_schema, - *, post_write_hook=None): + def __init__( + self, + component, + config_type, + path, + key_schema, + data_schema, + *, + post_write_hook=None, + ): """Initialize a config view.""" - self.url = '/api/config/%s/%s/{config_key}' % (component, config_type) - self.name = 'api:config:%s:%s' % (component, config_type) + self.url = "/api/config/%s/%s/{config_key}" % (component, config_type) + self.name = "api:config:%s:%s" % (component, config_type) self.path = path self.key_schema = key_schema self.data_schema = data_schema @@ -98,12 +107,12 @@ class BaseEditConfigView(HomeAssistantView): async def get(self, request, config_key): """Fetch device specific config.""" - hass = request.app['hass'] + hass = request.app["hass"] current = await self.read_config(hass) value = self._get_value(hass, current, config_key) if value is None: - return self.json_message('Resource not found', 404) + return self.json_message("Resource not found", 404) return self.json(value) @@ -112,21 +121,21 @@ class BaseEditConfigView(HomeAssistantView): try: data = await request.json() except ValueError: - return self.json_message('Invalid JSON specified', 400) + return self.json_message("Invalid JSON specified", 400) try: self.key_schema(config_key) except vol.Invalid as err: - return self.json_message('Key malformed: {}'.format(err), 400) + return self.json_message("Key malformed: {}".format(err), 400) try: # We just validate, we don't store that data because # we don't want to store the defaults. self.data_schema(data) except vol.Invalid as err: - return self.json_message('Message malformed: {}'.format(err), 400) + return self.json_message("Message malformed: {}".format(err), 400) - hass = request.app['hass'] + hass = request.app["hass"] path = hass.config.path(self.path) current = await self.read_config(hass) @@ -137,19 +146,17 @@ class BaseEditConfigView(HomeAssistantView): if self.post_write_hook is not None: hass.async_create_task(self.post_write_hook(hass)) - return self.json({ - 'result': 'ok', - }) + return self.json({"result": "ok"}) async def delete(self, request, config_key): """Remove an entry.""" - hass = request.app['hass'] + hass = request.app["hass"] current = await self.read_config(hass) value = self._get_value(hass, current, config_key) path = hass.config.path(self.path) if value is None: - return self.json_message('Resource not found', 404) + return self.json_message("Resource not found", 404) self._delete_value(hass, current, config_key) await hass.async_add_executor_job(_write, path, current) @@ -157,14 +164,11 @@ class BaseEditConfigView(HomeAssistantView): if self.post_write_hook is not None: hass.async_create_task(self.post_write_hook(hass)) - return self.json({ - 'result': 'ok', - }) + return self.json({"result": "ok"}) async def read_config(self, hass): """Read the config.""" - current = await hass.async_add_job( - _read, hass.config.path(self.path)) + current = await hass.async_add_job(_read, hass.config.path(self.path)) if not current: current = self._empty_config() return current @@ -199,8 +203,7 @@ class EditIdBasedConfigView(BaseEditConfigView): def _get_value(self, hass, data, config_key): """Get value.""" - return next( - (val for val in data if val.get(CONF_ID) == config_key), None) + return next((val for val in data if val.get(CONF_ID) == config_key), None) def _write_value(self, hass, data, config_key, new_value): """Set value.""" @@ -215,8 +218,8 @@ class EditIdBasedConfigView(BaseEditConfigView): def _delete_value(self, hass, data, config_key): """Delete value.""" index = next( - idx for idx, val in enumerate(data) - if val.get(CONF_ID) == config_key) + idx for idx, val in enumerate(data) if val.get(CONF_ID) == config_key + ) data.pop(index) @@ -233,5 +236,5 @@ def _write(path, data): # Do it before opening file. If dump causes error it will now not # truncate the file. data = dump(data) - with open(path, 'w', encoding='utf-8') as outfile: + with open(path, "w", encoding="utf-8") as outfile: outfile.write(data) diff --git a/homeassistant/components/config/area_registry.py b/homeassistant/components/config/area_registry.py index 06fc3eae34d..9c8853ac782 100644 --- a/homeassistant/components/config/area_registry.py +++ b/homeassistant/components/config/area_registry.py @@ -3,34 +3,36 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api.decorators import ( - async_response, require_admin) + async_response, + require_admin, +) from homeassistant.core import callback from homeassistant.helpers.area_registry import async_get_registry -WS_TYPE_LIST = 'config/area_registry/list' -SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_LIST, -}) +WS_TYPE_LIST = "config/area_registry/list" +SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_LIST} +) -WS_TYPE_CREATE = 'config/area_registry/create' -SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_CREATE, - vol.Required('name'): str, -}) +WS_TYPE_CREATE = "config/area_registry/create" +SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_CREATE, vol.Required("name"): str} +) -WS_TYPE_DELETE = 'config/area_registry/delete' -SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_DELETE, - vol.Required('area_id'): str, -}) +WS_TYPE_DELETE = "config/area_registry/delete" +SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_DELETE, vol.Required("area_id"): str} +) -WS_TYPE_UPDATE = 'config/area_registry/update' -SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_UPDATE, - vol.Required('area_id'): str, - vol.Required('name'): str, -}) +WS_TYPE_UPDATE = "config/area_registry/update" +SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_UPDATE, + vol.Required("area_id"): str, + vol.Required("name"): str, + } +) async def async_setup(hass): @@ -54,12 +56,15 @@ async def async_setup(hass): async def websocket_list_areas(hass, connection, msg): """Handle list areas command.""" registry = await async_get_registry(hass) - connection.send_message(websocket_api.result_message( - msg['id'], [{ - 'name': entry.name, - 'area_id': entry.id, - } for entry in registry.async_list_areas()] - )) + connection.send_message( + websocket_api.result_message( + msg["id"], + [ + {"name": entry.name, "area_id": entry.id} + for entry in registry.async_list_areas() + ], + ) + ) @require_admin @@ -68,15 +73,15 @@ async def websocket_create_area(hass, connection, msg): """Create area command.""" registry = await async_get_registry(hass) try: - entry = registry.async_create(msg['name']) + entry = registry.async_create(msg["name"]) except ValueError as err: - connection.send_message(websocket_api.error_message( - msg['id'], 'invalid_info', str(err) - )) + connection.send_message( + websocket_api.error_message(msg["id"], "invalid_info", str(err)) + ) else: - connection.send_message(websocket_api.result_message( - msg['id'], _entry_dict(entry) - )) + connection.send_message( + websocket_api.result_message(msg["id"], _entry_dict(entry)) + ) @require_admin @@ -86,15 +91,15 @@ async def websocket_delete_area(hass, connection, msg): registry = await async_get_registry(hass) try: - await registry.async_delete(msg['area_id']) + await registry.async_delete(msg["area_id"]) except KeyError: - connection.send_message(websocket_api.error_message( - msg['id'], 'invalid_info', "Area ID doesn't exist" - )) + connection.send_message( + websocket_api.error_message( + msg["id"], "invalid_info", "Area ID doesn't exist" + ) + ) else: - connection.send_message(websocket_api.result_message( - msg['id'], 'success' - )) + connection.send_message(websocket_api.result_message(msg["id"], "success")) @require_admin @@ -104,21 +109,18 @@ async def websocket_update_area(hass, connection, msg): registry = await async_get_registry(hass) try: - entry = registry.async_update(msg['area_id'], msg['name']) + entry = registry.async_update(msg["area_id"], msg["name"]) except ValueError as err: - connection.send_message(websocket_api.error_message( - msg['id'], 'invalid_info', str(err) - )) + connection.send_message( + websocket_api.error_message(msg["id"], "invalid_info", str(err)) + ) else: - connection.send_message(websocket_api.result_message( - msg['id'], _entry_dict(entry) - )) + connection.send_message( + websocket_api.result_message(msg["id"], _entry_dict(entry)) + ) @callback def _entry_dict(entry): """Convert entry to API format.""" - return { - 'area_id': entry.id, - 'name': entry.name - } + return {"area_id": entry.id, "name": entry.name} diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index e6451e09a98..977bae36083 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -4,37 +4,32 @@ import voluptuous as vol from homeassistant.components import websocket_api -WS_TYPE_LIST = 'config/auth/list' -SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_LIST, -}) +WS_TYPE_LIST = "config/auth/list" +SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_LIST} +) -WS_TYPE_DELETE = 'config/auth/delete' -SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_DELETE, - vol.Required('user_id'): str, -}) +WS_TYPE_DELETE = "config/auth/delete" +SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_DELETE, vol.Required("user_id"): str} +) -WS_TYPE_CREATE = 'config/auth/create' -SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_CREATE, - vol.Required('name'): str, -}) +WS_TYPE_CREATE = "config/auth/create" +SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_CREATE, vol.Required("name"): str} +) async def async_setup(hass): """Enable the Home Assistant views.""" hass.components.websocket_api.async_register_command( - WS_TYPE_LIST, websocket_list, - SCHEMA_WS_LIST + WS_TYPE_LIST, websocket_list, SCHEMA_WS_LIST ) hass.components.websocket_api.async_register_command( - WS_TYPE_DELETE, websocket_delete, - SCHEMA_WS_DELETE + WS_TYPE_DELETE, websocket_delete, SCHEMA_WS_DELETE ) hass.components.websocket_api.async_register_command( - WS_TYPE_CREATE, websocket_create, - SCHEMA_WS_CREATE + WS_TYPE_CREATE, websocket_create, SCHEMA_WS_CREATE ) hass.components.websocket_api.async_register_command(websocket_update) return True @@ -46,91 +41,95 @@ async def websocket_list(hass, connection, msg): """Return a list of users.""" result = [_user_info(u) for u in await hass.auth.async_get_users()] - connection.send_message( - websocket_api.result_message(msg['id'], result)) + connection.send_message(websocket_api.result_message(msg["id"], result)) @websocket_api.require_admin @websocket_api.async_response async def websocket_delete(hass, connection, msg): """Delete a user.""" - if msg['user_id'] == connection.user.id: - connection.send_message(websocket_api.error_message( - msg['id'], 'no_delete_self', - 'Unable to delete your own account')) + if msg["user_id"] == connection.user.id: + connection.send_message( + websocket_api.error_message( + msg["id"], "no_delete_self", "Unable to delete your own account" + ) + ) return - user = await hass.auth.async_get_user(msg['user_id']) + user = await hass.auth.async_get_user(msg["user_id"]) if not user: - connection.send_message(websocket_api.error_message( - msg['id'], 'not_found', 'User not found')) + connection.send_message( + websocket_api.error_message(msg["id"], "not_found", "User not found") + ) return await hass.auth.async_remove_user(user) - connection.send_message( - websocket_api.result_message(msg['id'])) + connection.send_message(websocket_api.result_message(msg["id"])) @websocket_api.require_admin @websocket_api.async_response async def websocket_create(hass, connection, msg): """Create a user.""" - user = await hass.auth.async_create_user(msg['name']) + user = await hass.auth.async_create_user(msg["name"]) connection.send_message( - websocket_api.result_message(msg['id'], { - 'user': _user_info(user) - })) + websocket_api.result_message(msg["id"], {"user": _user_info(user)}) + ) @websocket_api.require_admin @websocket_api.async_response -@websocket_api.websocket_command({ - vol.Required('type'): 'config/auth/update', - vol.Required('user_id'): str, - vol.Optional('name'): str, - vol.Optional('group_ids'): [str] -}) +@websocket_api.websocket_command( + { + vol.Required("type"): "config/auth/update", + vol.Required("user_id"): str, + vol.Optional("name"): str, + vol.Optional("group_ids"): [str], + } +) async def websocket_update(hass, connection, msg): """Update a user.""" - user = await hass.auth.async_get_user(msg.pop('user_id')) + user = await hass.auth.async_get_user(msg.pop("user_id")) if not user: - connection.send_message(websocket_api.error_message( - msg['id'], websocket_api.const.ERR_NOT_FOUND, 'User not found')) + connection.send_message( + websocket_api.error_message( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "User not found" + ) + ) return if user.system_generated: - connection.send_message(websocket_api.error_message( - msg['id'], 'cannot_modify_system_generated', - 'Unable to update system generated users.')) + connection.send_message( + websocket_api.error_message( + msg["id"], + "cannot_modify_system_generated", + "Unable to update system generated users.", + ) + ) return - msg.pop('type') - msg_id = msg.pop('id') + msg.pop("type") + msg_id = msg.pop("id") await hass.auth.async_update_user(user, **msg) connection.send_message( - websocket_api.result_message(msg_id, { - 'user': _user_info(user), - })) + websocket_api.result_message(msg_id, {"user": _user_info(user)}) + ) def _user_info(user): """Format a user.""" return { - 'id': user.id, - 'name': user.name, - 'is_owner': user.is_owner, - 'is_active': user.is_active, - 'system_generated': user.system_generated, - 'group_ids': [group.id for group in user.groups], - 'credentials': [ - { - 'type': c.auth_provider_type, - } for c in user.credentials - ] + "id": user.id, + "name": user.name, + "is_owner": user.is_owner, + "is_active": user.is_active, + "system_generated": user.system_generated, + "group_ids": [group.id for group in user.groups], + "credentials": [{"type": c.auth_provider_type} for c in user.credentials], } diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index f6fc4bc8cef..817675db238 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -5,41 +5,41 @@ from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.components import websocket_api -WS_TYPE_CREATE = 'config/auth_provider/homeassistant/create' -SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_CREATE, - vol.Required('user_id'): str, - vol.Required('username'): str, - vol.Required('password'): str, -}) +WS_TYPE_CREATE = "config/auth_provider/homeassistant/create" +SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_CREATE, + vol.Required("user_id"): str, + vol.Required("username"): str, + vol.Required("password"): str, + } +) -WS_TYPE_DELETE = 'config/auth_provider/homeassistant/delete' -SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_DELETE, - vol.Required('username'): str, -}) +WS_TYPE_DELETE = "config/auth_provider/homeassistant/delete" +SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_DELETE, vol.Required("username"): str} +) -WS_TYPE_CHANGE_PASSWORD = 'config/auth_provider/homeassistant/change_password' -SCHEMA_WS_CHANGE_PASSWORD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_CHANGE_PASSWORD, - vol.Required('current_password'): str, - vol.Required('new_password'): str -}) +WS_TYPE_CHANGE_PASSWORD = "config/auth_provider/homeassistant/change_password" +SCHEMA_WS_CHANGE_PASSWORD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_CHANGE_PASSWORD, + vol.Required("current_password"): str, + vol.Required("new_password"): str, + } +) async def async_setup(hass): """Enable the Home Assistant views.""" hass.components.websocket_api.async_register_command( - WS_TYPE_CREATE, websocket_create, - SCHEMA_WS_CREATE + WS_TYPE_CREATE, websocket_create, SCHEMA_WS_CREATE ) hass.components.websocket_api.async_register_command( - WS_TYPE_DELETE, websocket_delete, - SCHEMA_WS_DELETE + WS_TYPE_DELETE, websocket_delete, SCHEMA_WS_DELETE ) hass.components.websocket_api.async_register_command( - WS_TYPE_CHANGE_PASSWORD, websocket_change_password, - SCHEMA_WS_CHANGE_PASSWORD + WS_TYPE_CHANGE_PASSWORD, websocket_change_password, SCHEMA_WS_CHANGE_PASSWORD ) return True @@ -47,10 +47,10 @@ async def async_setup(hass): def _get_provider(hass): """Get homeassistant auth provider.""" for prv in hass.auth.auth_providers: - if prv.type == 'homeassistant': + if prv.type == "homeassistant": return prv - raise RuntimeError('Provider not found') + raise RuntimeError("Provider not found") @websocket_api.require_admin @@ -60,34 +60,43 @@ async def websocket_create(hass, connection, msg): provider = _get_provider(hass) await provider.async_initialize() - user = await hass.auth.async_get_user(msg['user_id']) + user = await hass.auth.async_get_user(msg["user_id"]) if user is None: - connection.send_message(websocket_api.error_message( - msg['id'], 'not_found', 'User not found')) + connection.send_message( + websocket_api.error_message(msg["id"], "not_found", "User not found") + ) return if user.system_generated: - connection.send_message(websocket_api.error_message( - msg['id'], 'system_generated', - 'Cannot add credentials to a system generated user.')) + connection.send_message( + websocket_api.error_message( + msg["id"], + "system_generated", + "Cannot add credentials to a system generated user.", + ) + ) return try: await hass.async_add_executor_job( - provider.data.add_auth, msg['username'], msg['password']) + provider.data.add_auth, msg["username"], msg["password"] + ) except auth_ha.InvalidUser: - connection.send_message(websocket_api.error_message( - msg['id'], 'username_exists', 'Username already exists')) + connection.send_message( + websocket_api.error_message( + msg["id"], "username_exists", "Username already exists" + ) + ) return - credentials = await provider.async_get_or_create_credentials({ - 'username': msg['username'] - }) + credentials = await provider.async_get_or_create_credentials( + {"username": msg["username"]} + ) await hass.auth.async_link_user(user, credentials) await provider.data.async_save() - connection.send_message(websocket_api.result_message(msg['id'])) + connection.send_message(websocket_api.result_message(msg["id"])) @websocket_api.require_admin @@ -97,29 +106,30 @@ async def websocket_delete(hass, connection, msg): provider = _get_provider(hass) await provider.async_initialize() - credentials = await provider.async_get_or_create_credentials({ - 'username': msg['username'] - }) + credentials = await provider.async_get_or_create_credentials( + {"username": msg["username"]} + ) # if not new, an existing credential exists. # Removing the credential will also remove the auth. if not credentials.is_new: await hass.auth.async_remove_credentials(credentials) - connection.send_message( - websocket_api.result_message(msg['id'])) + connection.send_message(websocket_api.result_message(msg["id"])) return try: - provider.data.async_remove_auth(msg['username']) + provider.data.async_remove_auth(msg["username"]) await provider.data.async_save() except auth_ha.InvalidUser: - connection.send_message(websocket_api.error_message( - msg['id'], 'auth_not_found', 'Given username was not found.')) + connection.send_message( + websocket_api.error_message( + msg["id"], "auth_not_found", "Given username was not found." + ) + ) return - connection.send_message( - websocket_api.result_message(msg['id'])) + connection.send_message(websocket_api.result_message(msg["id"])) @websocket_api.async_response @@ -127,8 +137,9 @@ async def websocket_change_password(hass, connection, msg): """Change user password.""" user = connection.user if user is None: - connection.send_message(websocket_api.error_message( - msg['id'], 'user_not_found', 'User not found')) + connection.send_message( + websocket_api.error_message(msg["id"], "user_not_found", "User not found") + ) return provider = _get_provider(hass) @@ -137,25 +148,30 @@ async def websocket_change_password(hass, connection, msg): username = None for credential in user.credentials: if credential.auth_provider_type == provider.type: - username = credential.data['username'] + username = credential.data["username"] break if username is None: - connection.send_message(websocket_api.error_message( - msg['id'], 'credentials_not_found', 'Credentials not found')) + connection.send_message( + websocket_api.error_message( + msg["id"], "credentials_not_found", "Credentials not found" + ) + ) return try: - await provider.async_validate_login( - username, msg['current_password']) + await provider.async_validate_login(username, msg["current_password"]) except auth_ha.InvalidAuth: - connection.send_message(websocket_api.error_message( - msg['id'], 'invalid_password', 'Invalid password')) + connection.send_message( + websocket_api.error_message( + msg["id"], "invalid_password", "Invalid password" + ) + ) return await hass.async_add_executor_job( - provider.data.change_password, username, msg['new_password']) + provider.data.change_password, username, msg["new_password"] + ) await provider.data.async_save() - connection.send_message( - websocket_api.result_message(msg['id'])) + connection.send_message(websocket_api.result_message(msg["id"])) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 175d90ff59c..7ca71fc4f93 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -8,19 +8,26 @@ import homeassistant.helpers.config_validation as cv from . import EditIdBasedConfigView -CONFIG_PATH = 'automations.yaml' +CONFIG_PATH = "automations.yaml" async def async_setup(hass): """Set up the Automation config API.""" + async def hook(hass): """post_write_hook for Config View that reloads automations.""" await hass.services.async_call(DOMAIN, SERVICE_RELOAD) - hass.http.register_view(EditAutomationConfigView( - DOMAIN, 'config', CONFIG_PATH, cv.string, - PLATFORM_SCHEMA, post_write_hook=hook - )) + hass.http.register_view( + EditAutomationConfigView( + DOMAIN, + "config", + CONFIG_PATH, + cv.string, + PLATFORM_SCHEMA, + post_write_hook=hook, + ) + ) return True @@ -46,7 +53,7 @@ class EditAutomationConfigView(EditIdBasedConfigView): # Iterate through some keys that we want to have ordered in the output updated_value = OrderedDict() - for key in ('id', 'alias', 'trigger', 'condition', 'action'): + for key in ("id", "alias", "trigger", "condition", "action"): if key in cur_value: updated_value[key] = cur_value[key] if key in new_value: diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 9687a407ccb..140b5a2b270 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -4,7 +4,9 @@ from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES from homeassistant.components.http import HomeAssistantView from homeassistant.exceptions import Unauthorized from homeassistant.helpers.data_entry_flow import ( - FlowManagerIndexView, FlowManagerResourceView) + FlowManagerIndexView, + FlowManagerResourceView, +) from homeassistant.loader import async_get_config_flows @@ -12,32 +14,32 @@ async def async_setup(hass): """Enable the Home Assistant views.""" hass.http.register_view(ConfigManagerEntryIndexView) hass.http.register_view(ConfigManagerEntryResourceView) - hass.http.register_view( - ConfigManagerFlowIndexView(hass.config_entries.flow)) - hass.http.register_view( - ConfigManagerFlowResourceView(hass.config_entries.flow)) + hass.http.register_view(ConfigManagerFlowIndexView(hass.config_entries.flow)) + hass.http.register_view(ConfigManagerFlowResourceView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerAvailableFlowView) hass.http.register_view( - OptionManagerFlowIndexView(hass.config_entries.options.flow)) + OptionManagerFlowIndexView(hass.config_entries.options.flow) + ) hass.http.register_view( - OptionManagerFlowResourceView(hass.config_entries.options.flow)) + OptionManagerFlowResourceView(hass.config_entries.options.flow) + ) return True def _prepare_json(result): """Convert result for JSON.""" - if result['type'] != data_entry_flow.RESULT_TYPE_FORM: + if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result import voluptuous_serialize data = result.copy() - schema = data['data_schema'] + schema = data["data_schema"] if schema is None: - data['data_schema'] = [] + data["data_schema"] = [] else: - data['data_schema'] = voluptuous_serialize.convert(schema) + data["data_schema"] = voluptuous_serialize.convert(schema) return data @@ -45,43 +47,49 @@ def _prepare_json(result): class ConfigManagerEntryIndexView(HomeAssistantView): """View to get available config entries.""" - url = '/api/config/config_entries/entry' - name = 'api:config:config_entries:entry' + url = "/api/config/config_entries/entry" + name = "api:config:config_entries:entry" async def get(self, request): """List available config entries.""" - hass = request.app['hass'] + hass = request.app["hass"] - return self.json([{ - 'entry_id': entry.entry_id, - 'domain': entry.domain, - 'title': entry.title, - 'source': entry.source, - 'state': entry.state, - 'connection_class': entry.connection_class, - 'supports_options': hasattr( - config_entries.HANDLERS.get(entry.domain), - 'async_get_options_flow'), - } for entry in hass.config_entries.async_entries()]) + return self.json( + [ + { + "entry_id": entry.entry_id, + "domain": entry.domain, + "title": entry.title, + "source": entry.source, + "state": entry.state, + "connection_class": entry.connection_class, + "supports_options": hasattr( + config_entries.HANDLERS.get(entry.domain), + "async_get_options_flow", + ), + } + for entry in hass.config_entries.async_entries() + ] + ) class ConfigManagerEntryResourceView(HomeAssistantView): """View to interact with a config entry.""" - url = '/api/config/config_entries/entry/{entry_id}' - name = 'api:config:config_entries:entry:resource' + url = "/api/config/config_entries/entry/{entry_id}" + name = "api:config:config_entries:entry:resource" async def delete(self, request, entry_id): """Delete a config entry.""" - if not request['hass_user'].is_admin: - raise Unauthorized(config_entry_id=entry_id, permission='remove') + if not request["hass_user"].is_admin: + raise Unauthorized(config_entry_id=entry_id, permission="remove") - hass = request.app['hass'] + hass = request.app["hass"] try: result = await hass.config_entries.async_remove(entry_id) except config_entries.UnknownEntry: - return self.json_message('Invalid entry specified', 404) + return self.json_message("Invalid entry specified", 404) return self.json(result) @@ -89,8 +97,8 @@ class ConfigManagerEntryResourceView(HomeAssistantView): class ConfigManagerFlowIndexView(FlowManagerIndexView): """View to create config flows.""" - url = '/api/config/config_entries/flow' - name = 'api:config:config_entries:flow' + url = "/api/config/config_entries/flow" + name = "api:config:config_entries:flow" async def get(self, request): """List flows that are in progress but not started by a user. @@ -98,89 +106,89 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): Example of a non-user initiated flow is a discovered Hue hub that requires user interaction to finish setup. """ - if not request['hass_user'].is_admin: - raise Unauthorized( - perm_category=CAT_CONFIG_ENTRIES, permission='add') + if not request["hass_user"].is_admin: + raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") - hass = request.app['hass'] + hass = request.app["hass"] - return self.json([ - flw for flw in hass.config_entries.flow.async_progress() - if flw['context']['source'] != config_entries.SOURCE_USER]) + return self.json( + [ + flw + for flw in hass.config_entries.flow.async_progress() + if flw["context"]["source"] != config_entries.SOURCE_USER + ] + ) # pylint: disable=arguments-differ async def post(self, request): """Handle a POST request.""" - if not request['hass_user'].is_admin: - raise Unauthorized( - perm_category=CAT_CONFIG_ENTRIES, permission='add') + if not request["hass_user"].is_admin: + raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") # pylint: disable=no-value-for-parameter return await super().post(request) def _prepare_result_json(self, result): """Convert result to JSON.""" - if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: return super()._prepare_result_json(result) data = result.copy() - data['result'] = data['result'].entry_id - data.pop('data') + data["result"] = data["result"].entry_id + data.pop("data") return data class ConfigManagerFlowResourceView(FlowManagerResourceView): """View to interact with the flow manager.""" - url = '/api/config/config_entries/flow/{flow_id}' - name = 'api:config:config_entries:flow:resource' + url = "/api/config/config_entries/flow/{flow_id}" + name = "api:config:config_entries:flow:resource" async def get(self, request, flow_id): """Get the current state of a data_entry_flow.""" - if not request['hass_user'].is_admin: - raise Unauthorized( - perm_category=CAT_CONFIG_ENTRIES, permission='add') + if not request["hass_user"].is_admin: + raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") return await super().get(request, flow_id) # pylint: disable=arguments-differ async def post(self, request, flow_id): """Handle a POST request.""" - if not request['hass_user'].is_admin: - raise Unauthorized( - perm_category=CAT_CONFIG_ENTRIES, permission='add') + if not request["hass_user"].is_admin: + raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") # pylint: disable=no-value-for-parameter return await super().post(request, flow_id) def _prepare_result_json(self, result): """Convert result to JSON.""" - if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: return super()._prepare_result_json(result) data = result.copy() - data['result'] = data['result'].entry_id - data.pop('data') + data["result"] = data["result"].entry_id + data.pop("data") return data class ConfigManagerAvailableFlowView(HomeAssistantView): """View to query available flows.""" - url = '/api/config/config_entries/flow_handlers' - name = 'api:config:config_entries:flow_handlers' + url = "/api/config/config_entries/flow_handlers" + name = "api:config:config_entries:flow_handlers" async def get(self, request): """List available flow handlers.""" - hass = request.app['hass'] + hass = request.app["hass"] return self.json(await async_get_config_flows(hass)) class OptionManagerFlowIndexView(FlowManagerIndexView): """View to create option flows.""" - url = '/api/config/config_entries/entry/option/flow' - name = 'api:config:config_entries:entry:resource:option:flow' + url = "/api/config/config_entries/entry/option/flow" + name = "api:config:config_entries:entry:resource:option:flow" # pylint: disable=arguments-differ async def post(self, request): @@ -188,9 +196,8 @@ class OptionManagerFlowIndexView(FlowManagerIndexView): handler in request is entry_id. """ - if not request['hass_user'].is_admin: - raise Unauthorized( - perm_category=CAT_CONFIG_ENTRIES, permission='edit') + if not request["hass_user"].is_admin: + raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="edit") # pylint: disable=no-value-for-parameter return await super().post(request) @@ -199,23 +206,21 @@ class OptionManagerFlowIndexView(FlowManagerIndexView): class OptionManagerFlowResourceView(FlowManagerResourceView): """View to interact with the option flow manager.""" - url = '/api/config/config_entries/options/flow/{flow_id}' - name = 'api:config:config_entries:options:flow:resource' + url = "/api/config/config_entries/options/flow/{flow_id}" + name = "api:config:config_entries:options:flow:resource" async def get(self, request, flow_id): """Get the current state of a data_entry_flow.""" - if not request['hass_user'].is_admin: - raise Unauthorized( - perm_category=CAT_CONFIG_ENTRIES, permission='edit') + if not request["hass_user"].is_admin: + raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="edit") return await super().get(request, flow_id) # pylint: disable=arguments-differ async def post(self, request, flow_id): """Handle a POST request.""" - if not request['hass_user'].is_admin: - raise Unauthorized( - perm_category=CAT_CONFIG_ENTRIES, permission='edit') + if not request["hass_user"].is_admin: + raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="edit") # pylint: disable=no-value-for-parameter return await super().post(request, flow_id) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index a83516bdc37..073f8f23d6c 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -5,9 +5,7 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.config import async_check_ha_config_file from homeassistant.components import websocket_api -from homeassistant.const import ( - CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL -) +from homeassistant.const import CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.helpers import config_validation as cv from homeassistant.util import location @@ -23,52 +21,47 @@ async def async_setup(hass): class CheckConfigView(HomeAssistantView): """Hassbian packages endpoint.""" - url = '/api/config/core/check_config' - name = 'api:config:core:check_config' + url = "/api/config/core/check_config" + name = "api:config:core:check_config" async def post(self, request): """Validate configuration and return results.""" - errors = await async_check_ha_config_file(request.app['hass']) + errors = await async_check_ha_config_file(request.app["hass"]) - state = 'invalid' if errors else 'valid' + state = "invalid" if errors else "valid" - return self.json({ - "result": state, - "errors": errors, - }) + return self.json({"result": state, "errors": errors}) @websocket_api.require_admin @websocket_api.async_response -@websocket_api.websocket_command({ - 'type': 'config/core/update', - vol.Optional('latitude'): cv.latitude, - vol.Optional('longitude'): cv.longitude, - vol.Optional('elevation'): int, - vol.Optional('unit_system'): cv.unit_system, - vol.Optional('location_name'): str, - vol.Optional('time_zone'): cv.time_zone, -}) +@websocket_api.websocket_command( + { + "type": "config/core/update", + vol.Optional("latitude"): cv.latitude, + vol.Optional("longitude"): cv.longitude, + vol.Optional("elevation"): int, + vol.Optional("unit_system"): cv.unit_system, + vol.Optional("location_name"): str, + vol.Optional("time_zone"): cv.time_zone, + } +) async def websocket_update_config(hass, connection, msg): """Handle update core config command.""" data = dict(msg) - data.pop('id') - data.pop('type') + data.pop("id") + data.pop("type") try: await hass.config.async_update(**data) - connection.send_result(msg['id']) + connection.send_result(msg["id"]) except ValueError as err: - connection.send_error( - msg['id'], 'invalid_info', str(err) - ) + connection.send_error(msg["id"], "invalid_info", str(err)) @websocket_api.require_admin @websocket_api.async_response -@websocket_api.websocket_command({ - 'type': 'config/core/detect', -}) +@websocket_api.websocket_command({"type": "config/core/detect"}) async def websocket_detect_config(hass, connection, msg): """Detect core config.""" session = hass.helpers.aiohttp_client.async_get_clientsession() @@ -77,21 +70,21 @@ async def websocket_detect_config(hass, connection, msg): info = {} if location_info is None: - connection.send_result(msg['id'], info) + connection.send_result(msg["id"], info) return if location_info.use_metric: - info['unit_system'] = CONF_UNIT_SYSTEM_METRIC + info["unit_system"] = CONF_UNIT_SYSTEM_METRIC else: - info['unit_system'] = CONF_UNIT_SYSTEM_IMPERIAL + info["unit_system"] = CONF_UNIT_SYSTEM_IMPERIAL if location_info.latitude: - info['latitude'] = location_info.latitude + info["latitude"] = location_info.latitude if location_info.longitude: - info['longitude'] = location_info.longitude + info["longitude"] = location_info.longitude if location_info.time_zone: - info['time_zone'] = location_info.time_zone + info["time_zone"] = location_info.time_zone - connection.send_result(msg['id'], info) + connection.send_result(msg["id"], info) diff --git a/homeassistant/components/config/customize.py b/homeassistant/components/config/customize.py index 85e9c0e6886..ed75a8a04a6 100644 --- a/homeassistant/components/config/customize.py +++ b/homeassistant/components/config/customize.py @@ -6,19 +6,21 @@ import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView -CONFIG_PATH = 'customize.yaml' +CONFIG_PATH = "customize.yaml" async def async_setup(hass): """Set up the Customize config API.""" + async def hook(hass): """post_write_hook for Config View that reloads groups.""" await hass.services.async_call(DOMAIN, SERVICE_RELOAD_CORE_CONFIG) - hass.http.register_view(CustomizeConfigView( - 'customize', 'config', CONFIG_PATH, cv.entity_id, dict, - post_write_hook=hook - )) + hass.http.register_view( + CustomizeConfigView( + "customize", "config", CONFIG_PATH, cv.entity_id, dict, post_write_hook=hook + ) + ) return True @@ -29,7 +31,7 @@ class CustomizeConfigView(EditKeyBasedConfigView): def _get_value(self, hass, data, config_key): """Get value.""" customize = hass.data.get(DATA_CUSTOMIZE, {}).get(config_key) or {} - return {'global': customize, 'local': data.get(config_key, {})} + return {"global": customize, "local": data.get(config_key, {})} def _write_value(self, hass, data, config_key, new_value): """Set value.""" diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 61b00bf6726..08f53f948fe 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -3,29 +3,32 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api.decorators import ( - async_response, require_admin) + async_response, + require_admin, +) from homeassistant.core import callback from homeassistant.helpers.device_registry import async_get_registry -WS_TYPE_LIST = 'config/device_registry/list' -SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_LIST, -}) +WS_TYPE_LIST = "config/device_registry/list" +SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_LIST} +) -WS_TYPE_UPDATE = 'config/device_registry/update' -SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_UPDATE, - vol.Required('device_id'): str, - vol.Optional('area_id'): vol.Any(str, None), - vol.Optional('name_by_user'): vol.Any(str, None), -}) +WS_TYPE_UPDATE = "config/device_registry/update" +SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_UPDATE, + vol.Required("device_id"): str, + vol.Optional("area_id"): vol.Any(str, None), + vol.Optional("name_by_user"): vol.Any(str, None), + } +) async def async_setup(hass): """Enable the Device Registry views.""" hass.components.websocket_api.async_register_command( - WS_TYPE_LIST, websocket_list_devices, - SCHEMA_WS_LIST + WS_TYPE_LIST, websocket_list_devices, SCHEMA_WS_LIST ) hass.components.websocket_api.async_register_command( WS_TYPE_UPDATE, websocket_update_device, SCHEMA_WS_UPDATE @@ -37,9 +40,11 @@ async def async_setup(hass): async def websocket_list_devices(hass, connection, msg): """Handle list devices command.""" registry = await async_get_registry(hass) - connection.send_message(websocket_api.result_message( - msg['id'], [_entry_dict(entry) for entry in registry.devices.values()] - )) + connection.send_message( + websocket_api.result_message( + msg["id"], [_entry_dict(entry) for entry in registry.devices.values()] + ) + ) @require_admin @@ -48,28 +53,26 @@ async def websocket_update_device(hass, connection, msg): """Handle update area websocket command.""" registry = await async_get_registry(hass) - msg.pop('type') - msg_id = msg.pop('id') + msg.pop("type") + msg_id = msg.pop("id") entry = registry.async_update_device(**msg) - connection.send_message(websocket_api.result_message( - msg_id, _entry_dict(entry) - )) + connection.send_message(websocket_api.result_message(msg_id, _entry_dict(entry))) @callback def _entry_dict(entry): """Convert entry to API format.""" return { - 'config_entries': list(entry.config_entries), - 'connections': list(entry.connections), - 'manufacturer': entry.manufacturer, - 'model': entry.model, - 'name': entry.name, - 'sw_version': entry.sw_version, - 'id': entry.id, - 'via_device_id': entry.via_device_id, - 'area_id': entry.area_id, - 'name_by_user': entry.name_by_user, + "config_entries": list(entry.config_entries), + "connections": list(entry.connections), + "manufacturer": entry.manufacturer, + "model": entry.model, + "name": entry.name, + "sw_version": entry.sw_version, + "id": entry.id, + "via_device_id": entry.via_device_id, + "area_id": entry.area_id, + "name_by_user": entry.name_by_user, } diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 341b05f966b..431723893c1 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -6,53 +6,51 @@ from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.components import websocket_api from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.websocket_api.decorators import ( - async_response, require_admin) + async_response, + require_admin, +) from homeassistant.helpers import config_validation as cv -WS_TYPE_LIST = 'config/entity_registry/list' -SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_LIST, -}) +WS_TYPE_LIST = "config/entity_registry/list" +SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_LIST} +) -WS_TYPE_GET = 'config/entity_registry/get' -SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET, - vol.Required('entity_id'): cv.entity_id -}) +WS_TYPE_GET = "config/entity_registry/get" +SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_GET, vol.Required("entity_id"): cv.entity_id} +) -WS_TYPE_UPDATE = 'config/entity_registry/update' -SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_UPDATE, - vol.Required('entity_id'): cv.entity_id, - # If passed in, we update value. Passing None will remove old value. - vol.Optional('name'): vol.Any(str, None), - vol.Optional('new_entity_id'): str, -}) +WS_TYPE_UPDATE = "config/entity_registry/update" +SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_UPDATE, + vol.Required("entity_id"): cv.entity_id, + # If passed in, we update value. Passing None will remove old value. + vol.Optional("name"): vol.Any(str, None), + vol.Optional("new_entity_id"): str, + } +) -WS_TYPE_REMOVE = 'config/entity_registry/remove' -SCHEMA_WS_REMOVE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_REMOVE, - vol.Required('entity_id'): cv.entity_id -}) +WS_TYPE_REMOVE = "config/entity_registry/remove" +SCHEMA_WS_REMOVE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_REMOVE, vol.Required("entity_id"): cv.entity_id} +) async def async_setup(hass): """Enable the Entity Registry views.""" hass.components.websocket_api.async_register_command( - WS_TYPE_LIST, websocket_list_entities, - SCHEMA_WS_LIST + WS_TYPE_LIST, websocket_list_entities, SCHEMA_WS_LIST ) hass.components.websocket_api.async_register_command( - WS_TYPE_GET, websocket_get_entity, - SCHEMA_WS_GET + WS_TYPE_GET, websocket_get_entity, SCHEMA_WS_GET ) hass.components.websocket_api.async_register_command( - WS_TYPE_UPDATE, websocket_update_entity, - SCHEMA_WS_UPDATE + WS_TYPE_UPDATE, websocket_update_entity, SCHEMA_WS_UPDATE ) hass.components.websocket_api.async_register_command( - WS_TYPE_REMOVE, websocket_remove_entity, - SCHEMA_WS_REMOVE + WS_TYPE_REMOVE, websocket_remove_entity, SCHEMA_WS_REMOVE ) return True @@ -64,9 +62,11 @@ async def websocket_list_entities(hass, connection, msg): Async friendly. """ registry = await async_get_registry(hass) - connection.send_message(websocket_api.result_message( - msg['id'], [_entry_dict(entry) for entry in registry.entities.values()] - )) + connection.send_message( + websocket_api.result_message( + msg["id"], [_entry_dict(entry) for entry in registry.entities.values()] + ) + ) @async_response @@ -76,16 +76,15 @@ async def websocket_get_entity(hass, connection, msg): Async friendly. """ registry = await async_get_registry(hass) - entry = registry.entities.get(msg['entity_id']) + entry = registry.entities.get(msg["entity_id"]) if entry is None: - connection.send_message(websocket_api.error_message( - msg['id'], ERR_NOT_FOUND, 'Entity not found')) + connection.send_message( + websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found") + ) return - connection.send_message(websocket_api.result_message( - msg['id'], _entry_dict(entry) - )) + connection.send_message(websocket_api.result_message(msg["id"], _entry_dict(entry))) @require_admin @@ -97,35 +96,38 @@ async def websocket_update_entity(hass, connection, msg): """ registry = await async_get_registry(hass) - if msg['entity_id'] not in registry.entities: - connection.send_message(websocket_api.error_message( - msg['id'], ERR_NOT_FOUND, 'Entity not found')) + if msg["entity_id"] not in registry.entities: + connection.send_message( + websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found") + ) return changes = {} - if 'name' in msg: - changes['name'] = msg['name'] + if "name" in msg: + changes["name"] = msg["name"] - if 'new_entity_id' in msg and msg['new_entity_id'] != msg['entity_id']: - changes['new_entity_id'] = msg['new_entity_id'] - if hass.states.get(msg['new_entity_id']) is not None: - connection.send_message(websocket_api.error_message( - msg['id'], 'invalid_info', 'Entity is already registered')) + if "new_entity_id" in msg and msg["new_entity_id"] != msg["entity_id"]: + changes["new_entity_id"] = msg["new_entity_id"] + if hass.states.get(msg["new_entity_id"]) is not None: + connection.send_message( + websocket_api.error_message( + msg["id"], "invalid_info", "Entity is already registered" + ) + ) return try: if changes: - entry = registry.async_update_entity( - msg['entity_id'], **changes) + entry = registry.async_update_entity(msg["entity_id"], **changes) except ValueError as err: - connection.send_message(websocket_api.error_message( - msg['id'], 'invalid_info', str(err) - )) + connection.send_message( + websocket_api.error_message(msg["id"], "invalid_info", str(err)) + ) else: - connection.send_message(websocket_api.result_message( - msg['id'], _entry_dict(entry) - )) + connection.send_message( + websocket_api.result_message(msg["id"], _entry_dict(entry)) + ) @require_admin @@ -137,23 +139,24 @@ async def websocket_remove_entity(hass, connection, msg): """ registry = await async_get_registry(hass) - if msg['entity_id'] not in registry.entities: - connection.send_message(websocket_api.error_message( - msg['id'], ERR_NOT_FOUND, 'Entity not found')) + if msg["entity_id"] not in registry.entities: + connection.send_message( + websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found") + ) return - registry.async_remove(msg['entity_id']) - connection.send_message(websocket_api.result_message(msg['id'])) + registry.async_remove(msg["entity_id"]) + connection.send_message(websocket_api.result_message(msg["id"])) @callback def _entry_dict(entry): """Convert entry to API format.""" return { - 'config_entry_id': entry.config_entry_id, - 'device_id': entry.device_id, - 'disabled_by': entry.disabled_by, - 'entity_id': entry.entity_id, - 'name': entry.name, - 'platform': entry.platform, + "config_entry_id": entry.config_entry_id, + "device_id": entry.device_id, + "disabled_by": entry.disabled_by, + "entity_id": entry.entity_id, + "name": entry.name, + "platform": entry.platform, } diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index 60421bcc125..371bd98cf08 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -5,17 +5,19 @@ import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView -CONFIG_PATH = 'groups.yaml' +CONFIG_PATH = "groups.yaml" async def async_setup(hass): """Set up the Group config API.""" + async def hook(hass): """post_write_hook for Config View that reloads groups.""" await hass.services.async_call(DOMAIN, SERVICE_RELOAD) - hass.http.register_view(EditKeyBasedConfigView( - 'group', 'config', CONFIG_PATH, cv.slug, GROUP_SCHEMA, - post_write_hook=hook - )) + hass.http.register_view( + EditKeyBasedConfigView( + "group", "config", CONFIG_PATH, cv.slug, GROUP_SCHEMA, post_write_hook=hook + ) + ) return True diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index c8a58e5d72a..8ce163745f1 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -5,17 +5,24 @@ import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView -CONFIG_PATH = 'scripts.yaml' +CONFIG_PATH = "scripts.yaml" async def async_setup(hass): """Set up the script config API.""" + async def hook(hass): """post_write_hook for Config View that reloads scripts.""" await hass.services.async_call(DOMAIN, SERVICE_RELOAD) - hass.http.register_view(EditKeyBasedConfigView( - 'script', 'config', CONFIG_PATH, cv.slug, SCRIPT_ENTRY_SCHEMA, - post_write_hook=hook - )) + hass.http.register_view( + EditKeyBasedConfigView( + "script", + "config", + CONFIG_PATH, + cv.slug, + SCRIPT_ENTRY_SCHEMA, + post_write_hook=hook, + ) + ) return True diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index e7e39968401..eaed84fe24d 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -13,16 +13,21 @@ import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView _LOGGER = logging.getLogger(__name__) -CONFIG_PATH = 'zwave_device_config.yaml' -OZW_LOG_FILENAME = 'OZW_Log.txt' +CONFIG_PATH = "zwave_device_config.yaml" +OZW_LOG_FILENAME = "OZW_Log.txt" async def async_setup(hass): """Set up the Z-Wave config API.""" - hass.http.register_view(EditKeyBasedConfigView( - 'zwave', 'device_config', CONFIG_PATH, cv.entity_id, - DEVICE_CONFIG_SCHEMA_ENTRY - )) + hass.http.register_view( + EditKeyBasedConfigView( + "zwave", + "device_config", + CONFIG_PATH, + cv.entity_id, + DEVICE_CONFIG_SCHEMA_ENTRY, + ) + ) hass.http.register_view(ZWaveNodeValueView) hass.http.register_view(ZWaveNodeGroupView) hass.http.register_view(ZWaveNodeConfigView) @@ -40,23 +45,23 @@ class ZWaveLogView(HomeAssistantView): url = "/api/zwave/ozwlog" name = "api:zwave:ozwlog" -# pylint: disable=no-self-use + # pylint: disable=no-self-use async def get(self, request): """Retrieve the lines from ZWave log.""" try: - lines = int(request.query.get('lines', 0)) + lines = int(request.query.get("lines", 0)) except ValueError: - return Response(text='Invalid datetime', status=400) + return Response(text="Invalid datetime", status=400) - hass = request.app['hass'] + hass = request.app["hass"] response = await hass.async_add_job(self._get_log, hass, lines) - return Response(text='\n'.join(response)) + return Response(text="\n".join(response)) def _get_log(self, hass, lines): """Retrieve the logfile content.""" logfilepath = hass.config.path(OZW_LOG_FILENAME) - with open(logfilepath, 'r') as logfile: + with open(logfilepath, "r") as logfile: data = (line.rstrip() for line in logfile) if lines == 0: loglines = list(data) @@ -74,15 +79,13 @@ class ZWaveConfigWriteView(HomeAssistantView): @ha.callback def post(self, request): """Save cache configuration to zwcfg_xxxxx.xml.""" - hass = request.app['hass'] + hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) if network is None: - return self.json_message('No Z-Wave network data found', - HTTP_NOT_FOUND) + return self.json_message("No Z-Wave network data found", HTTP_NOT_FOUND) _LOGGER.info("Z-Wave configuration written to file.") network.write_config() - return self.json_message('Z-Wave configuration saved to file.', - HTTP_OK) + return self.json_message("Z-Wave configuration saved to file.", HTTP_OK) class ZWaveNodeValueView(HomeAssistantView): @@ -95,7 +98,7 @@ class ZWaveNodeValueView(HomeAssistantView): def get(self, request, node_id): """Retrieve groups of node.""" nodeid = int(node_id) - hass = request.app['hass'] + hass = request.app["hass"] values_list = hass.data[const.DATA_ENTITY_VALUES] values_data = {} @@ -106,10 +109,10 @@ class ZWaveNodeValueView(HomeAssistantView): continue values_data[entity_values.primary.value_id] = { - 'label': entity_values.primary.label, - 'index': entity_values.primary.index, - 'instance': entity_values.primary.instance, - 'poll_intensity': entity_values.primary.poll_intensity, + "label": entity_values.primary.label, + "index": entity_values.primary.index, + "instance": entity_values.primary.instance, + "poll_intensity": entity_values.primary.poll_intensity, } return self.json(values_data) @@ -124,19 +127,20 @@ class ZWaveNodeGroupView(HomeAssistantView): def get(self, request, node_id): """Retrieve groups of node.""" nodeid = int(node_id) - hass = request.app['hass'] + hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) node = network.nodes.get(nodeid) if node is None: - return self.json_message('Node not found', HTTP_NOT_FOUND) + return self.json_message("Node not found", HTTP_NOT_FOUND) groupdata = node.groups groups = {} for key, value in groupdata.items(): - groups[key] = {'associations': value.associations, - 'association_instances': - value.associations_instances, - 'label': value.label, - 'max_associations': value.max_associations} + groups[key] = { + "associations": value.associations, + "association_instances": value.associations_instances, + "label": value.label, + "max_associations": value.max_associations, + } return self.json(groups) @@ -150,22 +154,24 @@ class ZWaveNodeConfigView(HomeAssistantView): def get(self, request, node_id): """Retrieve configurations of node.""" nodeid = int(node_id) - hass = request.app['hass'] + hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) node = network.nodes.get(nodeid) if node is None: - return self.json_message('Node not found', HTTP_NOT_FOUND) + return self.json_message("Node not found", HTTP_NOT_FOUND) config = {} - for value in ( - node.get_values(class_id=const.COMMAND_CLASS_CONFIGURATION) - .values()): - config[value.index] = {'label': value.label, - 'type': value.type, - 'help': value.help, - 'data_items': value.data_items, - 'data': value.data, - 'max': value.max, - 'min': value.min} + for value in node.get_values( + class_id=const.COMMAND_CLASS_CONFIGURATION + ).values(): + config[value.index] = { + "label": value.label, + "type": value.type, + "help": value.help, + "data_items": value.data_items, + "data": value.data, + "max": value.max, + "min": value.min, + } return self.json(config) @@ -179,22 +185,22 @@ class ZWaveUserCodeView(HomeAssistantView): def get(self, request, node_id): """Retrieve usercodes of node.""" nodeid = int(node_id) - hass = request.app['hass'] + hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) node = network.nodes.get(nodeid) if node is None: - return self.json_message('Node not found', HTTP_NOT_FOUND) + return self.json_message("Node not found", HTTP_NOT_FOUND) usercodes = {} if not node.has_command_class(const.COMMAND_CLASS_USER_CODE): return self.json(usercodes) - for value in ( - node.get_values(class_id=const.COMMAND_CLASS_USER_CODE) - .values()): + for value in node.get_values(class_id=const.COMMAND_CLASS_USER_CODE).values(): if value.genre != const.GENRE_USER: continue - usercodes[value.index] = {'code': value.data, - 'label': value.label, - 'length': len(value.data)} + usercodes[value.index] = { + "code": value.data, + "label": value.label, + "length": len(value.data), + } return self.json(usercodes) @@ -207,22 +213,23 @@ class ZWaveProtectionView(HomeAssistantView): async def get(self, request, node_id): """Retrieve the protection commandclass options of node.""" nodeid = int(node_id) - hass = request.app['hass'] + hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) def _fetch_protection(): """Get protection data.""" node = network.nodes.get(nodeid) if node is None: - return self.json_message('Node not found', HTTP_NOT_FOUND) + return self.json_message("Node not found", HTTP_NOT_FOUND) protection_options = {} if not node.has_command_class(const.COMMAND_CLASS_PROTECTION): return self.json(protection_options) protections = node.get_protections() protection_options = { - 'value_id': '{0:d}'.format(list(protections)[0]), - 'selected': node.get_protection_item(list(protections)[0]), - 'options': node.get_protection_items(list(protections)[0])} + "value_id": "{0:d}".format(list(protections)[0]), + "selected": node.get_protection_item(list(protections)[0]), + "options": node.get_protection_items(list(protections)[0]), + } return self.json(protection_options) return await hass.async_add_executor_job(_fetch_protection) @@ -230,7 +237,7 @@ class ZWaveProtectionView(HomeAssistantView): async def post(self, request, node_id): """Change the selected option in protection commandclass.""" nodeid = int(node_id) - hass = request.app['hass'] + hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) protection_data = await request.json() @@ -240,15 +247,14 @@ class ZWaveProtectionView(HomeAssistantView): selection = protection_data["selection"] value_id = int(protection_data[const.ATTR_VALUE_ID]) if node is None: - return self.json_message('Node not found', HTTP_NOT_FOUND) + return self.json_message("Node not found", HTTP_NOT_FOUND) if not node.has_command_class(const.COMMAND_CLASS_PROTECTION): return self.json_message( - 'No protection commandclass on this node', HTTP_NOT_FOUND) + "No protection commandclass on this node", HTTP_NOT_FOUND + ) state = node.set_protection(value_id, selection) if not state: - return self.json_message( - 'Protection setting did not complete', 202) - return self.json_message( - 'Protection setting succsessfully set', HTTP_OK) + return self.json_message("Protection setting did not complete", 202) + return self.json_message("Protection setting succsessfully set", HTTP_OK) return await hass.async_add_executor_job(_set_protection) diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index 74d8339b1fa..99995959c23 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -10,50 +10,61 @@ import functools as ft import logging from homeassistant.core import callback as async_callback -from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \ - ATTR_ENTITY_PICTURE +from homeassistant.const import ( + EVENT_TIME_CHANGED, + ATTR_FRIENDLY_NAME, + ATTR_ENTITY_PICTURE, +) from homeassistant.loader import bind_hass from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) -_KEY_INSTANCE = 'configurator' +_KEY_INSTANCE = "configurator" -DATA_REQUESTS = 'configurator_requests' +DATA_REQUESTS = "configurator_requests" -ATTR_CONFIGURE_ID = 'configure_id' -ATTR_DESCRIPTION = 'description' -ATTR_DESCRIPTION_IMAGE = 'description_image' -ATTR_ERRORS = 'errors' -ATTR_FIELDS = 'fields' -ATTR_LINK_NAME = 'link_name' -ATTR_LINK_URL = 'link_url' -ATTR_SUBMIT_CAPTION = 'submit_caption' +ATTR_CONFIGURE_ID = "configure_id" +ATTR_DESCRIPTION = "description" +ATTR_DESCRIPTION_IMAGE = "description_image" +ATTR_ERRORS = "errors" +ATTR_FIELDS = "fields" +ATTR_LINK_NAME = "link_name" +ATTR_LINK_URL = "link_url" +ATTR_SUBMIT_CAPTION = "submit_caption" -DOMAIN = 'configurator' +DOMAIN = "configurator" -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" -SERVICE_CONFIGURE = 'configure' -STATE_CONFIGURE = 'configure' -STATE_CONFIGURED = 'configured' +SERVICE_CONFIGURE = "configure" +STATE_CONFIGURE = "configure" +STATE_CONFIGURED = "configured" @bind_hass @async_callback def async_request_config( - hass, name, callback=None, description=None, description_image=None, - submit_caption=None, fields=None, link_name=None, link_url=None, - entity_picture=None): + hass, + name, + callback=None, + description=None, + description_image=None, + submit_caption=None, + fields=None, + link_name=None, + link_url=None, + entity_picture=None, +): """Create a new request for configuration. Will return an ID to be used for sequent calls. """ if link_name is not None and link_url is not None: - description += '\n\n[{}]({})'.format(link_name, link_url) + description += "\n\n[{}]({})".format(link_name, link_url) if description_image is not None: - description += '\n\n![Description image]({})'.format(description_image) + description += "\n\n![Description image]({})".format(description_image) instance = hass.data.get(_KEY_INSTANCE) @@ -61,7 +72,8 @@ def async_request_config( instance = hass.data[_KEY_INSTANCE] = Configurator(hass) request_id = instance.async_request_config( - name, callback, description, submit_caption, fields, entity_picture) + name, callback, description, submit_caption, fields, entity_picture + ) if DATA_REQUESTS not in hass.data: hass.data[DATA_REQUESTS] = {} @@ -87,8 +99,7 @@ def request_config(hass, *args, **kwargs): def async_notify_errors(hass, request_id, error): """Add errors to a config request.""" try: - hass.data[DATA_REQUESTS][request_id].async_notify_errors( - request_id, error) + hass.data[DATA_REQUESTS][request_id].async_notify_errors(request_id, error) except KeyError: # If request_id does not exist pass @@ -135,15 +146,15 @@ class Configurator: self._cur_id = 0 self._requests = {} hass.services.async_register( - DOMAIN, SERVICE_CONFIGURE, self.async_handle_service_call) + DOMAIN, SERVICE_CONFIGURE, self.async_handle_service_call + ) @async_callback def async_request_config( - self, name, callback, description, submit_caption, fields, - entity_picture): + self, name, callback, description, submit_caption, fields, entity_picture + ): """Set up a request for configuration.""" - entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, name, hass=self.hass) + entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass) if fields is None: fields = [] @@ -159,12 +170,16 @@ class Configurator: ATTR_ENTITY_PICTURE: entity_picture, } - data.update({ - key: value for key, value in [ - (ATTR_DESCRIPTION, description), - (ATTR_SUBMIT_CAPTION, submit_caption), - ] if value is not None - }) + data.update( + { + key: value + for key, value in [ + (ATTR_DESCRIPTION, description), + (ATTR_SUBMIT_CAPTION, submit_caption), + ] + if value is not None + } + ) self.hass.states.async_set(entity_id, STATE_CONFIGURE, data) @@ -217,8 +232,7 @@ class Configurator: # field validation goes here? if callback: - await self.hass.async_add_job(callback, - call.data.get(ATTR_FIELDS, {})) + await self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {})) def _generate_unique_id(self): """Generate a unique configurator ID.""" diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index bd577127fa0..9d7d510b10e 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -6,8 +6,7 @@ import voluptuous as vol from homeassistant import core from homeassistant.components import http -from homeassistant.components.cover import ( - INTENT_CLOSE_COVER, INTENT_OPEN_COVER) +from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import callback @@ -19,31 +18,36 @@ from .util import create_matcher _LOGGER = logging.getLogger(__name__) -ATTR_TEXT = 'text' +ATTR_TEXT = "text" -DOMAIN = 'conversation' +DOMAIN = "conversation" -REGEX_TURN_COMMAND = re.compile(r'turn (?P(?: |\w)+) (?P\w+)') -REGEX_TYPE = type(re.compile('')) +REGEX_TURN_COMMAND = re.compile(r"turn (?P(?: |\w)+) (?P\w+)") +REGEX_TYPE = type(re.compile("")) UTTERANCES = { - 'cover': { - INTENT_OPEN_COVER: ['Open [the] [a] [an] {name}[s]'], - INTENT_CLOSE_COVER: ['Close [the] [a] [an] {name}[s]'] + "cover": { + INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"], + INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"], } } -SERVICE_PROCESS = 'process' +SERVICE_PROCESS = "process" -SERVICE_PROCESS_SCHEMA = vol.Schema({ - vol.Required(ATTR_TEXT): cv.string, -}) +SERVICE_PROCESS_SCHEMA = vol.Schema({vol.Required(ATTR_TEXT): cv.string}) -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({ - vol.Optional('intents'): vol.Schema({ - cv.string: vol.All(cv.ensure_list, [cv.string]) - }) -})}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional("intents"): vol.Schema( + {cv.string: vol.All(cv.ensure_list, [cv.string])} + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) @core.callback @@ -79,7 +83,7 @@ async def async_setup(hass, config): if intents is None: intents = hass.data[DOMAIN] = {} - for intent_type, utterances in config.get('intents', {}).items(): + for intent_type, utterances in config.get("intents", {}).items(): conf = intents.get(intent_type) if conf is None: @@ -90,14 +94,15 @@ async def async_setup(hass, config): async def process(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] - _LOGGER.debug('Processing: <%s>', text) + _LOGGER.debug("Processing: <%s>", text) try: await _process(hass, text) except intent.IntentHandleError as err: - _LOGGER.error('Error processing %s: %s', text, err) + _LOGGER.error("Error processing %s: %s", text, err) hass.services.async_register( - DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA) + DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA + ) hass.http.register_view(ConversationProcessView) @@ -105,18 +110,21 @@ async def async_setup(hass, config): # if a letter is not there. By removing 's' we can match singular and # plural names. - async_register(hass, intent.INTENT_TURN_ON, [ - 'Turn [the] [a] {name}[s] on', - 'Turn on [the] [a] [an] {name}[s]', - ]) - async_register(hass, intent.INTENT_TURN_OFF, [ - 'Turn [the] [a] [an] {name}[s] off', - 'Turn off [the] [a] [an] {name}[s]', - ]) - async_register(hass, intent.INTENT_TOGGLE, [ - 'Toggle [the] [a] [an] {name}[s]', - '[the] [a] [an] {name}[s] toggle', - ]) + async_register( + hass, + intent.INTENT_TURN_ON, + ["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"], + ) + async_register( + hass, + intent.INTENT_TURN_OFF, + ["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"], + ) + async_register( + hass, + intent.INTENT_TOGGLE, + ["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"], + ) @callback def register_utterances(component): @@ -152,27 +160,27 @@ async def _process(hass, text): continue response = await hass.helpers.intent.async_handle( - DOMAIN, intent_type, - {key: {'value': value} for key, value - in match.groupdict().items()}, text) + DOMAIN, + intent_type, + {key: {"value": value} for key, value in match.groupdict().items()}, + text, + ) return response class ConversationProcessView(http.HomeAssistantView): """View to retrieve shopping list content.""" - url = '/api/conversation/process' + url = "/api/conversation/process" name = "api:conversation:process" - @RequestDataValidator(vol.Schema({ - vol.Required('text'): str, - })) + @RequestDataValidator(vol.Schema({vol.Required("text"): str})) async def post(self, request, data): """Send a request for processing.""" - hass = request.app['hass'] + hass = request.app["hass"] try: - intent_result = await _process(hass, data['text']) + intent_result = await _process(hass, data["text"]) except intent.IntentHandleError as err: intent_result = intent.IntentResponse() intent_result.async_set_speech(str(err)) diff --git a/homeassistant/components/conversation/util.py b/homeassistant/components/conversation/util.py index 60d861afdbe..4904cb9f990 100644 --- a/homeassistant/components/conversation/util.py +++ b/homeassistant/components/conversation/util.py @@ -6,13 +6,13 @@ def create_matcher(utterance): """Create a regex that matches the utterance.""" # Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL # Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name} - parts = re.split(r'({\w+}|\[[\w\s]+\] *)', utterance) + parts = re.split(r"({\w+}|\[[\w\s]+\] *)", utterance) # Pattern to extract name from GROUP part. Matches {name} - group_matcher = re.compile(r'{(\w+)}') + group_matcher = re.compile(r"{(\w+)}") # Pattern to extract text from OPTIONAL part. Matches [the color] - optional_matcher = re.compile(r'\[([\w ]+)\] *') + optional_matcher = re.compile(r"\[([\w ]+)\] *") - pattern = ['^'] + pattern = ["^"] for part in parts: group_match = group_matcher.match(part) optional_match = optional_matcher.match(part) @@ -24,12 +24,11 @@ def create_matcher(utterance): # Group part if group_match is not None: - pattern.append( - r'(?P<{}>[\w ]+?)\s*'.format(group_match.groups()[0])) + pattern.append(r"(?P<{}>[\w ]+?)\s*".format(group_match.groups()[0])) # Optional part elif optional_match is not None: - pattern.append(r'(?:{} *)?'.format(optional_match.groups()[0])) + pattern.append(r"(?:{} *)?".format(optional_match.groups()[0])) - pattern.append('$') - return re.compile(''.join(pattern), re.I) + pattern.append("$") + return re.compile("".join(pattern), re.I) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 378a1c0c281..7379d66777b 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -6,39 +6,59 @@ import voluptuous as vol from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate.const import ( - HVAC_MODE_OFF, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, SUPPORT_FAN_MODE, - SUPPORT_TARGET_TEMPERATURE) + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, +) from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_HOST, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_TEMPERATURE, + CONF_HOST, + CONF_PORT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) import homeassistant.helpers.config_validation as cv -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE) +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE DEFAULT_PORT = 10102 -AVAILABLE_MODES = [HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL, - HVAC_MODE_DRY, HVAC_MODE_AUTO, HVAC_MODE_FAN_ONLY] +AVAILABLE_MODES = [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_AUTO, + HVAC_MODE_FAN_ONLY, +] CM_TO_HA_STATE = { - 'heat': HVAC_MODE_HEAT, - 'cool': HVAC_MODE_COOL, - 'auto': HVAC_MODE_AUTO, - 'dry': HVAC_MODE_DRY, - 'fan': HVAC_MODE_FAN_ONLY, + "heat": HVAC_MODE_HEAT, + "cool": HVAC_MODE_COOL, + "auto": HVAC_MODE_AUTO, + "dry": HVAC_MODE_DRY, + "fan": HVAC_MODE_FAN_ONLY, } HA_STATE_TO_CM = {value: key for key, value in CM_TO_HA_STATE.items()} -FAN_MODES = ['low', 'med', 'high', 'auto'] +FAN_MODES = ["low", "med", "high", "auto"] -CONF_SUPPORTED_MODES = 'supported_modes' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SUPPORTED_MODES, default=AVAILABLE_MODES): - vol.All(cv.ensure_list, [vol.In(AVAILABLE_MODES)]), -}) +CONF_SUPPORTED_MODES = "supported_modes" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SUPPORTED_MODES, default=AVAILABLE_MODES): vol.All( + cv.ensure_list, [vol.In(AVAILABLE_MODES)] + ), + } +) _LOGGER = logging.getLogger(__name__) @@ -58,8 +78,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): cool = CoolMasterNet(host, port=port) devices = cool.devices() - all_devices = [_build_entity(device, supported_modes) - for device in devices] + all_devices = [_build_entity(device, supported_modes) for device in devices] add_entities(all_devices, True) @@ -83,18 +102,18 @@ class CoolmasterClimate(ClimateDevice): def update(self): """Pull state from CoolMasterNet.""" status = self._device.status - self._target_temperature = status['thermostat'] - self._current_temperature = status['temperature'] - self._current_fan_mode = status['fan_speed'] - self._on = status['is_on'] + self._target_temperature = status["thermostat"] + self._current_temperature = status["temperature"] + self._current_fan_mode = status["fan_speed"] + self._on = status["is_on"] - device_mode = status['mode'] + device_mode = status["mode"] if self._on: self._hvac_mode = CM_TO_HA_STATE[device_mode] else: self._hvac_mode = HVAC_MODE_OFF - if status['unit'] == 'celsius': + if status["unit"] == "celsius": self._unit = TEMP_CELSIUS else: self._unit = TEMP_FAHRENHEIT @@ -153,20 +172,17 @@ class CoolmasterClimate(ClimateDevice): """Set new target temperatures.""" temp = kwargs.get(ATTR_TEMPERATURE) if temp is not None: - _LOGGER.debug("Setting temp of %s to %s", self.unique_id, - str(temp)) + _LOGGER.debug("Setting temp of %s to %s", self.unique_id, str(temp)) self._device.set_thermostat(str(temp)) def set_fan_mode(self, fan_mode): """Set new fan mode.""" - _LOGGER.debug("Setting fan mode of %s to %s", self.unique_id, - fan_mode) + _LOGGER.debug("Setting fan mode of %s to %s", self.unique_id, fan_mode) self._device.set_fan_speed(fan_mode) def set_hvac_mode(self, hvac_mode): """Set new operation mode.""" - _LOGGER.debug("Setting operation mode of %s to %s", self.unique_id, - hvac_mode) + _LOGGER.debug("Setting operation mode of %s to %s", self.unique_id, hvac_mode) if hvac_mode == HVAC_MODE_OFF: self.turn_off() diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 53aa21c91c6..79877d63f14 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -3,62 +3,68 @@ import logging import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME,\ - CONF_MAXIMUM, CONF_MINIMUM +from homeassistant.const import CONF_ICON, CONF_NAME, CONF_MAXIMUM, CONF_MINIMUM import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) -ATTR_INITIAL = 'initial' -ATTR_STEP = 'step' -ATTR_MINIMUM = 'minimum' -ATTR_MAXIMUM = 'maximum' +ATTR_INITIAL = "initial" +ATTR_STEP = "step" +ATTR_MINIMUM = "minimum" +ATTR_MAXIMUM = "maximum" -CONF_INITIAL = 'initial' -CONF_RESTORE = 'restore' -CONF_STEP = 'step' +CONF_INITIAL = "initial" +CONF_RESTORE = "restore" +CONF_STEP = "step" DEFAULT_INITIAL = 0 DEFAULT_STEP = 1 -DOMAIN = 'counter' +DOMAIN = "counter" -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" -SERVICE_DECREMENT = 'decrement' -SERVICE_INCREMENT = 'increment' -SERVICE_RESET = 'reset' -SERVICE_CONFIGURE = 'configure' +SERVICE_DECREMENT = "decrement" +SERVICE_INCREMENT = "increment" +SERVICE_RESET = "reset" +SERVICE_CONFIGURE = "configure" -SERVICE_SCHEMA_SIMPLE = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, -}) +SERVICE_SCHEMA_CONFIGURE = ENTITY_SERVICE_SCHEMA.extend( + { + vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_STEP): cv.positive_int, + } +) -SERVICE_SCHEMA_CONFIGURE = vol.Schema({ - ATTR_ENTITY_ID: cv.comp_entity_ids, - vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(ATTR_STEP): cv.positive_int, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys( - vol.Any({ - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): - cv.positive_int, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MAXIMUM, default=None): - vol.Any(None, vol.Coerce(int)), - vol.Optional(CONF_MINIMUM, default=None): - vol.Any(None, vol.Coerce(int)), - vol.Optional(CONF_RESTORE, default=True): cv.boolean, - vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, - }, None) - ) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: cv.schema_with_slug_keys( + vol.Any( + { + vol.Optional(CONF_ICON): cv.icon, + vol.Optional( + CONF_INITIAL, default=DEFAULT_INITIAL + ): cv.positive_int, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MAXIMUM, default=None): vol.Any( + None, vol.Coerce(int) + ), + vol.Optional(CONF_MINIMUM, default=None): vol.Any( + None, vol.Coerce(int) + ), + vol.Optional(CONF_RESTORE, default=True): cv.boolean, + vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, + }, + None, + ) + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -79,24 +85,25 @@ async def async_setup(hass, config): minimum = cfg.get(CONF_MINIMUM) maximum = cfg.get(CONF_MAXIMUM) - entities.append(Counter(object_id, name, initial, minimum, maximum, - restore, step, icon)) + entities.append( + Counter(object_id, name, initial, minimum, maximum, restore, step, icon) + ) if not entities: return False component.async_register_entity_service( - SERVICE_INCREMENT, SERVICE_SCHEMA_SIMPLE, - 'async_increment') + SERVICE_INCREMENT, ENTITY_SERVICE_SCHEMA, "async_increment" + ) component.async_register_entity_service( - SERVICE_DECREMENT, SERVICE_SCHEMA_SIMPLE, - 'async_decrement') + SERVICE_DECREMENT, ENTITY_SERVICE_SCHEMA, "async_decrement" + ) component.async_register_entity_service( - SERVICE_RESET, SERVICE_SCHEMA_SIMPLE, - 'async_reset') + SERVICE_RESET, ENTITY_SERVICE_SCHEMA, "async_reset" + ) component.async_register_entity_service( - SERVICE_CONFIGURE, SERVICE_SCHEMA_CONFIGURE, - 'async_configure') + SERVICE_CONFIGURE, SERVICE_SCHEMA_CONFIGURE, "async_configure" + ) await component.async_add_entities(entities) return True @@ -105,8 +112,7 @@ async def async_setup(hass, config): class Counter(RestoreEntity): """Representation of a counter.""" - def __init__(self, object_id, name, initial, minimum, maximum, - restore, step, icon): + def __init__(self, object_id, name, initial, minimum, maximum, restore, step, icon): """Initialize a counter.""" self.entity_id = ENTITY_ID_FORMAT.format(object_id) self._name = name @@ -140,10 +146,7 @@ class Counter(RestoreEntity): @property def state_attributes(self): """Return the state attributes.""" - ret = { - ATTR_INITIAL: self._initial, - ATTR_STEP: self._step, - } + ret = {ATTR_INITIAL: self._initial, ATTR_STEP: self._step} if self._min is not None: ret[CONF_MINIMUM] = self._min if self._max is not None: diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 4e90a7bd186..696524f5792 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -9,37 +9,49 @@ from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import ( # noqa - PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) -import homeassistant.helpers.config_validation as cv + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.components import group from homeassistant.helpers import intent from homeassistant.const import ( - SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, - SERVICE_STOP_COVER, SERVICE_TOGGLE, SERVICE_OPEN_COVER_TILT, - SERVICE_CLOSE_COVER_TILT, SERVICE_STOP_COVER_TILT, - SERVICE_SET_COVER_TILT_POSITION, SERVICE_TOGGLE_COVER_TILT, - STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING, ATTR_ENTITY_ID) + SERVICE_OPEN_COVER, + SERVICE_CLOSE_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + SERVICE_TOGGLE, + SERVICE_OPEN_COVER_TILT, + SERVICE_CLOSE_COVER_TILT, + SERVICE_STOP_COVER_TILT, + SERVICE_SET_COVER_TILT_POSITION, + SERVICE_TOGGLE_COVER_TILT, + STATE_OPEN, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, +) _LOGGER = logging.getLogger(__name__) -DOMAIN = 'cover' +DOMAIN = "cover" SCAN_INTERVAL = timedelta(seconds=15) -GROUP_NAME_ALL_COVERS = 'all covers' -ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers') +GROUP_NAME_ALL_COVERS = "all covers" +ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format("all_covers") -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" # Refer to the cover dev docs for device class descriptions -DEVICE_CLASS_AWNING = 'awning' -DEVICE_CLASS_BLIND = 'blind' -DEVICE_CLASS_CURTAIN = 'curtain' -DEVICE_CLASS_DAMPER = 'damper' -DEVICE_CLASS_DOOR = 'door' -DEVICE_CLASS_GARAGE = 'garage' -DEVICE_CLASS_SHADE = 'shade' -DEVICE_CLASS_SHUTTER = 'shutter' -DEVICE_CLASS_WINDOW = 'window' +DEVICE_CLASS_AWNING = "awning" +DEVICE_CLASS_BLIND = "blind" +DEVICE_CLASS_CURTAIN = "curtain" +DEVICE_CLASS_DAMPER = "damper" +DEVICE_CLASS_DOOR = "door" +DEVICE_CLASS_GARAGE = "garage" +DEVICE_CLASS_SHADE = "shade" +DEVICE_CLASS_SHUTTER = "shutter" +DEVICE_CLASS_WINDOW = "window" DEVICE_CLASSES = [ DEVICE_CLASS_AWNING, DEVICE_CLASS_BLIND, @@ -49,7 +61,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_GARAGE, DEVICE_CLASS_SHADE, DEVICE_CLASS_SHUTTER, - DEVICE_CLASS_WINDOW + DEVICE_CLASS_WINDOW, ] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) @@ -62,27 +74,25 @@ SUPPORT_CLOSE_TILT = 32 SUPPORT_STOP_TILT = 64 SUPPORT_SET_TILT_POSITION = 128 -ATTR_CURRENT_POSITION = 'current_position' -ATTR_CURRENT_TILT_POSITION = 'current_tilt_position' -ATTR_POSITION = 'position' -ATTR_TILT_POSITION = 'tilt_position' +ATTR_CURRENT_POSITION = "current_position" +ATTR_CURRENT_TILT_POSITION = "current_tilt_position" +ATTR_POSITION = "position" +ATTR_TILT_POSITION = "tilt_position" -INTENT_OPEN_COVER = 'HassOpenCover' -INTENT_CLOSE_COVER = 'HassCloseCover' +INTENT_OPEN_COVER = "HassOpenCover" +INTENT_CLOSE_COVER = "HassCloseCover" -COVER_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, -}) +COVER_SET_COVER_POSITION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_POSITION): vol.All(vol.Coerce(int), vol.Range(min=0, max=100))} +) -COVER_SET_COVER_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_POSITION): - vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), -}) - -COVER_SET_COVER_TILT_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_TILT_POSITION): - vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), -}) +COVER_SET_COVER_TILT_POSITION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + { + vol.Required(ATTR_TILT_POSITION): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + } +) @bind_hass @@ -95,66 +105,65 @@ def is_closed(hass, entity_id=None): async def async_setup(hass, config): """Track states and offer events for covers.""" component = hass.data[DOMAIN] = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS) + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS + ) await component.async_setup(config) component.async_register_entity_service( - SERVICE_OPEN_COVER, COVER_SERVICE_SCHEMA, - 'async_open_cover' + SERVICE_OPEN_COVER, ENTITY_SERVICE_SCHEMA, "async_open_cover" ) component.async_register_entity_service( - SERVICE_CLOSE_COVER, COVER_SERVICE_SCHEMA, - 'async_close_cover' + SERVICE_CLOSE_COVER, ENTITY_SERVICE_SCHEMA, "async_close_cover" ) component.async_register_entity_service( - SERVICE_SET_COVER_POSITION, COVER_SET_COVER_POSITION_SCHEMA, - 'async_set_cover_position' + SERVICE_SET_COVER_POSITION, + COVER_SET_COVER_POSITION_SCHEMA, + "async_set_cover_position", ) component.async_register_entity_service( - SERVICE_STOP_COVER, COVER_SERVICE_SCHEMA, - 'async_stop_cover' + SERVICE_STOP_COVER, ENTITY_SERVICE_SCHEMA, "async_stop_cover" ) component.async_register_entity_service( - SERVICE_TOGGLE, COVER_SERVICE_SCHEMA, - 'async_toggle' + SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" ) component.async_register_entity_service( - SERVICE_OPEN_COVER_TILT, COVER_SERVICE_SCHEMA, - 'async_open_cover_tilt' + SERVICE_OPEN_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_open_cover_tilt" ) component.async_register_entity_service( - SERVICE_CLOSE_COVER_TILT, COVER_SERVICE_SCHEMA, - 'async_close_cover_tilt' + SERVICE_CLOSE_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_close_cover_tilt" ) component.async_register_entity_service( - SERVICE_STOP_COVER_TILT, COVER_SERVICE_SCHEMA, - 'async_stop_cover_tilt' + SERVICE_STOP_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_stop_cover_tilt" ) component.async_register_entity_service( - SERVICE_SET_COVER_TILT_POSITION, COVER_SET_COVER_TILT_POSITION_SCHEMA, - 'async_set_cover_tilt_position' + SERVICE_SET_COVER_TILT_POSITION, + COVER_SET_COVER_TILT_POSITION_SCHEMA, + "async_set_cover_tilt_position", ) component.async_register_entity_service( - SERVICE_TOGGLE_COVER_TILT, COVER_SERVICE_SCHEMA, - 'async_toggle_tilt' + SERVICE_TOGGLE_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_toggle_tilt" ) - hass.helpers.intent.async_register(intent.ServiceIntentHandler( - INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, - "Opened {}")) - hass.helpers.intent.async_register(intent.ServiceIntentHandler( - INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, - "Closed {}")) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, "Opened {}" + ) + ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, "Closed {}" + ) + ) return True @@ -228,8 +237,11 @@ class CoverDevice(Entity): if self.current_cover_tilt_position is not None: supported_features |= ( - SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT | - SUPPORT_SET_TILT_POSITION) + SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT + | SUPPORT_SET_TILT_POSITION + ) return supported_features @@ -295,8 +307,7 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job( - ft.partial(self.set_cover_position, **kwargs)) + return self.hass.async_add_job(ft.partial(self.set_cover_position, **kwargs)) def stop_cover(self, **kwargs): """Stop the cover.""" @@ -318,8 +329,7 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job( - ft.partial(self.open_cover_tilt, **kwargs)) + return self.hass.async_add_job(ft.partial(self.open_cover_tilt, **kwargs)) def close_cover_tilt(self, **kwargs): """Close the cover tilt.""" @@ -330,8 +340,7 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job( - ft.partial(self.close_cover_tilt, **kwargs)) + return self.hass.async_add_job(ft.partial(self.close_cover_tilt, **kwargs)) def set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" @@ -343,7 +352,8 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ return self.hass.async_add_job( - ft.partial(self.set_cover_tilt_position, **kwargs)) + ft.partial(self.set_cover_tilt_position, **kwargs) + ) def stop_cover_tilt(self, **kwargs): """Stop the cover.""" @@ -354,8 +364,7 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job( - ft.partial(self.stop_cover_tilt, **kwargs)) + return self.hass.async_add_job(ft.partial(self.stop_cover_tilt, **kwargs)) def toggle_tilt(self, **kwargs) -> None: """Toggle the entity.""" diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index 608ce6dad6b..c1c62a26dd9 100755 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -5,23 +5,25 @@ from datetime import timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA, DeviceScanner, DOMAIN -) -from homeassistant.const import ( - CONF_HOST, CONF_API_KEY + PLATFORM_SCHEMA, + DeviceScanner, + DOMAIN, ) +from homeassistant.const import CONF_HOST, CONF_API_KEY SCAN_INTERVAL = timedelta(seconds=120) -CLIENT_ID = 'client_id' +CLIENT_ID = "client_id" -GRANT_TYPE = 'client_credentials' +GRANT_TYPE = "client_credentials" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CLIENT_ID): cv.string, - vol.Required(CONF_API_KEY): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CLIENT_ID): cv.string, + vol.Required(CONF_API_KEY): cv.string, + } +) _LOGGER = logging.getLogger(__name__) @@ -29,11 +31,12 @@ _LOGGER = logging.getLogger(__name__) def get_scanner(hass, config): """Initialize Scanner.""" from clearpasspy import ClearPass + data = { - 'server': config[DOMAIN][CONF_HOST], - 'grant_type': GRANT_TYPE, - 'secret': config[DOMAIN][CONF_API_KEY], - 'client': config[DOMAIN][CLIENT_ID] + "server": config[DOMAIN][CONF_HOST], + "grant_type": GRANT_TYPE, + "secret": config[DOMAIN][CONF_API_KEY], + "client": config[DOMAIN][CLIENT_ID], } cppm = ClearPass(data) if cppm.access_token is None: @@ -53,25 +56,22 @@ class CPPMDeviceScanner(DeviceScanner): def scan_devices(self): """Initialize scanner.""" self.get_cppm_data() - return [device['mac'] for device in self.results] + return [device["mac"] for device in self.results] def get_device_name(self, device): """Retrieve device name.""" - name = next(( - result['name'] for result in self.results - if result['mac'] == device), None) + name = next( + (result["name"] for result in self.results if result["mac"] == device), None + ) return name def get_cppm_data(self): """Retrieve data from Aruba Clearpass and return parsed result.""" - endpoints = self._cppm.get_endpoints(100)['_embedded']['items'] + endpoints = self._cppm.get_endpoints(100)["_embedded"]["items"] devices = [] for item in endpoints: - if self._cppm.online_status(item['mac_address']): - device = { - 'mac': item['mac_address'], - 'name': item['mac_address'] - } + if self._cppm.online_status(item["mac_address"]): + device = {"mac": item["mac_address"], "name": item["mac_address"]} devices.append(device) else: continue diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index ef9cb218cd7..9484e770998 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -10,20 +10,20 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_BRAND = 'Brand' -ATTR_HZ = 'GHz Advertised' -ATTR_ARCH = 'arch' +ATTR_BRAND = "Brand" +ATTR_HZ = "GHz Advertised" +ATTR_ARCH = "arch" -HZ_ACTUAL_RAW = 'hz_actual_raw' -HZ_ADVERTISED_RAW = 'hz_advertised_raw' +HZ_ACTUAL_RAW = "hz_actual_raw" +HZ_ADVERTISED_RAW = "hz_advertised_raw" -DEFAULT_NAME = 'CPU speed' +DEFAULT_NAME = "CPU speed" -ICON = 'mdi:pulse' +ICON = "mdi:pulse" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -41,7 +41,7 @@ class CpuSpeedSensor(Entity): self._name = name self._state = None self.info = None - self._unit_of_measurement = 'GHz' + self._unit_of_measurement = "GHz" @property def name(self): @@ -62,15 +62,10 @@ class CpuSpeedSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" if self.info is not None: - attrs = { - ATTR_ARCH: self.info['arch'], - ATTR_BRAND: self.info['brand'], - } + attrs = {ATTR_ARCH: self.info["arch"], ATTR_BRAND: self.info["brand"]} if HZ_ADVERTISED_RAW in self.info: - attrs[ATTR_HZ] = round( - self.info[HZ_ADVERTISED_RAW][0] / 10 ** 9, 2 - ) + attrs[ATTR_HZ] = round(self.info[HZ_ADVERTISED_RAW][0] / 10 ** 9, 2) return attrs @property @@ -84,8 +79,6 @@ class CpuSpeedSensor(Entity): self.info = cpuinfo.get_cpu_info() if HZ_ACTUAL_RAW in self.info: - self._state = round( - float(self.info[HZ_ACTUAL_RAW][0]) / 10 ** 9, 2 - ) + self._state = round(float(self.info[HZ_ACTUAL_RAW][0]) / 10 ** 9, 2) else: self._state = None diff --git a/homeassistant/components/crimereports/sensor.py b/homeassistant/components/crimereports/sensor.py index 5e25d800247..2ad31e7513b 100644 --- a/homeassistant/components/crimereports/sensor.py +++ b/homeassistant/components/crimereports/sensor.py @@ -7,9 +7,18 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_INCLUDE, CONF_EXCLUDE, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_RADIUS, - LENGTH_KILOMETERS, LENGTH_METERS) + CONF_INCLUDE, + CONF_EXCLUDE, + CONF_NAME, + CONF_LATITUDE, + CONF_LONGITUDE, + ATTR_ATTRIBUTION, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_RADIUS, + LENGTH_KILOMETERS, + LENGTH_METERS, +) from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.util.distance import convert @@ -18,20 +27,22 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DOMAIN = 'crimereports' +DOMAIN = "crimereports" -EVENT_INCIDENT = '{}_incident'.format(DOMAIN) +EVENT_INCIDENT = "{}_incident".format(DOMAIN) SCAN_INTERVAL = timedelta(minutes=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_RADIUS): vol.Coerce(float), - vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude, - vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_RADIUS): vol.Coerce(float), + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -43,24 +54,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): include = config.get(CONF_INCLUDE) exclude = config.get(CONF_EXCLUDE) - add_entities([CrimeReportsSensor( - hass, name, latitude, longitude, radius, include, exclude)], True) + add_entities( + [CrimeReportsSensor(hass, name, latitude, longitude, radius, include, exclude)], + True, + ) class CrimeReportsSensor(Entity): """Representation of a Crime Reports Sensor.""" - def __init__(self, hass, name, latitude, longitude, radius, - include, exclude): + def __init__(self, hass, name, latitude, longitude, radius, include, exclude): """Initialize the Crime Reports sensor.""" import crimereports + self._hass = hass self._name = name self._include = include self._exclude = exclude radius_kilometers = convert(radius, LENGTH_METERS, LENGTH_KILOMETERS) self._crimereports = crimereports.CrimeReports( - (latitude, longitude), radius_kilometers) + (latitude, longitude), radius_kilometers + ) self._attributes = None self._state = None self._previous_incidents = set() @@ -83,36 +97,37 @@ class CrimeReportsSensor(Entity): def _incident_event(self, incident): """Fire if an event occurs.""" data = { - 'type': incident.get('type'), - 'description': incident.get('friendly_description'), - 'timestamp': incident.get('timestamp'), - 'location': incident.get('location') + "type": incident.get("type"), + "description": incident.get("friendly_description"), + "timestamp": incident.get("timestamp"), + "location": incident.get("location"), } - if incident.get('coordinates'): - data.update({ - ATTR_LATITUDE: incident.get('coordinates')[0], - ATTR_LONGITUDE: incident.get('coordinates')[1] - }) + if incident.get("coordinates"): + data.update( + { + ATTR_LATITUDE: incident.get("coordinates")[0], + ATTR_LONGITUDE: incident.get("coordinates")[1], + } + ) self._hass.bus.fire(EVENT_INCIDENT, data) def update(self): """Update device state.""" import crimereports + incident_counts = defaultdict(int) incidents = self._crimereports.get_incidents( - now().date(), include=self._include, exclude=self._exclude) + now().date(), include=self._include, exclude=self._exclude + ) fire_events = len(self._previous_incidents) > 0 if len(incidents) < len(self._previous_incidents): self._previous_incidents = set() for incident in incidents: - incident_type = slugify(incident.get('type')) + incident_type = slugify(incident.get("type")) incident_counts[incident_type] += 1 - if (fire_events and incident.get('id') - not in self._previous_incidents): + if fire_events and incident.get("id") not in self._previous_incidents: self._incident_event(incident) - self._previous_incidents.add(incident.get('id')) - self._attributes = { - ATTR_ATTRIBUTION: crimereports.ATTRIBUTION - } + self._previous_incidents.add(incident.get("id")) + self._attributes = {ATTR_ATTRIBUTION: crimereports.ATTRIBUTION} self._attributes.update(incident_counts) self._state = len(incidents) diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index ea171f94bf3..79bb050a617 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -13,45 +13,42 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_MARKER_TYPE = 'marker_type' -ATTR_MARKER_LOW_LEVEL = 'marker_low_level' -ATTR_MARKER_HIGH_LEVEL = 'marker_high_level' -ATTR_PRINTER_NAME = 'printer_name' -ATTR_DEVICE_URI = 'device_uri' -ATTR_PRINTER_INFO = 'printer_info' -ATTR_PRINTER_IS_SHARED = 'printer_is_shared' -ATTR_PRINTER_LOCATION = 'printer_location' -ATTR_PRINTER_MODEL = 'printer_model' -ATTR_PRINTER_STATE_MESSAGE = 'printer_state_message' -ATTR_PRINTER_STATE_REASON = 'printer_state_reason' -ATTR_PRINTER_TYPE = 'printer_type' -ATTR_PRINTER_URI_SUPPORTED = 'printer_uri_supported' +ATTR_MARKER_TYPE = "marker_type" +ATTR_MARKER_LOW_LEVEL = "marker_low_level" +ATTR_MARKER_HIGH_LEVEL = "marker_high_level" +ATTR_PRINTER_NAME = "printer_name" +ATTR_DEVICE_URI = "device_uri" +ATTR_PRINTER_INFO = "printer_info" +ATTR_PRINTER_IS_SHARED = "printer_is_shared" +ATTR_PRINTER_LOCATION = "printer_location" +ATTR_PRINTER_MODEL = "printer_model" +ATTR_PRINTER_STATE_MESSAGE = "printer_state_message" +ATTR_PRINTER_STATE_REASON = "printer_state_reason" +ATTR_PRINTER_TYPE = "printer_type" +ATTR_PRINTER_URI_SUPPORTED = "printer_uri_supported" -CONF_PRINTERS = 'printers' -CONF_IS_CUPS_SERVER = 'is_cups_server' +CONF_PRINTERS = "printers" +CONF_IS_CUPS_SERVER = "is_cups_server" -DEFAULT_HOST = '127.0.0.1' +DEFAULT_HOST = "127.0.0.1" DEFAULT_PORT = 631 DEFAULT_IS_CUPS_SERVER = True -ICON_PRINTER = 'mdi:printer' -ICON_MARKER = 'mdi:water' +ICON_PRINTER = "mdi:printer" +ICON_MARKER = "mdi:water" SCAN_INTERVAL = timedelta(minutes=1) -PRINTER_STATES = { - 3: 'idle', - 4: 'printing', - 5: 'stopped', -} +PRINTER_STATES = {3: "idle", 4: "printing", 5: "stopped"} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PRINTERS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_IS_CUPS_SERVER, - default=DEFAULT_IS_CUPS_SERVER): cv.boolean, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PRINTERS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_IS_CUPS_SERVER, default=DEFAULT_IS_CUPS_SERVER): cv.boolean, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -65,8 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = CupsData(host, port, None) data.update() if data.available is False: - _LOGGER.error("Unable to connect to CUPS server: %s:%s", - host, port) + _LOGGER.error("Unable to connect to CUPS server: %s:%s", host, port) raise PlatformNotReady() dev = [] @@ -86,8 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = CupsData(host, port, printers) data.update() if data.available is False: - _LOGGER.error("Unable to connect to IPP printer: %s:%s", - host, port) + _LOGGER.error("Unable to connect to IPP printer: %s:%s", host, port) raise PlatformNotReady() dev = [] @@ -122,7 +117,7 @@ class CupsSensor(Entity): if self._printer is None: return None - key = self._printer['printer-state'] + key = self._printer["printer-state"] return PRINTER_STATES.get(key, key) @property @@ -142,18 +137,15 @@ class CupsSensor(Entity): return None return { - ATTR_DEVICE_URI: self._printer['device-uri'], - ATTR_PRINTER_INFO: self._printer['printer-info'], - ATTR_PRINTER_IS_SHARED: self._printer['printer-is-shared'], - ATTR_PRINTER_LOCATION: self._printer['printer-location'], - ATTR_PRINTER_MODEL: self._printer['printer-make-and-model'], - ATTR_PRINTER_STATE_MESSAGE: - self._printer['printer-state-message'], - ATTR_PRINTER_STATE_REASON: - self._printer['printer-state-reasons'], - ATTR_PRINTER_TYPE: self._printer['printer-type'], - ATTR_PRINTER_URI_SUPPORTED: - self._printer['printer-uri-supported'], + ATTR_DEVICE_URI: self._printer["device-uri"], + ATTR_PRINTER_INFO: self._printer["printer-info"], + ATTR_PRINTER_IS_SHARED: self._printer["printer-is-shared"], + ATTR_PRINTER_LOCATION: self._printer["printer-location"], + ATTR_PRINTER_MODEL: self._printer["printer-make-and-model"], + ATTR_PRINTER_STATE_MESSAGE: self._printer["printer-state-message"], + ATTR_PRINTER_STATE_REASON: self._printer["printer-state-reasons"], + ATTR_PRINTER_TYPE: self._printer["printer-type"], + ATTR_PRINTER_URI_SUPPORTED: self._printer["printer-uri-supported"], } def update(self): @@ -179,7 +171,7 @@ class IPPSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return self._attributes['printer-make-and-model'] + return self._attributes["printer-make-and-model"] @property def icon(self): @@ -197,7 +189,7 @@ class IPPSensor(Entity): if self._attributes is None: return None - key = self._attributes['printer-state'] + key = self._attributes["printer-state"] return PRINTER_STATES.get(key, key) @property @@ -208,25 +200,28 @@ class IPPSensor(Entity): state_attributes = {} - if 'printer-info' in self._attributes: - state_attributes[ATTR_PRINTER_INFO] = \ - self._attributes['printer-info'] + if "printer-info" in self._attributes: + state_attributes[ATTR_PRINTER_INFO] = self._attributes["printer-info"] - if 'printer-location' in self._attributes: - state_attributes[ATTR_PRINTER_LOCATION] = \ - self._attributes['printer-location'] + if "printer-location" in self._attributes: + state_attributes[ATTR_PRINTER_LOCATION] = self._attributes[ + "printer-location" + ] - if 'printer-state-message' in self._attributes: - state_attributes[ATTR_PRINTER_STATE_MESSAGE] = \ - self._attributes['printer-state-message'] + if "printer-state-message" in self._attributes: + state_attributes[ATTR_PRINTER_STATE_MESSAGE] = self._attributes[ + "printer-state-message" + ] - if 'printer-state-reasons' in self._attributes: - state_attributes[ATTR_PRINTER_STATE_REASON] = \ - self._attributes['printer-state-reasons'] + if "printer-state-reasons" in self._attributes: + state_attributes[ATTR_PRINTER_STATE_REASON] = self._attributes[ + "printer-state-reasons" + ] - if 'printer-uri-supported' in self._attributes: - state_attributes[ATTR_PRINTER_URI_SUPPORTED] = \ - self._attributes['printer-uri-supported'] + if "printer-uri-supported" in self._attributes: + state_attributes[ATTR_PRINTER_URI_SUPPORTED] = self._attributes[ + "printer-uri-supported" + ] return state_attributes @@ -248,7 +243,7 @@ class MarkerSensor(Entity): self.data = data self._name = name self._printer = printer - self._index = data.attributes[printer]['marker-names'].index(name) + self._index = data.attributes[printer]["marker-names"].index(name) self._is_cups = is_cups self._attributes = None @@ -268,7 +263,7 @@ class MarkerSensor(Entity): if self._attributes is None: return None - return self._attributes[self._printer]['marker-levels'][self._index] + return self._attributes[self._printer]["marker-levels"][self._index] @property def unit_of_measurement(self): @@ -281,30 +276,28 @@ class MarkerSensor(Entity): if self._attributes is None: return None - high_level = self._attributes[self._printer]['marker-high-levels'] + high_level = self._attributes[self._printer]["marker-high-levels"] if isinstance(high_level, list): high_level = high_level[self._index] - low_level = self._attributes[self._printer]['marker-low-levels'] + low_level = self._attributes[self._printer]["marker-low-levels"] if isinstance(low_level, list): low_level = low_level[self._index] - marker_types = self._attributes[self._printer]['marker-types'] + marker_types = self._attributes[self._printer]["marker-types"] if isinstance(marker_types, list): marker_types = marker_types[self._index] if self._is_cups: printer_name = self._printer else: - printer_name = \ - self._attributes[self._printer]['printer-make-and-model'] + printer_name = self._attributes[self._printer]["printer-make-and-model"] return { ATTR_MARKER_HIGH_LEVEL: high_level, ATTR_MARKER_LOW_LEVEL: low_level, ATTR_MARKER_TYPE: marker_types, - ATTR_PRINTER_NAME: printer_name - + ATTR_PRINTER_NAME: printer_name, } def update(self): @@ -322,27 +315,26 @@ class CupsData: self._host = host self._port = port self._ipp_printers = ipp_printers - self.is_cups = (ipp_printers is None) + self.is_cups = ipp_printers is None self.printers = None self.attributes = {} self.available = False def update(self): """Get the latest data from CUPS.""" - cups = importlib.import_module('cups') + cups = importlib.import_module("cups") try: conn = cups.Connection(host=self._host, port=self._port) if self.is_cups: self.printers = conn.getPrinters() for printer in self.printers: - self.attributes[printer] = conn.getPrinterAttributes( - name=printer) + self.attributes[printer] = conn.getPrinterAttributes(name=printer) else: for ipp_printer in self._ipp_printers: self.attributes[ipp_printer] = conn.getPrinterAttributes( - uri="ipp://{}:{}/{}" - .format(self._host, self._port, ipp_printer)) + uri="ipp://{}:{}/{}".format(self._host, self._port, ipp_printer) + ) self.available = True except RuntimeError: diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index bedd5f079ce..dbafae55187 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -8,46 +8,49 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, CONF_NAME, CONF_BASE, CONF_QUOTE, ATTR_ATTRIBUTION) + CONF_API_KEY, + CONF_NAME, + CONF_BASE, + CONF_QUOTE, + ATTR_ATTRIBUTION, +) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -_RESOURCE = 'http://apilayer.net/api/live' +_RESOURCE = "http://apilayer.net/api/live" ATTRIBUTION = "Data provided by currencylayer.com" -DEFAULT_BASE = 'USD' -DEFAULT_NAME = 'CurrencyLayer Sensor' +DEFAULT_BASE = "USD" +DEFAULT_NAME = "CurrencyLayer Sensor" -ICON = 'mdi:currency' +ICON = "mdi:currency" SCAN_INTERVAL = timedelta(hours=2) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_QUOTE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_BASE, default=DEFAULT_BASE): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_QUOTE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_BASE, default=DEFAULT_BASE): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Currencylayer sensor.""" base = config.get(CONF_BASE) api_key = config.get(CONF_API_KEY) - parameters = { - 'source': base, - 'access_key': api_key, - 'format': 1, - } + parameters = {"source": base, "access_key": api_key, "format": 1} rest = CurrencylayerData(_RESOURCE, parameters) response = requests.get(_RESOURCE, params=parameters, timeout=10) sensors = [] - for variable in config['quote']: + for variable in config["quote"]: sensors.append(CurrencylayerSensor(rest, base, variable)) - if 'error' in response.json(): + if "error" in response.json(): return False add_entities(sensors, True) @@ -85,17 +88,14 @@ class CurrencylayerSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - } + return {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Update current date.""" self.rest.update() value = self.rest.data if value is not None: - self._state = round( - value['{}{}'.format(self._base, self._quote)], 4) + self._state = round(value["{}{}".format(self._base, self._quote)], 4) class CurrencylayerData: @@ -110,13 +110,11 @@ class CurrencylayerData: def update(self): """Get the latest data from Currencylayer.""" try: - result = requests.get( - self._resource, params=self._parameters, timeout=10) - if 'error' in result.json(): - raise ValueError(result.json()['error']['info']) - self.data = result.json()['quotes'] - _LOGGER.debug("Currencylayer data updated: %s", - result.json()['timestamp']) + result = requests.get(self._resource, params=self._parameters, timeout=10) + if "error" in result.json(): + raise ValueError(result.json()["error"]["info"]) + self.data = result.json()["quotes"] + _LOGGER.debug("Currencylayer data updated: %s", result.json()["timestamp"]) except ValueError as err: _LOGGER.error("Check Currencylayer API %s", err.args) self.data = None diff --git a/homeassistant/components/daikin/.translations/bg.json b/homeassistant/components/daikin/.translations/bg.json index beb1bc0d6e6..b0ddcbf4903 100644 --- a/homeassistant/components/daikin/.translations/bg.json +++ b/homeassistant/components/daikin/.translations/bg.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "device_fail": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u044a\u0437\u0434\u0430\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", + "device_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e." + }, "step": { "user": { "data": { "host": "\u0410\u0434\u0440\u0435\u0441" - } + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u043a Daikin.", + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u043a Daikin" } - } + }, + "title": "\u041a\u043b\u0438\u043c\u0430\u0442\u0438\u043a Daikin" } } \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/pt-BR.json b/homeassistant/components/daikin/.translations/pt-BR.json index 58c5a9c77b2..bbdf68ed794 100644 --- a/homeassistant/components/daikin/.translations/pt-BR.json +++ b/homeassistant/components/daikin/.translations/pt-BR.json @@ -7,6 +7,9 @@ }, "step": { "user": { + "data": { + "host": "Host" + }, "description": "Digite o endere\u00e7o IP do seu AC Daikin.", "title": "Configurar o AC Daikin" } diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index edc447fe721..390e80d0916 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -19,20 +19,21 @@ from . import config_flow # noqa pylint_disable=unused-import _LOGGER = logging.getLogger(__name__) -DOMAIN = 'daikin' +DOMAIN = "daikin" PARALLEL_UPDATES = 0 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -COMPONENT_TYPES = ['climate', 'sensor', 'switch'] +COMPONENT_TYPES = ["climate", "sensor", "switch"] -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional( - CONF_HOSTS, default=[] - ): vol.All(cv.ensure_list, [cv.string]), - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Optional(CONF_HOSTS, default=[]): vol.All(cv.ensure_list, [cv.string])} + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -44,15 +45,15 @@ async def async_setup(hass, config): if not hosts: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={'source': SOURCE_IMPORT})) + DOMAIN, context={"source": SOURCE_IMPORT} + ) + ) for host in hosts: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, - context={'source': SOURCE_IMPORT}, - data={ - CONF_HOST: host, - })) + DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_HOST: host} + ) + ) return True @@ -65,17 +66,19 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) for component in COMPONENT_TYPES: hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - entry, component)) + hass.config_entries.async_forward_entry_setup(entry, component) + ) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - await asyncio.wait([ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in COMPONENT_TYPES - ]) + await asyncio.wait( + [ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in COMPONENT_TYPES + ] + ) hass.data[DOMAIN].pop(config_entry.entry_id) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) @@ -85,6 +88,7 @@ async def async_unload_entry(hass, config_entry): async def daikin_api_setup(hass, host): """Create a Daikin instance only once.""" from pydaikin.appliance import Appliance + session = hass.helpers.aiohttp_client.async_get_clientsession() try: with timeout(10): @@ -111,7 +115,7 @@ class DaikinApi: def __init__(self, device): """Initialize the Daikin Handle.""" self.device = device - self.name = device.values['name'] + self.name = device.values["name"] self.ip_address = device.ip self._available = True @@ -122,9 +126,7 @@ class DaikinApi: await self.device.update_status() self._available = True except ClientConnectionError: - _LOGGER.warning( - "Connection failed for %s", self.ip_address - ) + _LOGGER.warning("Connection failed for %s", self.ip_address) self._available = False @property @@ -142,10 +144,10 @@ class DaikinApi: """Return a device description for device registry.""" info = self.device.values return { - 'connections': {(CONNECTION_NETWORK_MAC, self.mac)}, - 'identifieres': self.mac, - 'manufacturer': 'Daikin', - 'model': info.get('model'), - 'name': info.get('name'), - 'sw_version': info.get('ver').replace('_', '.'), + "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, + "identifieres": self.mac, + "manufacturer": "Daikin", + "model": info.get("model"), + "name": info.get("name"), + "sw_version": info.get("ver").replace("_", "."), } diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 397c9a607b3..ddc5353250c 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -1,70 +1,77 @@ """Support for the Daikin HVAC.""" import logging -import re import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice -from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS) from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, + ATTR_FAN_MODE, + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + ATTR_SWING_MODE, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_NONE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, - HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, - PRESET_AWAY, PRESET_HOME, - ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, - ATTR_HVAC_MODE, ATTR_SWING_MODE, - ATTR_PRESET_MODE) + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from . import DOMAIN as DAIKIN_DOMAIN from .const import ( - ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE) + ATTR_INSIDE_TEMPERATURE, + ATTR_OUTSIDE_TEMPERATURE, + ATTR_STATE_OFF, + ATTR_STATE_ON, + ATTR_TARGET_TEMPERATURE, +) _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string} +) HA_STATE_TO_DAIKIN = { - HVAC_MODE_FAN_ONLY: 'fan', - HVAC_MODE_DRY: 'dry', - HVAC_MODE_COOL: 'cool', - HVAC_MODE_HEAT: 'hot', - HVAC_MODE_HEAT_COOL: 'auto', - HVAC_MODE_OFF: 'off', + HVAC_MODE_FAN_ONLY: "fan", + HVAC_MODE_DRY: "dry", + HVAC_MODE_COOL: "cool", + HVAC_MODE_HEAT: "hot", + HVAC_MODE_HEAT_COOL: "auto", + HVAC_MODE_OFF: "off", } DAIKIN_TO_HA_STATE = { - 'fan': HVAC_MODE_FAN_ONLY, - 'dry': HVAC_MODE_DRY, - 'cool': HVAC_MODE_COOL, - 'hot': HVAC_MODE_HEAT, - 'auto': HVAC_MODE_HEAT_COOL, - 'off': HVAC_MODE_OFF, + "fan": HVAC_MODE_FAN_ONLY, + "dry": HVAC_MODE_DRY, + "cool": HVAC_MODE_COOL, + "hot": HVAC_MODE_HEAT, + "auto": HVAC_MODE_HEAT_COOL, + "off": HVAC_MODE_OFF, } -HA_PRESET_TO_DAIKIN = { - PRESET_AWAY: 'on', - PRESET_HOME: 'off' -} +HA_PRESET_TO_DAIKIN = {PRESET_AWAY: "on", PRESET_NONE: "off"} HA_ATTR_TO_DAIKIN = { - ATTR_PRESET_MODE: 'en_hol', - ATTR_HVAC_MODE: 'mode', - ATTR_FAN_MODE: 'f_rate', - ATTR_SWING_MODE: 'f_dir', - ATTR_INSIDE_TEMPERATURE: 'htemp', - ATTR_OUTSIDE_TEMPERATURE: 'otemp', - ATTR_TARGET_TEMPERATURE: 'stemp' + ATTR_PRESET_MODE: "en_hol", + ATTR_HVAC_MODE: "mode", + ATTR_FAN_MODE: "f_rate", + ATTR_SWING_MODE: "f_dir", + ATTR_INSIDE_TEMPERATURE: "htemp", + ATTR_OUTSIDE_TEMPERATURE: "otemp", + ATTR_TARGET_TEMPERATURE: "stemp", } -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up the Daikin HVAC platform. Can only be called when a user accidentally mentions the platform in their @@ -93,7 +100,7 @@ class DaikinClimate(ClimateDevice): ATTR_SWING_MODE: list( map( str.title, - appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE]) + appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE]), ) ), } @@ -109,62 +116,11 @@ class DaikinClimate(ClimateDevice): if self._api.device.support_swing_mode: self._supported_features |= SUPPORT_SWING_MODE - def get(self, key): - """Retrieve device settings from API library cache.""" - value = None - cast_to_float = False - - if key in [ATTR_TEMPERATURE, ATTR_INSIDE_TEMPERATURE, - ATTR_CURRENT_TEMPERATURE]: - key = ATTR_INSIDE_TEMPERATURE - - daikin_attr = HA_ATTR_TO_DAIKIN.get(key) - - if key == ATTR_INSIDE_TEMPERATURE: - value = self._api.device.values.get(daikin_attr) - cast_to_float = True - elif key == ATTR_TARGET_TEMPERATURE: - value = self._api.device.values.get(daikin_attr) - cast_to_float = True - elif key == ATTR_OUTSIDE_TEMPERATURE: - value = self._api.device.values.get(daikin_attr) - cast_to_float = True - elif key == ATTR_FAN_MODE: - value = self._api.device.represent(daikin_attr)[1].title() - elif key == ATTR_SWING_MODE: - value = self._api.device.represent(daikin_attr)[1].title() - elif key == ATTR_HVAC_MODE: - # Daikin can return also internal states auto-1 or auto-7 - # and we need to translate them as AUTO - daikin_mode = re.sub( - '[^a-z]', '', - self._api.device.represent(daikin_attr)[1]) - ha_mode = DAIKIN_TO_HA_STATE.get(daikin_mode) - value = ha_mode - elif key == ATTR_PRESET_MODE: - away = (self._api.device.represent(daikin_attr)[1] - != HA_STATE_TO_DAIKIN[HVAC_MODE_OFF]) - value = PRESET_AWAY if away else PRESET_HOME - - if value is None: - _LOGGER.error("Invalid value requested for key %s", key) - else: - if value in ("-", "--"): - value = None - elif cast_to_float: - try: - value = float(value) - except ValueError: - value = None - - return value - async def _set(self, settings): """Set device settings using API.""" values = {} - for attr in [ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE, - ATTR_HVAC_MODE, ATTR_PRESET_MODE]: + for attr in [ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE, ATTR_HVAC_MODE]: value = settings.get(attr) if value is None: continue @@ -173,8 +129,6 @@ class DaikinClimate(ClimateDevice): if daikin_attr is not None: if attr == ATTR_HVAC_MODE: values[daikin_attr] = HA_STATE_TO_DAIKIN[value] - elif attr == ATTR_PRESET_MODE: - values[daikin_attr] = HA_PRESET_TO_DAIKIN[value] elif value in self._list[attr]: values[daikin_attr] = value.lower() else: @@ -183,7 +137,7 @@ class DaikinClimate(ClimateDevice): # temperature elif attr == ATTR_TEMPERATURE: try: - values['stemp'] = str(int(value)) + values[HA_ATTR_TO_DAIKIN[ATTR_TARGET_TEMPERATURE]] = str(int(value)) except ValueError: _LOGGER.error("Invalid temperature %s", value) @@ -213,12 +167,12 @@ class DaikinClimate(ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - return self.get(ATTR_CURRENT_TEMPERATURE) + return self._api.device.inside_temperature @property def target_temperature(self): """Return the temperature we try to reach.""" - return self.get(ATTR_TARGET_TEMPERATURE) + return self._api.device.target_temperature @property def target_temperature_step(self): @@ -232,7 +186,8 @@ class DaikinClimate(ClimateDevice): @property def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" - return self.get(ATTR_HVAC_MODE) + daikin_mode = self._api.device.represent(HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE])[1] + return DAIKIN_TO_HA_STATE.get(daikin_mode, HVAC_MODE_HEAT_COOL) @property def hvac_modes(self): @@ -246,7 +201,7 @@ class DaikinClimate(ClimateDevice): @property def fan_mode(self): """Return the fan setting.""" - return self.get(ATTR_FAN_MODE) + return self._api.device.represent(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE])[1].title() async def async_set_fan_mode(self, fan_mode): """Set fan mode.""" @@ -260,7 +215,7 @@ class DaikinClimate(ClimateDevice): @property def swing_mode(self): """Return the fan setting.""" - return self.get(ATTR_SWING_MODE) + return self._api.device.represent(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE])[1].title() async def async_set_swing_mode(self, swing_mode): """Set new target temperature.""" @@ -273,22 +228,40 @@ class DaikinClimate(ClimateDevice): @property def preset_mode(self): - """Return the fan setting.""" - return self.get(ATTR_PRESET_MODE) + """Return the preset_mode.""" + if ( + self._api.device.represent(HA_ATTR_TO_DAIKIN[ATTR_PRESET_MODE])[1] + == HA_PRESET_TO_DAIKIN[PRESET_AWAY] + ): + return PRESET_AWAY + return PRESET_NONE async def async_set_preset_mode(self, preset_mode): - """Set new target temperature.""" - await self._set({ATTR_PRESET_MODE: preset_mode}) + """Set preset mode.""" + if preset_mode == PRESET_AWAY: + await self._api.device.set_holiday(ATTR_STATE_ON) + else: + await self._api.device.set_holiday(ATTR_STATE_OFF) @property def preset_modes(self): - """List of available swing modes.""" + """List of available preset modes.""" return list(HA_PRESET_TO_DAIKIN) async def async_update(self): """Retrieve latest state.""" await self._api.async_update() + async def async_turn_on(self): + """Turn device on.""" + await self._api.device.set({}) + + async def async_turn_off(self): + """Turn device off.""" + await self._api.device.set( + {HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVAC_MODE_OFF]} + ) + @property def device_info(self): """Return a device description for device registry.""" diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 7c214e77050..36d8ef0d383 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -14,7 +14,7 @@ from .const import KEY_IP, KEY_MAC _LOGGER = logging.getLogger(__name__) -@config_entries.HANDLERS.register('daikin') +@config_entries.HANDLERS.register("daikin") class FlowHandler(config_entries.ConfigFlow): """Handle a config flow.""" @@ -26,45 +26,37 @@ class FlowHandler(config_entries.ConfigFlow): # Check if mac already is registered for entry in self._async_current_entries(): if entry.data[KEY_MAC] == mac: - return self.async_abort(reason='already_configured') + return self.async_abort(reason="already_configured") - return self.async_create_entry( - title=host, - data={ - CONF_HOST: host, - KEY_MAC: mac - }) + return self.async_create_entry(title=host, data={CONF_HOST: host, KEY_MAC: mac}) async def _create_device(self, host): """Create device.""" from pydaikin.appliance import Appliance + try: device = Appliance( - host, - self.hass.helpers.aiohttp_client.async_get_clientsession(), + host, self.hass.helpers.aiohttp_client.async_get_clientsession() ) with timeout(10): await device.init() except asyncio.TimeoutError: - return self.async_abort(reason='device_timeout') + return self.async_abort(reason="device_timeout") except ClientError: _LOGGER.exception("ClientError") - return self.async_abort(reason='device_fail') + return self.async_abort(reason="device_fail") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error creating device") - return self.async_abort(reason='device_fail') + return self.async_abort(reason="device_fail") - mac = device.values.get('mac') + mac = device.values.get("mac") return await self._create_entry(host, mac) async def async_step_user(self, user_input=None): """User initiated config flow.""" if user_input is None: return self.async_show_form( - step_id='user', - data_schema=vol.Schema({ - vol.Required(CONF_HOST): str - }) + step_id="user", data_schema=vol.Schema({vol.Required(CONF_HOST): str}) ) return await self._create_device(user_input[CONF_HOST]) @@ -78,5 +70,4 @@ class FlowHandler(config_entries.ConfigFlow): async def async_step_discovery(self, user_input): """Initialize step from discovery.""" _LOGGER.info("Discovered device: %s", user_input) - return await self._create_entry(user_input[KEY_IP], - user_input[KEY_MAC]) + return await self._create_entry(user_input[KEY_IP], user_input[KEY_MAC]) diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py index 90967904579..ef24a51be89 100644 --- a/homeassistant/components/daikin/const.py +++ b/homeassistant/components/daikin/const.py @@ -1,24 +1,27 @@ """Constants for Daikin.""" from homeassistant.const import CONF_ICON, CONF_NAME, CONF_TYPE -ATTR_TARGET_TEMPERATURE = 'target_temperature' -ATTR_INSIDE_TEMPERATURE = 'inside_temperature' -ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature' +ATTR_TARGET_TEMPERATURE = "target_temperature" +ATTR_INSIDE_TEMPERATURE = "inside_temperature" +ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" -SENSOR_TYPE_TEMPERATURE = 'temperature' +ATTR_STATE_ON = "on" +ATTR_STATE_OFF = "off" + +SENSOR_TYPE_TEMPERATURE = "temperature" SENSOR_TYPES = { ATTR_INSIDE_TEMPERATURE: { - CONF_NAME: 'Inside Temperature', - CONF_ICON: 'mdi:thermometer', - CONF_TYPE: SENSOR_TYPE_TEMPERATURE + CONF_NAME: "Inside Temperature", + CONF_ICON: "mdi:thermometer", + CONF_TYPE: SENSOR_TYPE_TEMPERATURE, }, ATTR_OUTSIDE_TEMPERATURE: { - CONF_NAME: 'Outside Temperature', - CONF_ICON: 'mdi:thermometer', - CONF_TYPE: SENSOR_TYPE_TEMPERATURE - } + CONF_NAME: "Outside Temperature", + CONF_ICON: "mdi:thermometer", + CONF_TYPE: SENSOR_TYPE_TEMPERATURE, + }, } -KEY_MAC = 'mac' -KEY_IP = 'ip' +KEY_MAC = "mac" +KEY_IP = "ip" diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index bb6db101314..a60355efa0b 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/daikin", "requirements": [ - "pydaikin==1.4.6" + "pydaikin==1.6.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index 8196acc5cf7..c55988b8dc1 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -7,14 +7,16 @@ from homeassistant.util.unit_system import UnitSystem from . import DOMAIN as DAIKIN_DOMAIN from .const import ( - ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, SENSOR_TYPE_TEMPERATURE, - SENSOR_TYPES) + ATTR_INSIDE_TEMPERATURE, + ATTR_OUTSIDE_TEMPERATURE, + SENSOR_TYPE_TEMPERATURE, + SENSOR_TYPES, +) _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up the Daikin sensors. Can only be called when a user accidentally mentions the platform in their @@ -29,17 +31,18 @@ async def async_setup_entry(hass, entry, async_add_entities): sensors = [ATTR_INSIDE_TEMPERATURE] if daikin_api.device.support_outside_temperature: sensors.append(ATTR_OUTSIDE_TEMPERATURE) - async_add_entities([ - DaikinClimateSensor(daikin_api, sensor, hass.config.units) - for sensor in sensors - ]) + async_add_entities( + [ + DaikinClimateSensor(daikin_api, sensor, hass.config.units) + for sensor in sensors + ] + ) class DaikinClimateSensor(Entity): """Representation of a Sensor.""" - def __init__(self, api, monitored_state, units: UnitSystem, - name=None) -> None: + def __init__(self, api, monitored_state, units: UnitSystem, name=None) -> None: """Initialize the sensor.""" self._api = api self._sensor = SENSOR_TYPES.get(monitored_state) @@ -57,30 +60,6 @@ class DaikinClimateSensor(Entity): """Return a unique ID.""" return "{}-{}".format(self._api.mac, self._device_attribute) - def get(self, key): - """Retrieve device settings from API library cache.""" - value = None - cast_to_float = False - - if key == ATTR_INSIDE_TEMPERATURE: - value = self._api.device.values.get('htemp') - cast_to_float = True - elif key == ATTR_OUTSIDE_TEMPERATURE: - value = self._api.device.values.get('otemp') - - if value is None: - _LOGGER.warning("Invalid value requested for key %s", key) - else: - if value in ("-", "--"): - value = None - elif cast_to_float: - try: - value = float(value) - except ValueError: - value = None - - return value - @property def icon(self): """Icon to use in the frontend, if any.""" @@ -94,7 +73,11 @@ class DaikinClimateSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self.get(self._device_attribute) + if self._device_attribute == ATTR_INSIDE_TEMPERATURE: + return self._api.device.inside_temperature + if self._device_attribute == ATTR_OUTSIDE_TEMPERATURE: + return self._api.device.outside_temperature + return None @property def unit_of_measurement(self): diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index f1a058957fa..6290e6fecef 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -7,11 +7,10 @@ from . import DOMAIN as DAIKIN_DOMAIN _LOGGER = logging.getLogger(__name__) -ZONE_ICON = 'mdi:home-circle' +ZONE_ICON = "mdi:home-circle" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up the platform. Can only be called when a user accidentally mentions the platform in their @@ -25,10 +24,13 @@ async def async_setup_entry(hass, entry, async_add_entities): daikin_api = hass.data[DAIKIN_DOMAIN][entry.entry_id] zones = daikin_api.device.zones if zones: - async_add_entities([ - DaikinZoneSwitch(daikin_api, zone_id) - for zone_id, zone in enumerate(zones) if zone != ('-', '0') - ]) + async_add_entities( + [ + DaikinZoneSwitch(daikin_api, zone_id) + for zone_id, zone in enumerate(zones) + if zone != ("-", "0") + ] + ) class DaikinZoneSwitch(ToggleEntity): @@ -52,13 +54,12 @@ class DaikinZoneSwitch(ToggleEntity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._api.name, - self._api.device.zones[self._zone_id][0]) + return "{} {}".format(self._api.name, self._api.device.zones[self._zone_id][0]) @property def is_on(self): """Return the state of the sensor.""" - return self._api.device.zones[self._zone_id][1] == '1' + return self._api.device.zones[self._zone_id][1] == "1" @property def device_info(self): @@ -71,8 +72,8 @@ class DaikinZoneSwitch(ToggleEntity): async def async_turn_on(self, **kwargs): """Turn the zone on.""" - await self._api.device.set_zone(self._zone_id, '1') + await self._api.device.set_zone(self._zone_id, "1") async def async_turn_off(self, **kwargs): """Turn the zone off.""" - await self._api.device.set_zone(self._zone_id, '0') + await self._api.device.set_zone(self._zone_id, "0") diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index 6e86b16c02d..adfb13e722c 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -11,16 +11,14 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -DANFOSS_AIR_PLATFORMS = ['sensor', 'binary_sensor', 'switch'] -DOMAIN = 'danfoss_air' +DANFOSS_AIR_PLATFORMS = ["sensor", "binary_sensor", "switch"] +DOMAIN = "danfoss_air" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA +) def setup(hass, config): @@ -59,35 +57,41 @@ class DanfossAir: """Use the data from Danfoss Air API.""" _LOGGER.debug("Fetching data from Danfoss Air CCM module") from pydanfossair.commands import ReadCommand - self._data[ReadCommand.exhaustTemperature] \ - = self._client.command(ReadCommand.exhaustTemperature) - self._data[ReadCommand.outdoorTemperature] \ - = self._client.command(ReadCommand.outdoorTemperature) - self._data[ReadCommand.supplyTemperature] \ - = self._client.command(ReadCommand.supplyTemperature) - self._data[ReadCommand.extractTemperature] \ - = self._client.command(ReadCommand.extractTemperature) - self._data[ReadCommand.humidity] \ - = round(self._client.command(ReadCommand.humidity), 2) - self._data[ReadCommand.filterPercent] \ - = round(self._client.command(ReadCommand.filterPercent), 2) - self._data[ReadCommand.bypass] \ - = self._client.command(ReadCommand.bypass) - self._data[ReadCommand.fan_step] \ - = self._client.command(ReadCommand.fan_step) - self._data[ReadCommand.supply_fan_speed] \ - = self._client.command(ReadCommand.supply_fan_speed) - self._data[ReadCommand.exhaust_fan_speed] \ - = self._client.command(ReadCommand.exhaust_fan_speed) - self._data[ReadCommand.away_mode] \ - = self._client.command(ReadCommand.away_mode) - self._data[ReadCommand.boost] \ - = self._client.command(ReadCommand.boost) - self._data[ReadCommand.battery_percent] \ - = self._client.command(ReadCommand.battery_percent) - self._data[ReadCommand.bypass] \ - = self._client.command(ReadCommand.bypass) - self._data[ReadCommand.automatic_bypass] \ - = self._client.command(ReadCommand.automatic_bypass) + + self._data[ReadCommand.exhaustTemperature] = self._client.command( + ReadCommand.exhaustTemperature + ) + self._data[ReadCommand.outdoorTemperature] = self._client.command( + ReadCommand.outdoorTemperature + ) + self._data[ReadCommand.supplyTemperature] = self._client.command( + ReadCommand.supplyTemperature + ) + self._data[ReadCommand.extractTemperature] = self._client.command( + ReadCommand.extractTemperature + ) + self._data[ReadCommand.humidity] = round( + self._client.command(ReadCommand.humidity), 2 + ) + self._data[ReadCommand.filterPercent] = round( + self._client.command(ReadCommand.filterPercent), 2 + ) + self._data[ReadCommand.bypass] = self._client.command(ReadCommand.bypass) + self._data[ReadCommand.fan_step] = self._client.command(ReadCommand.fan_step) + self._data[ReadCommand.supply_fan_speed] = self._client.command( + ReadCommand.supply_fan_speed + ) + self._data[ReadCommand.exhaust_fan_speed] = self._client.command( + ReadCommand.exhaust_fan_speed + ) + self._data[ReadCommand.away_mode] = self._client.command(ReadCommand.away_mode) + self._data[ReadCommand.boost] = self._client.command(ReadCommand.boost) + self._data[ReadCommand.battery_percent] = self._client.command( + ReadCommand.battery_percent + ) + self._data[ReadCommand.bypass] = self._client.command(ReadCommand.bypass) + self._data[ReadCommand.automatic_bypass] = self._client.command( + ReadCommand.automatic_bypass + ) _LOGGER.debug("Done fetching data from Danfoss Air CCM module") diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py index 723b0d08801..d5aab8b35bb 100644 --- a/homeassistant/components/danfoss_air/binary_sensor.py +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -7,6 +7,7 @@ from . import DOMAIN as DANFOSS_AIR_DOMAIN def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available Danfoss Air sensors etc.""" from pydanfossair.commands import ReadCommand + data = hass.data[DANFOSS_AIR_DOMAIN] sensors = [ @@ -17,8 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] for sensor in sensors: - dev.append(DanfossAirBinarySensor( - data, sensor[0], sensor[1], sensor[2])) + dev.append(DanfossAirBinarySensor(data, sensor[0], sensor[1], sensor[2])) add_entities(dev, True) diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index a5dc2a2eb09..ea0002d0ac3 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -2,8 +2,11 @@ import logging from homeassistant.const import ( - DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS) + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, +) from homeassistant.helpers.entity import Entity from . import DOMAIN as DANFOSS_AIR_DOMAIN @@ -18,33 +21,47 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = hass.data[DANFOSS_AIR_DOMAIN] sensors = [ - ["Danfoss Air Exhaust Temperature", TEMP_CELSIUS, - ReadCommand.exhaustTemperature, DEVICE_CLASS_TEMPERATURE], - ["Danfoss Air Outdoor Temperature", TEMP_CELSIUS, - ReadCommand.outdoorTemperature, DEVICE_CLASS_TEMPERATURE], - ["Danfoss Air Supply Temperature", TEMP_CELSIUS, - ReadCommand.supplyTemperature, DEVICE_CLASS_TEMPERATURE], - ["Danfoss Air Extract Temperature", TEMP_CELSIUS, - ReadCommand.extractTemperature, DEVICE_CLASS_TEMPERATURE], - ["Danfoss Air Remaining Filter", '%', - ReadCommand.filterPercent, None], - ["Danfoss Air Humidity", '%', - ReadCommand.humidity, DEVICE_CLASS_HUMIDITY], - ["Danfoss Air Fan Step", '%', - ReadCommand.fan_step, None], - ["Dandoss Air Exhaust Fan Speed", 'RPM', - ReadCommand.exhaust_fan_speed, None], - ["Dandoss Air Supply Fan Speed", 'RPM', - ReadCommand.supply_fan_speed, None], - ["Dandoss Air Dial Battery", '%', - ReadCommand.battery_percent, DEVICE_CLASS_BATTERY] - ] + [ + "Danfoss Air Exhaust Temperature", + TEMP_CELSIUS, + ReadCommand.exhaustTemperature, + DEVICE_CLASS_TEMPERATURE, + ], + [ + "Danfoss Air Outdoor Temperature", + TEMP_CELSIUS, + ReadCommand.outdoorTemperature, + DEVICE_CLASS_TEMPERATURE, + ], + [ + "Danfoss Air Supply Temperature", + TEMP_CELSIUS, + ReadCommand.supplyTemperature, + DEVICE_CLASS_TEMPERATURE, + ], + [ + "Danfoss Air Extract Temperature", + TEMP_CELSIUS, + ReadCommand.extractTemperature, + DEVICE_CLASS_TEMPERATURE, + ], + ["Danfoss Air Remaining Filter", "%", ReadCommand.filterPercent, None], + ["Danfoss Air Humidity", "%", ReadCommand.humidity, DEVICE_CLASS_HUMIDITY], + ["Danfoss Air Fan Step", "%", ReadCommand.fan_step, None], + ["Dandoss Air Exhaust Fan Speed", "RPM", ReadCommand.exhaust_fan_speed, None], + ["Dandoss Air Supply Fan Speed", "RPM", ReadCommand.supply_fan_speed, None], + [ + "Dandoss Air Dial Battery", + "%", + ReadCommand.battery_percent, + DEVICE_CLASS_BATTERY, + ], + ] dev = [] for sensor in sensors: - dev.append(DanfossAir( - data, sensor[0], sensor[1], sensor[2], sensor[3])) + dev.append(DanfossAir(data, sensor[0], sensor[1], sensor[2], sensor[3])) add_entities(dev, True) diff --git a/homeassistant/components/danfoss_air/switch.py b/homeassistant/components/danfoss_air/switch.py index 4e7fce28dc7..8a1e0f9c746 100644 --- a/homeassistant/components/danfoss_air/switch.py +++ b/homeassistant/components/danfoss_air/switch.py @@ -15,25 +15,30 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = hass.data[DANFOSS_AIR_DOMAIN] switches = [ - ["Danfoss Air Boost", - ReadCommand.boost, - UpdateCommand.boost_activate, - UpdateCommand.boost_deactivate], - ["Danfoss Air Bypass", - ReadCommand.bypass, - UpdateCommand.bypass_activate, - UpdateCommand.bypass_deactivate], - ["Danfoss Air Automatic Bypass", - ReadCommand.automatic_bypass, - UpdateCommand.bypass_activate, - UpdateCommand.bypass_deactivate], + [ + "Danfoss Air Boost", + ReadCommand.boost, + UpdateCommand.boost_activate, + UpdateCommand.boost_deactivate, + ], + [ + "Danfoss Air Bypass", + ReadCommand.bypass, + UpdateCommand.bypass_activate, + UpdateCommand.bypass_deactivate, + ], + [ + "Danfoss Air Automatic Bypass", + ReadCommand.automatic_bypass, + UpdateCommand.bypass_activate, + UpdateCommand.bypass_deactivate, + ], ] dev = [] for switch in switches: - dev.append(DanfossAir( - data, switch[0], switch[1], switch[2], switch[3])) + dev.append(DanfossAir(data, switch[0], switch[1], switch[2], switch[3])) add_entities(dev) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 394541378e4..e5886ae5e59 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -3,14 +3,20 @@ import logging from datetime import timedelta import voluptuous as vol -from requests.exceptions import ( - ConnectionError as ConnectError, HTTPError, Timeout) +from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - CONF_MONITORED_CONDITIONS, CONF_NAME, UNIT_UV_INDEX, CONF_SCAN_INTERVAL) + ATTR_ATTRIBUTION, + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + UNIT_UV_INDEX, + CONF_SCAN_INTERVAL, +) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -18,187 +24,451 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Dark Sky" -CONF_FORECAST = 'forecast' -CONF_HOURLY_FORECAST = 'hourly_forecast' -CONF_LANGUAGE = 'language' -CONF_UNITS = 'units' +CONF_FORECAST = "forecast" +CONF_HOURLY_FORECAST = "hourly_forecast" +CONF_LANGUAGE = "language" +CONF_UNITS = "units" -DEFAULT_LANGUAGE = 'en' -DEFAULT_NAME = 'Dark Sky' +DEFAULT_LANGUAGE = "en" +DEFAULT_NAME = "Dark Sky" SCAN_INTERVAL = timedelta(seconds=300) DEPRECATED_SENSOR_TYPES = { - 'apparent_temperature_max', - 'apparent_temperature_min', - 'temperature_max', - 'temperature_min', + "apparent_temperature_max", + "apparent_temperature_min", + "temperature_max", + "temperature_min", } # Sensor types are defined like so: # Name, si unit, us unit, ca unit, uk unit, uk2 unit SENSOR_TYPES = { - 'summary': ['Summary', None, None, None, None, None, None, - ['currently', 'hourly', 'daily']], - 'minutely_summary': ['Minutely Summary', - None, None, None, None, None, None, []], - 'hourly_summary': ['Hourly Summary', None, None, None, None, None, None, - []], - 'daily_summary': ['Daily Summary', None, None, None, None, None, None, []], - 'icon': ['Icon', None, None, None, None, None, None, - ['currently', 'hourly', 'daily']], - 'nearest_storm_distance': ['Nearest Storm Distance', - 'km', 'mi', 'km', 'km', 'mi', - 'mdi:weather-lightning', ['currently']], - 'nearest_storm_bearing': ['Nearest Storm Bearing', - '°', '°', '°', '°', '°', - 'mdi:weather-lightning', ['currently']], - 'precip_type': ['Precip', None, None, None, None, None, - 'mdi:weather-pouring', - ['currently', 'minutely', 'hourly', 'daily']], - 'precip_intensity': ['Precip Intensity', - 'mm/h', 'in', 'mm/h', 'mm/h', 'mm/h', - 'mdi:weather-rainy', - ['currently', 'minutely', 'hourly', 'daily']], - 'precip_probability': ['Precip Probability', - '%', '%', '%', '%', '%', 'mdi:water-percent', - ['currently', 'minutely', 'hourly', 'daily']], - 'precip_accumulation': ['Precip Accumulation', - 'cm', 'in', 'cm', 'cm', 'cm', 'mdi:weather-snowy', - ['hourly', 'daily']], - 'temperature': ['Temperature', - '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['currently', 'hourly']], - 'apparent_temperature': ['Apparent Temperature', - '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['currently', 'hourly']], - 'dew_point': ['Dew Point', '°C', '°F', '°C', '°C', '°C', - 'mdi:thermometer', ['currently', 'hourly', 'daily']], - 'wind_speed': ['Wind Speed', 'm/s', 'mph', 'km/h', 'mph', 'mph', - 'mdi:weather-windy', ['currently', 'hourly', 'daily']], - 'wind_bearing': ['Wind Bearing', '°', '°', '°', '°', '°', 'mdi:compass', - ['currently', 'hourly', 'daily']], - 'wind_gust': ['Wind Gust', 'm/s', 'mph', 'km/h', 'mph', 'mph', - 'mdi:weather-windy-variant', - ['currently', 'hourly', 'daily']], - 'cloud_cover': ['Cloud Coverage', '%', '%', '%', '%', '%', - 'mdi:weather-partlycloudy', - ['currently', 'hourly', 'daily']], - 'humidity': ['Humidity', '%', '%', '%', '%', '%', 'mdi:water-percent', - ['currently', 'hourly', 'daily']], - 'pressure': ['Pressure', 'mbar', 'mbar', 'mbar', 'mbar', 'mbar', - 'mdi:gauge', ['currently', 'hourly', 'daily']], - 'visibility': ['Visibility', 'km', 'mi', 'km', 'km', 'mi', 'mdi:eye', - ['currently', 'hourly', 'daily']], - 'ozone': ['Ozone', 'DU', 'DU', 'DU', 'DU', 'DU', 'mdi:eye', - ['currently', 'hourly', 'daily']], - 'apparent_temperature_max': ['Daily High Apparent Temperature', - '°C', '°F', '°C', '°C', '°C', - 'mdi:thermometer', ['daily']], - 'apparent_temperature_high': ["Daytime High Apparent Temperature", - '°C', '°F', '°C', '°C', '°C', - 'mdi:thermometer', ['daily']], - 'apparent_temperature_min': ['Daily Low Apparent Temperature', - '°C', '°F', '°C', '°C', '°C', - 'mdi:thermometer', ['daily']], - 'apparent_temperature_low': ['Overnight Low Apparent Temperature', - '°C', '°F', '°C', '°C', '°C', - 'mdi:thermometer', ['daily']], - 'temperature_max': ['Daily High Temperature', - '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['daily']], - 'temperature_high': ['Daytime High Temperature', - '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['daily']], - 'temperature_min': ['Daily Low Temperature', - '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['daily']], - 'temperature_low': ['Overnight Low Temperature', - '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['daily']], - 'precip_intensity_max': ['Daily Max Precip Intensity', - 'mm/h', 'in', 'mm/h', 'mm/h', 'mm/h', - 'mdi:thermometer', ['daily']], - 'uv_index': ['UV Index', - UNIT_UV_INDEX, UNIT_UV_INDEX, UNIT_UV_INDEX, - UNIT_UV_INDEX, UNIT_UV_INDEX, 'mdi:weather-sunny', - ['currently', 'hourly', 'daily']], - 'moon_phase': ['Moon Phase', None, None, None, None, None, - 'mdi:weather-night', ['daily']], - 'sunrise_time': ['Sunrise', None, None, None, None, None, - 'mdi:white-balance-sunny', ['daily']], - 'sunset_time': ['Sunset', None, None, None, None, None, - 'mdi:weather-night', ['daily']], - 'alerts': ['Alerts', None, None, None, None, None, - 'mdi:alert-circle-outline', []] + "summary": [ + "Summary", + None, + None, + None, + None, + None, + None, + ["currently", "hourly", "daily"], + ], + "minutely_summary": ["Minutely Summary", None, None, None, None, None, None, []], + "hourly_summary": ["Hourly Summary", None, None, None, None, None, None, []], + "daily_summary": ["Daily Summary", None, None, None, None, None, None, []], + "icon": [ + "Icon", + None, + None, + None, + None, + None, + None, + ["currently", "hourly", "daily"], + ], + "nearest_storm_distance": [ + "Nearest Storm Distance", + "km", + "mi", + "km", + "km", + "mi", + "mdi:weather-lightning", + ["currently"], + ], + "nearest_storm_bearing": [ + "Nearest Storm Bearing", + "°", + "°", + "°", + "°", + "°", + "mdi:weather-lightning", + ["currently"], + ], + "precip_type": [ + "Precip", + None, + None, + None, + None, + None, + "mdi:weather-pouring", + ["currently", "minutely", "hourly", "daily"], + ], + "precip_intensity": [ + "Precip Intensity", + "mm/h", + "in", + "mm/h", + "mm/h", + "mm/h", + "mdi:weather-rainy", + ["currently", "minutely", "hourly", "daily"], + ], + "precip_probability": [ + "Precip Probability", + "%", + "%", + "%", + "%", + "%", + "mdi:water-percent", + ["currently", "minutely", "hourly", "daily"], + ], + "precip_accumulation": [ + "Precip Accumulation", + "cm", + "in", + "cm", + "cm", + "cm", + "mdi:weather-snowy", + ["hourly", "daily"], + ], + "temperature": [ + "Temperature", + "°C", + "°F", + "°C", + "°C", + "°C", + "mdi:thermometer", + ["currently", "hourly"], + ], + "apparent_temperature": [ + "Apparent Temperature", + "°C", + "°F", + "°C", + "°C", + "°C", + "mdi:thermometer", + ["currently", "hourly"], + ], + "dew_point": [ + "Dew Point", + "°C", + "°F", + "°C", + "°C", + "°C", + "mdi:thermometer", + ["currently", "hourly", "daily"], + ], + "wind_speed": [ + "Wind Speed", + "m/s", + "mph", + "km/h", + "mph", + "mph", + "mdi:weather-windy", + ["currently", "hourly", "daily"], + ], + "wind_bearing": [ + "Wind Bearing", + "°", + "°", + "°", + "°", + "°", + "mdi:compass", + ["currently", "hourly", "daily"], + ], + "wind_gust": [ + "Wind Gust", + "m/s", + "mph", + "km/h", + "mph", + "mph", + "mdi:weather-windy-variant", + ["currently", "hourly", "daily"], + ], + "cloud_cover": [ + "Cloud Coverage", + "%", + "%", + "%", + "%", + "%", + "mdi:weather-partlycloudy", + ["currently", "hourly", "daily"], + ], + "humidity": [ + "Humidity", + "%", + "%", + "%", + "%", + "%", + "mdi:water-percent", + ["currently", "hourly", "daily"], + ], + "pressure": [ + "Pressure", + "mbar", + "mbar", + "mbar", + "mbar", + "mbar", + "mdi:gauge", + ["currently", "hourly", "daily"], + ], + "visibility": [ + "Visibility", + "km", + "mi", + "km", + "km", + "mi", + "mdi:eye", + ["currently", "hourly", "daily"], + ], + "ozone": [ + "Ozone", + "DU", + "DU", + "DU", + "DU", + "DU", + "mdi:eye", + ["currently", "hourly", "daily"], + ], + "apparent_temperature_max": [ + "Daily High Apparent Temperature", + "°C", + "°F", + "°C", + "°C", + "°C", + "mdi:thermometer", + ["daily"], + ], + "apparent_temperature_high": [ + "Daytime High Apparent Temperature", + "°C", + "°F", + "°C", + "°C", + "°C", + "mdi:thermometer", + ["daily"], + ], + "apparent_temperature_min": [ + "Daily Low Apparent Temperature", + "°C", + "°F", + "°C", + "°C", + "°C", + "mdi:thermometer", + ["daily"], + ], + "apparent_temperature_low": [ + "Overnight Low Apparent Temperature", + "°C", + "°F", + "°C", + "°C", + "°C", + "mdi:thermometer", + ["daily"], + ], + "temperature_max": [ + "Daily High Temperature", + "°C", + "°F", + "°C", + "°C", + "°C", + "mdi:thermometer", + ["daily"], + ], + "temperature_high": [ + "Daytime High Temperature", + "°C", + "°F", + "°C", + "°C", + "°C", + "mdi:thermometer", + ["daily"], + ], + "temperature_min": [ + "Daily Low Temperature", + "°C", + "°F", + "°C", + "°C", + "°C", + "mdi:thermometer", + ["daily"], + ], + "temperature_low": [ + "Overnight Low Temperature", + "°C", + "°F", + "°C", + "°C", + "°C", + "mdi:thermometer", + ["daily"], + ], + "precip_intensity_max": [ + "Daily Max Precip Intensity", + "mm/h", + "in", + "mm/h", + "mm/h", + "mm/h", + "mdi:thermometer", + ["daily"], + ], + "uv_index": [ + "UV Index", + UNIT_UV_INDEX, + UNIT_UV_INDEX, + UNIT_UV_INDEX, + UNIT_UV_INDEX, + UNIT_UV_INDEX, + "mdi:weather-sunny", + ["currently", "hourly", "daily"], + ], + "moon_phase": [ + "Moon Phase", + None, + None, + None, + None, + None, + "mdi:weather-night", + ["daily"], + ], + "sunrise_time": [ + "Sunrise", + None, + None, + None, + None, + None, + "mdi:white-balance-sunny", + ["daily"], + ], + "sunset_time": [ + "Sunset", + None, + None, + None, + None, + None, + "mdi:weather-night", + ["daily"], + ], + "alerts": ["Alerts", None, None, None, None, None, "mdi:alert-circle-outline", []], } CONDITION_PICTURES = { - 'clear-day': ['/static/images/darksky/weather-sunny.svg', - 'mdi:weather-sunny'], - 'clear-night': ['/static/images/darksky/weather-night.svg', - 'mdi:weather-sunny'], - 'rain': ['/static/images/darksky/weather-pouring.svg', - 'mdi:weather-pouring'], - 'snow': ['/static/images/darksky/weather-snowy.svg', - 'mdi:weather-snowy'], - 'sleet': ['/static/images/darksky/weather-hail.svg', - 'mdi:weather-snowy-rainy'], - 'wind': ['/static/images/darksky/weather-windy.svg', - 'mdi:weather-windy'], - 'fog': ['/static/images/darksky/weather-fog.svg', - 'mdi:weather-fog'], - 'cloudy': ['/static/images/darksky/weather-cloudy.svg', - 'mdi:weather-cloudy'], - 'partly-cloudy-day': ['/static/images/darksky/weather-partlycloudy.svg', - 'mdi:weather-partlycloudy'], - 'partly-cloudy-night': ['/static/images/darksky/weather-cloudy.svg', - 'mdi:weather-partlycloudy'], + "clear-day": ["/static/images/darksky/weather-sunny.svg", "mdi:weather-sunny"], + "clear-night": ["/static/images/darksky/weather-night.svg", "mdi:weather-sunny"], + "rain": ["/static/images/darksky/weather-pouring.svg", "mdi:weather-pouring"], + "snow": ["/static/images/darksky/weather-snowy.svg", "mdi:weather-snowy"], + "sleet": ["/static/images/darksky/weather-hail.svg", "mdi:weather-snowy-rainy"], + "wind": ["/static/images/darksky/weather-windy.svg", "mdi:weather-windy"], + "fog": ["/static/images/darksky/weather-fog.svg", "mdi:weather-fog"], + "cloudy": ["/static/images/darksky/weather-cloudy.svg", "mdi:weather-cloudy"], + "partly-cloudy-day": [ + "/static/images/darksky/weather-partlycloudy.svg", + "mdi:weather-partlycloudy", + ], + "partly-cloudy-night": [ + "/static/images/darksky/weather-cloudy.svg", + "mdi:weather-partlycloudy", + ], } # Language Supported Codes LANGUAGE_CODES = [ - 'ar', 'az', 'be', 'bg', 'bn', 'bs', 'ca', 'cs', 'da', 'de', 'el', 'en', - 'ja', 'ka', 'kn', 'ko', 'eo', 'es', 'et', 'fi', 'fr', 'he', 'hi', 'hr', - 'hu', 'id', 'is', 'it', 'kw', 'lv', 'ml', 'mr', 'nb', 'nl', 'pa', 'pl', - 'pt', 'ro', 'ru', 'sk', 'sl', 'sr', 'sv', 'ta', 'te', 'tet', 'tr', 'uk', - 'ur', 'x-pig-latin', 'zh', 'zh-tw', + "ar", + "az", + "be", + "bg", + "bn", + "bs", + "ca", + "cs", + "da", + "de", + "el", + "en", + "ja", + "ka", + "kn", + "ko", + "eo", + "es", + "et", + "fi", + "fr", + "he", + "hi", + "hr", + "hu", + "id", + "is", + "it", + "kw", + "lv", + "ml", + "mr", + "nb", + "nl", + "pa", + "pl", + "pt", + "ro", + "ru", + "sk", + "sl", + "sr", + "sv", + "ta", + "te", + "tet", + "tr", + "uk", + "ur", + "x-pig-latin", + "zh", + "zh-tw", ] -ALLOWED_UNITS = ['auto', 'si', 'us', 'ca', 'uk', 'uk2'] +ALLOWED_UNITS = ["auto", "si", "us", "ca", "uk", "uk2"] -ALERTS_ATTRS = [ - 'time', - 'description', - 'expires', - 'severity', - 'uri', - 'regions', - 'title' -] +ALERTS_ATTRS = ["time", "description", "expires", "severity", "uri", "regions", "title"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNITS): vol.In(ALLOWED_UNITS), - vol.Optional(CONF_LANGUAGE, - default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), - vol.Inclusive( - CONF_LATITUDE, - 'coordinates', - 'Latitude and longitude must exist together' - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, - 'coordinates', - 'Latitude and longitude must exist together' - ): cv.longitude, - vol.Optional(CONF_FORECAST): - vol.All(cv.ensure_list, [vol.Range(min=0, max=7)]), - vol.Optional(CONF_HOURLY_FORECAST): - vol.All(cv.ensure_list, [vol.Range(min=0, max=48)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MONITORED_CONDITIONS): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNITS): vol.In(ALLOWED_UNITS), + vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), + vol.Inclusive( + CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" + ): cv.latitude, + vol.Inclusive( + CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" + ): cv.longitude, + vol.Optional(CONF_FORECAST): vol.All(cv.ensure_list, [vol.Range(min=0, max=7)]), + vol.Optional(CONF_HOURLY_FORECAST): vol.All( + cv.ensure_list, [vol.Range(min=0, max=48)] + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -211,13 +481,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if CONF_UNITS in config: units = config[CONF_UNITS] elif hass.config.units.is_metric: - units = 'si' + units = "si" else: - units = 'us' + units = "us" forecast_data = DarkSkyData( - api_key=config.get(CONF_API_KEY, None), latitude=latitude, - longitude=longitude, units=units, language=language, interval=interval) + api_key=config.get(CONF_API_KEY, None), + latitude=latitude, + longitude=longitude, + units=units, + language=language, + interval=interval, + ) forecast_data.update() forecast_data.update_currently() @@ -233,22 +508,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for variable in config[CONF_MONITORED_CONDITIONS]: if variable in DEPRECATED_SENSOR_TYPES: _LOGGER.warning("Monitored condition %s is deprecated", variable) - if (not SENSOR_TYPES[variable][7] or - 'currently' in SENSOR_TYPES[variable][7]): - if variable == 'alerts': - sensors.append(DarkSkyAlertSensor( - forecast_data, variable, name)) + if not SENSOR_TYPES[variable][7] or "currently" in SENSOR_TYPES[variable][7]: + if variable == "alerts": + sensors.append(DarkSkyAlertSensor(forecast_data, variable, name)) else: sensors.append(DarkSkySensor(forecast_data, variable, name)) - if forecast is not None and 'daily' in SENSOR_TYPES[variable][7]: + if forecast is not None and "daily" in SENSOR_TYPES[variable][7]: for forecast_day in forecast: - sensors.append(DarkSkySensor( - forecast_data, variable, name, forecast_day=forecast_day)) - if forecast_hour is not None and 'hourly' in SENSOR_TYPES[variable][7]: + sensors.append( + DarkSkySensor( + forecast_data, variable, name, forecast_day=forecast_day + ) + ) + if forecast_hour is not None and "hourly" in SENSOR_TYPES[variable][7]: for forecast_h in forecast_hour: - sensors.append(DarkSkySensor( - forecast_data, variable, name, forecast_hour=forecast_h)) + sensors.append( + DarkSkySensor( + forecast_data, variable, name, forecast_hour=forecast_h + ) + ) add_entities(sensors, True) @@ -256,8 +535,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DarkSkySensor(Entity): """Implementation of a Dark Sky sensor.""" - def __init__(self, forecast_data, sensor_type, name, - forecast_day=None, forecast_hour=None): + def __init__( + self, forecast_data, sensor_type, name, forecast_day=None, forecast_hour=None + ): """Initialize the sensor.""" self.client_name = name self._name = SENSOR_TYPES[sensor_type][0] @@ -273,13 +553,10 @@ class DarkSkySensor(Entity): def name(self): """Return the name of the sensor.""" if self.forecast_day is not None: - return '{} {} {}d'.format( - self.client_name, self._name, self.forecast_day) + return "{} {} {}d".format(self.client_name, self._name, self.forecast_day) if self.forecast_hour is not None: - return '{} {} {}h'.format( - self.client_name, self._name, self.forecast_hour) - return '{} {}'.format( - self.client_name, self._name) + return "{} {} {}h".format(self.client_name, self._name, self.forecast_hour) + return "{} {}".format(self.client_name, self._name) @property def state(self): @@ -299,7 +576,7 @@ class DarkSkySensor(Entity): @property def entity_picture(self): """Return the entity picture to use in the frontend, if any.""" - if self._icon is None or 'summary' not in self.type: + if self._icon is None or "summary" not in self.type: return None if self._icon in CONDITION_PICTURES: @@ -309,19 +586,15 @@ class DarkSkySensor(Entity): def update_unit_of_measurement(self): """Update units based on unit system.""" - unit_index = { - 'si': 1, - 'us': 2, - 'ca': 3, - 'uk': 4, - 'uk2': 5 - }.get(self.unit_system, 1) + unit_index = {"si": 1, "us": 2, "ca": 3, "uk": 4, "uk2": 5}.get( + self.unit_system, 1 + ) self._unit_of_measurement = SENSOR_TYPES[self.type][unit_index] @property def icon(self): """Icon to use in the frontend, if any.""" - if 'summary' in self.type and self._icon in CONDITION_PICTURES: + if "summary" in self.type and self._icon in CONDITION_PICTURES: return CONDITION_PICTURES[self._icon][1] return SENSOR_TYPES[self.type][6] @@ -329,9 +602,7 @@ class DarkSkySensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - } + return {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Get the latest data from Dark Sky and updates the states.""" @@ -342,32 +613,32 @@ class DarkSkySensor(Entity): self.forecast_data.update() self.update_unit_of_measurement() - if self.type == 'minutely_summary': + if self.type == "minutely_summary": self.forecast_data.update_minutely() minutely = self.forecast_data.data_minutely - self._state = getattr(minutely, 'summary', '') - self._icon = getattr(minutely, 'icon', '') - elif self.type == 'hourly_summary': + self._state = getattr(minutely, "summary", "") + self._icon = getattr(minutely, "icon", "") + elif self.type == "hourly_summary": self.forecast_data.update_hourly() hourly = self.forecast_data.data_hourly - self._state = getattr(hourly, 'summary', '') - self._icon = getattr(hourly, 'icon', '') + self._state = getattr(hourly, "summary", "") + self._icon = getattr(hourly, "icon", "") elif self.forecast_hour is not None: self.forecast_data.update_hourly() hourly = self.forecast_data.data_hourly - if hasattr(hourly, 'data'): + if hasattr(hourly, "data"): self._state = self.get_state(hourly.data[self.forecast_hour]) else: self._state = 0 - elif self.type == 'daily_summary': + elif self.type == "daily_summary": self.forecast_data.update_daily() daily = self.forecast_data.data_daily - self._state = getattr(daily, 'summary', '') - self._icon = getattr(daily, 'icon', '') + self._state = getattr(daily, "summary", "") + self._icon = getattr(daily, "icon", "") elif self.forecast_day is not None: self.forecast_data.update_daily() daily = self.forecast_data.data_daily - if hasattr(daily, 'data'): + if hasattr(daily, "data"): self._state = self.get_state(daily.data[self.forecast_day]) else: self._state = 0 @@ -388,21 +659,31 @@ class DarkSkySensor(Entity): if state is None: return state - if 'summary' in self.type: - self._icon = getattr(data, 'icon', '') + if "summary" in self.type: + self._icon = getattr(data, "icon", "") # Some state data needs to be rounded to whole values or converted to # percentages - if self.type in ['precip_probability', 'cloud_cover', 'humidity']: + if self.type in ["precip_probability", "cloud_cover", "humidity"]: return round(state * 100, 1) - if self.type in ['dew_point', 'temperature', 'apparent_temperature', - 'temperature_low', 'apparent_temperature_low', - 'temperature_min', 'apparent_temperature_min', - 'temperature_high', 'apparent_temperature_high', - 'temperature_max', 'apparent_temperature_max' - 'precip_accumulation', 'pressure', 'ozone', - 'uvIndex']: + if self.type in [ + "dew_point", + "temperature", + "apparent_temperature", + "temperature_low", + "apparent_temperature_low", + "temperature_min", + "apparent_temperature_min", + "temperature_high", + "apparent_temperature_high", + "temperature_max", + "apparent_temperature_max", + "precip_accumulation", + "pressure", + "ozone", + "uvIndex", + ]: return round(state, 1) return state @@ -423,8 +704,7 @@ class DarkSkyAlertSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format( - self.client_name, self._name) + return "{} {}".format(self.client_name, self._name) @property def state(self): @@ -469,7 +749,7 @@ class DarkSkyAlertSensor(Entity): for i, alert in enumerate(data): for attr in ALERTS_ATTRS: if multiple_alerts: - dkey = attr + '_' + str(i) + dkey = attr + "_" + str(i) else: dkey = attr alerts[dkey] = getattr(alert, attr) @@ -484,15 +764,14 @@ def convert_to_camel(data): This is not pythonic, but needed for certain situations. """ - components = data.split('_') + components = data.split("_") return components[0] + "".join(x.title() for x in components[1:]) class DarkSkyData: """Get the latest data from Darksky.""" - def __init__( - self, api_key, latitude, longitude, units, language, interval): + def __init__(self, api_key, latitude, longitude, units, language, interval): """Initialize the data object.""" self._api_key = api_key self.latitude = latitude @@ -522,12 +801,16 @@ class DarkSkyData: try: self.data = forecastio.load_forecast( - self._api_key, self.latitude, self.longitude, units=self.units, - lang=self.language) + self._api_key, + self.latitude, + self.longitude, + units=self.units, + lang=self.language, + ) except (ConnectError, HTTPError, Timeout, ValueError) as error: _LOGGER.error("Unable to connect to Dark Sky: %s", error) self.data = None - self.unit_system = self.data and self.data.json['flags']['units'] + self.unit_system = self.data and self.data.json["flags"]["units"] def _update_currently(self): """Update currently data.""" diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 84de690504e..e95381cdf73 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -2,54 +2,71 @@ from datetime import datetime, timedelta import logging -from requests.exceptions import ( - ConnectionError as ConnectError, HTTPError, Timeout) +from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + PLATFORM_SCHEMA, + WeatherEntity, +) from homeassistant.const import ( - CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, - PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS, TEMP_FAHRENHEIT) + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, + PRESSURE_HPA, + PRESSURE_INHG, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from homeassistant.util.pressure import convert as convert_pressure + _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Dark Sky" -FORECAST_MODE = ['hourly', 'daily'] +FORECAST_MODE = ["hourly", "daily"] MAP_CONDITION = { - 'clear-day': 'sunny', - 'clear-night': 'clear-night', - 'rain': 'rainy', - 'snow': 'snowy', - 'sleet': 'snowy-rainy', - 'wind': 'windy', - 'fog': 'fog', - 'cloudy': 'cloudy', - 'partly-cloudy-day': 'partlycloudy', - 'partly-cloudy-night': 'partlycloudy', - 'hail': 'hail', - 'thunderstorm': 'lightning', - 'tornado': None, + "clear-day": "sunny", + "clear-night": "clear-night", + "rain": "rainy", + "snow": "snowy", + "sleet": "snowy-rainy", + "wind": "windy", + "fog": "fog", + "cloudy": "cloudy", + "partly-cloudy-day": "partlycloudy", + "partly-cloudy-night": "partlycloudy", + "hail": "hail", + "thunderstorm": "lightning", + "tornado": None, } -CONF_UNITS = 'units' +CONF_UNITS = "units" -DEFAULT_NAME = 'Dark Sky' +DEFAULT_NAME = "Dark Sky" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_MODE, default='hourly'): vol.In(FORECAST_MODE), - vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_MODE, default="hourly"): vol.In(FORECAST_MODE), + vol.Optional(CONF_UNITS): vol.In(["auto", "si", "us", "ca", "uk", "uk2"]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=3) @@ -63,10 +80,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): units = config.get(CONF_UNITS) if not units: - units = 'ca' if hass.config.units.is_metric else 'us' + units = "ca" if hass.config.units.is_metric else "us" - dark_sky = DarkSkyData( - config.get(CONF_API_KEY), latitude, longitude, units) + dark_sky = DarkSkyData(config.get(CONF_API_KEY), latitude, longitude, units) add_entities([DarkSkyWeather(name, dark_sky, mode)], True) @@ -98,54 +114,52 @@ class DarkSkyWeather(WeatherEntity): @property def temperature(self): """Return the temperature.""" - return self._ds_currently.get('temperature') + return self._ds_currently.get("temperature") @property def temperature_unit(self): """Return the unit of measurement.""" if self._dark_sky.units is None: return None - return TEMP_FAHRENHEIT if 'us' in self._dark_sky.units \ - else TEMP_CELSIUS + return TEMP_FAHRENHEIT if "us" in self._dark_sky.units else TEMP_CELSIUS @property def humidity(self): """Return the humidity.""" - return round(self._ds_currently.get('humidity') * 100.0, 2) + return round(self._ds_currently.get("humidity") * 100.0, 2) @property def wind_speed(self): """Return the wind speed.""" - return self._ds_currently.get('windSpeed') + return self._ds_currently.get("windSpeed") @property def wind_bearing(self): """Return the wind bearing.""" - return self._ds_currently.get('windBearing') + return self._ds_currently.get("windBearing") @property def ozone(self): """Return the ozone level.""" - return self._ds_currently.get('ozone') + return self._ds_currently.get("ozone") @property def pressure(self): """Return the pressure.""" - pressure = self._ds_currently.get('pressure') - if 'us' in self._dark_sky.units: - return round( - convert_pressure(pressure, PRESSURE_HPA, PRESSURE_INHG), 2) + pressure = self._ds_currently.get("pressure") + if "us" in self._dark_sky.units: + return round(convert_pressure(pressure, PRESSURE_HPA, PRESSURE_INHG), 2) return pressure @property def visibility(self): """Return the visibility.""" - return self._ds_currently.get('visibility') + return self._ds_currently.get("visibility") @property def condition(self): """Return the weather condition.""" - return MAP_CONDITION.get(self._ds_currently.get('icon')) + return MAP_CONDITION.get(self._ds_currently.get("icon")) @property def forecast(self): @@ -161,34 +175,37 @@ class DarkSkyWeather(WeatherEntity): data = None - if self._mode == 'daily': - data = [{ - ATTR_FORECAST_TIME: - datetime.fromtimestamp(entry.d.get('time')).isoformat(), - ATTR_FORECAST_TEMP: - entry.d.get('temperatureHigh'), - ATTR_FORECAST_TEMP_LOW: - entry.d.get('temperatureLow'), - ATTR_FORECAST_PRECIPITATION: - calc_precipitation(entry.d.get('precipIntensity'), 24), - ATTR_FORECAST_WIND_SPEED: - entry.d.get('windSpeed'), - ATTR_FORECAST_WIND_BEARING: - entry.d.get('windBearing'), - ATTR_FORECAST_CONDITION: - MAP_CONDITION.get(entry.d.get('icon')) - } for entry in self._ds_daily.data] + if self._mode == "daily": + data = [ + { + ATTR_FORECAST_TIME: datetime.fromtimestamp( + entry.d.get("time") + ).isoformat(), + ATTR_FORECAST_TEMP: entry.d.get("temperatureHigh"), + ATTR_FORECAST_TEMP_LOW: entry.d.get("temperatureLow"), + ATTR_FORECAST_PRECIPITATION: calc_precipitation( + entry.d.get("precipIntensity"), 24 + ), + ATTR_FORECAST_WIND_SPEED: entry.d.get("windSpeed"), + ATTR_FORECAST_WIND_BEARING: entry.d.get("windBearing"), + ATTR_FORECAST_CONDITION: MAP_CONDITION.get(entry.d.get("icon")), + } + for entry in self._ds_daily.data + ] else: - data = [{ - ATTR_FORECAST_TIME: - datetime.fromtimestamp(entry.d.get('time')).isoformat(), - ATTR_FORECAST_TEMP: - entry.d.get('temperature'), - ATTR_FORECAST_PRECIPITATION: - calc_precipitation(entry.d.get('precipIntensity'), 1), - ATTR_FORECAST_CONDITION: - MAP_CONDITION.get(entry.d.get('icon')) - } for entry in self._ds_hourly.data] + data = [ + { + ATTR_FORECAST_TIME: datetime.fromtimestamp( + entry.d.get("time") + ).isoformat(), + ATTR_FORECAST_TEMP: entry.d.get("temperature"), + ATTR_FORECAST_PRECIPITATION: calc_precipitation( + entry.d.get("precipIntensity"), 1 + ), + ATTR_FORECAST_CONDITION: MAP_CONDITION.get(entry.d.get("icon")), + } + for entry in self._ds_hourly.data + ] return data @@ -224,8 +241,8 @@ class DarkSkyData: try: self.data = forecastio.load_forecast( - self._api_key, self.latitude, self.longitude, - units=self.requested_units) + self._api_key, self.latitude, self.longitude, units=self.requested_units + ) self.currently = self.data.currently() self.hourly = self.data.hourly() self.daily = self.data.daily() @@ -238,4 +255,4 @@ class DarkSkyData: """Get the unit system of returned data.""" if self.data is None: return None - return self.data.json.get('flags').get('units') + return self.data.json.get("flags").get("units") diff --git a/homeassistant/components/datadog/__init__.py b/homeassistant/components/datadog/__init__.py index a59d828301c..1ad4ed9aab8 100644 --- a/homeassistant/components/datadog/__init__.py +++ b/homeassistant/components/datadog/__init__.py @@ -4,29 +4,40 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_HOST, CONF_PORT, CONF_PREFIX, EVENT_LOGBOOK_ENTRY, - EVENT_STATE_CHANGED, STATE_UNKNOWN) + CONF_HOST, + CONF_PORT, + CONF_PREFIX, + EVENT_LOGBOOK_ENTRY, + EVENT_STATE_CHANGED, + STATE_UNKNOWN, +) from homeassistant.helpers import state as state_helper import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_RATE = 'rate' -DEFAULT_HOST = 'localhost' +CONF_RATE = "rate" +DEFAULT_HOST = "localhost" DEFAULT_PORT = 8125 -DEFAULT_PREFIX = 'hass' +DEFAULT_PREFIX = "hass" DEFAULT_RATE = 1 -DOMAIN = 'datadog' +DOMAIN = "datadog" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string, - vol.Optional(CONF_RATE, default=DEFAULT_RATE): - vol.All(vol.Coerce(int), vol.Range(min=1)), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string, + vol.Optional(CONF_RATE, default=DEFAULT_RATE): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -43,28 +54,28 @@ def setup(hass, config): def logbook_entry_listener(event): """Listen for logbook entries and send them as events.""" - name = event.data.get('name') - message = event.data.get('message') + name = event.data.get("name") + message = event.data.get("message") statsd.event( title="Home Assistant", text="%%% \n **{}** {} \n %%%".format(name, message), tags=[ - "entity:{}".format(event.data.get('entity_id')), - "domain:{}".format(event.data.get('domain')) - ] + "entity:{}".format(event.data.get("entity_id")), + "domain:{}".format(event.data.get("domain")), + ], ) - _LOGGER.debug('Sent event %s', event.data.get('entity_id')) + _LOGGER.debug("Sent event %s", event.data.get("entity_id")) def state_changed_listener(event): """Listen for new messages on the bus and sends them to Datadog.""" - state = event.data.get('new_state') + state = event.data.get("new_state") if state is None or state.state == STATE_UNKNOWN: return - if state.attributes.get('hidden') is True: + if state.attributes.get("hidden") is True: return states = dict(state.attributes) @@ -73,23 +84,20 @@ def setup(hass, config): for key, value in states.items(): if isinstance(value, (float, int)): - attribute = "{}.{}".format(metric, key.replace(' ', '_')) - statsd.gauge( - attribute, value, sample_rate=sample_rate, tags=tags) + attribute = "{}.{}".format(metric, key.replace(" ", "_")) + statsd.gauge(attribute, value, sample_rate=sample_rate, tags=tags) - _LOGGER.debug( - "Sent metric %s: %s (tags: %s)", attribute, value, tags) + _LOGGER.debug("Sent metric %s: %s (tags: %s)", attribute, value, tags) try: value = state_helper.state_as_number(state) except ValueError: - _LOGGER.debug( - "Error sending %s: %s (tags: %s)", metric, state.state, tags) + _LOGGER.debug("Error sending %s: %s (tags: %s)", metric, state.state, tags) return statsd.gauge(metric, value, sample_rate=sample_rate, tags=tags) - _LOGGER.debug('Sent metric %s: %s (tags: %s)', metric, value, tags) + _LOGGER.debug("Sent metric %s: %s (tags: %s)", metric, value, tags) hass.bus.listen(EVENT_LOGBOOK_ENTRY, logbook_entry_listener) hass.bus.listen(EVENT_STATE_CHANGED, state_changed_listener) diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index e412e33fa17..4a40561b9e3 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -6,29 +6,39 @@ import requests import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL) + CONF_HOST, + CONF_PASSWORD, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -_DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}') -_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})') +_DDWRT_DATA_REGEX = re.compile(r"\{(\w+)::([^\}]*)\}") +_MAC_REGEX = re.compile(r"(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})") DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True -CONF_WIRELESS_ONLY = 'wireless_only' +CONF_WIRELESS_ONLY = "wireless_only" DEFAULT_WIRELESS_ONLY = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(CONF_WIRELESS_ONLY, default=DEFAULT_WIRELESS_ONLY): cv.boolean -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_WIRELESS_ONLY, default=DEFAULT_WIRELESS_ONLY): cv.boolean, + } +) def get_scanner(hass, config): @@ -44,7 +54,7 @@ class DdWrtDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the DD-WRT scanner.""" - self.protocol = 'https' if config[CONF_SSL] else 'http' + self.protocol = "https" if config[CONF_SSL] else "http" self.verify_ssl = config[CONF_VERIFY_SSL] self.host = config[CONF_HOST] self.username = config[CONF_USERNAME] @@ -55,11 +65,10 @@ class DdWrtDeviceScanner(DeviceScanner): self.mac2name = {} # Test the router is accessible - url = '{}://{}/Status_Wireless.live.asp'.format( - self.protocol, self.host) + url = "{}://{}/Status_Wireless.live.asp".format(self.protocol, self.host) data = self.get_ddwrt_data(url) if not data: - raise ConnectionError('Cannot connect to DD-Wrt router') + raise ConnectionError("Cannot connect to DD-Wrt router") def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -71,22 +80,20 @@ class DdWrtDeviceScanner(DeviceScanner): """Return the name of the given device or None if we don't know.""" # If not initialised and not already scanned and not found. if device not in self.mac2name: - url = '{}://{}/Status_Lan.live.asp'.format( - self.protocol, self.host) + url = "{}://{}/Status_Lan.live.asp".format(self.protocol, self.host) data = self.get_ddwrt_data(url) if not data: return None - dhcp_leases = data.get('dhcp_leases', None) + dhcp_leases = data.get("dhcp_leases", None) if not dhcp_leases: return None # Remove leading and trailing quotes and spaces - cleaned_str = dhcp_leases.replace( - "\"", "").replace("\'", "").replace(" ", "") - elements = cleaned_str.split(',') + cleaned_str = dhcp_leases.replace('"', "").replace("'", "").replace(" ", "") + elements = cleaned_str.split(",") num_clients = int(len(elements) / 5) self.mac2name = {} for idx in range(0, num_clients): @@ -107,9 +114,8 @@ class DdWrtDeviceScanner(DeviceScanner): """ _LOGGER.info("Checking ARP") - endpoint = 'Wireless' if self.wireless_only else 'Lan' - url = '{}://{}/Status_{}.live.asp'.format( - self.protocol, self.host, endpoint) + endpoint = "Wireless" if self.wireless_only else "Lan" + url = "{}://{}/Status_{}.live.asp".format(self.protocol, self.host, endpoint) data = self.get_ddwrt_data(url) if not data: @@ -118,9 +124,9 @@ class DdWrtDeviceScanner(DeviceScanner): self.last_results = [] if self.wireless_only: - active_clients = data.get('active_wireless', None) + active_clients = data.get("active_wireless", None) else: - active_clients = data.get('arp_table', None) + active_clients = data.get("arp_table", None) if not active_clients: return False @@ -130,8 +136,7 @@ class DdWrtDeviceScanner(DeviceScanner): clean_str = active_clients.strip().strip("'") elements = clean_str.split("','") - self.last_results.extend(item for item in elements - if _MAC_REGEX.match(item)) + self.last_results.extend(item for item in elements if _MAC_REGEX.match(item)) return True @@ -139,8 +144,11 @@ class DdWrtDeviceScanner(DeviceScanner): """Retrieve data from DD-WRT and return parsed result.""" try: response = requests.get( - url, auth=(self.username, self.password), - timeout=4, verify=self.verify_ssl) + url, + auth=(self.username, self.password), + timeout=4, + verify=self.verify_ssl, + ) except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") return @@ -149,12 +157,12 @@ class DdWrtDeviceScanner(DeviceScanner): if response.status_code == 401: # Authentication error _LOGGER.exception( - "Failed to authenticate, check your username and password") + "Failed to authenticate, check your username and password" + ) return _LOGGER.error("Invalid response from DD-WRT: %s", response) def _parse_ddwrt_response(data_str): """Parse the DD-WRT data format.""" - return { - key: val for key, val in _DDWRT_DATA_REGEX.findall(data_str)} + return {key: val for key, val in _DDWRT_DATA_REGEX.findall(data_str)} diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index c2cc8f97a89..a02a6ff4223 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -2,13 +2,24 @@ "config": { "abort": { "already_configured": "\u041c\u043e\u0441\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "already_in_progress": "\u0412 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u0442\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f.", "no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ", - "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ" + "not_deconz_bridge": "\u041d\u0435 \u0435 deCONZ \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ", + "updated_instance": "\u041e\u0431\u043d\u043e\u0432\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0441 \u043d\u043e\u0432 \u0430\u0434\u0440\u0435\u0441" }, "error": { "no_key": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u043f\u043e\u043b\u0443\u0447\u0438 API \u043a\u043b\u044e\u0447" }, "step": { + "hassio_confirm": { + "data": { + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u043d\u0438 \u0441\u0435\u043d\u0437\u043e\u0440\u0438", + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0438 \u043e\u0442 deCONZ" + }, + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 deCONZ \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 \u0437\u0430 hass.io {addon}?", + "title": "deCONZ Zigbee \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0447\u0440\u0435\u0437 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430" + }, "init": { "data": { "host": "\u0410\u0434\u0440\u0435\u0441", @@ -19,6 +30,13 @@ "link": { "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 deCONZ\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Unlock Gateway\"", "title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u043d\u0438 \u0441\u0435\u043d\u0437\u043e\u0440\u0438", + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0438 \u043e\u0442 deCONZ" + }, + "title": "\u0414\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0438 \u043e\u043f\u0446\u0438\u0438 \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index e4e5f098a4d..f79538ffeb6 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -2,13 +2,24 @@ "config": { "abort": { "already_configured": "Bridge er allerede konfigureret", + "already_in_progress": "Bro konfiguration er allerede i gang.", "no_bridges": "Ingen deConz bridge fundet", - "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n deCONZ forekomst" + "not_deconz_bridge": "Ikke en deCONZ bro", + "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n deCONZ forekomst", + "updated_instance": "Opdaterede deCONZ instans med ny v\u00e6rtsadresse" }, "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8gle" }, "step": { + "hassio_confirm": { + "data": { + "allow_clip_sensor": "Tillad import af virtuelle sensorer", + "allow_deconz_groups": "Tillad import af deCONZ grupper" + }, + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ gateway leveret af Hass.io add-on {addon}?", + "title": "deCONZ Zigbee-gateway via Hass.io add-on" + }, "init": { "data": { "host": "V\u00e6rt", diff --git a/homeassistant/components/deconz/.translations/pt-BR.json b/homeassistant/components/deconz/.translations/pt-BR.json index dc7e682cafb..d066cbcc510 100644 --- a/homeassistant/components/deconz/.translations/pt-BR.json +++ b/homeassistant/components/deconz/.translations/pt-BR.json @@ -12,6 +12,14 @@ "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API" }, "step": { + "hassio_confirm": { + "data": { + "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais", + "allow_deconz_groups": "Permitir a importa\u00e7\u00e3o de grupos deCONZ" + }, + "description": "Deseja configurar o Home Assistant para conectar-se ao gateway deCONZ fornecido pelo add-on hass.io {addon} ?", + "title": "Gateway deCONZ Zigbee via add-on Hass.io" + }, "init": { "data": { "host": "Hospedeiro", diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 0c9efd8992b..fbe422bf927 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -23,12 +23,12 @@ "init": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "port": "\u901a\u8a0a\u57e0\uff08\u9810\u8a2d\u503c\uff1a'80'\uff09" + "port": "\u901a\u8a0a\u57e0" }, "title": "\u5b9a\u7fa9 deCONZ \u7db2\u95dc" }, "link": { - "description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u89e3\u9664\u7db2\u95dc\u9396\u5b9a\uff08Unlock Gateway\uff09\u300d\u6309\u9215", + "description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a -> \u7db2\u95dc -> \u9032\u968e\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u8a8d\u8b49\u7a0b\u5f0f\uff08Authenticate app\uff09\u300d\u6309\u9215", "title": "\u9023\u7d50\u81f3 deCONZ" }, "options": { @@ -39,6 +39,6 @@ "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee \u7db2\u95dc" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 71e03da70b7..68974d12253 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -3,42 +3,60 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( - CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers import config_validation as cv # Loading the config flow file will register the flow from .config_flow import get_master_gateway from .const import ( - CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, - CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN, _LOGGER) + CONF_ALLOW_CLIP_SENSOR, + CONF_ALLOW_DECONZ_GROUPS, + CONF_BRIDGEID, + CONF_MASTER_GATEWAY, + DEFAULT_PORT, + DOMAIN, + _LOGGER, +) from .gateway import DeconzGateway -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -SERVICE_DECONZ = 'configure' +SERVICE_DECONZ = "configure" -SERVICE_FIELD = 'field' -SERVICE_ENTITY = 'entity' -SERVICE_DATA = 'data' +SERVICE_FIELD = "field" +SERVICE_ENTITY = "entity" +SERVICE_DATA = "data" -SERVICE_SCHEMA = vol.All(vol.Schema({ - vol.Optional(SERVICE_ENTITY): cv.entity_id, - vol.Optional(SERVICE_FIELD): cv.matches_regex('/.*'), - vol.Required(SERVICE_DATA): dict, - vol.Optional(CONF_BRIDGEID): str -}), cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD)) +SERVICE_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(SERVICE_ENTITY): cv.entity_id, + vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"), + vol.Required(SERVICE_DATA): dict, + vol.Optional(CONF_BRIDGEID): str, + } + ), + cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD), +) -SERVICE_DEVICE_REFRESH = 'device_refresh' +SERVICE_DEVICE_REFRESH = "device_refresh" -SERVICE_DEVICE_REFRESCH_SCHEMA = vol.All(vol.Schema({ - vol.Optional(CONF_BRIDGEID): str -})) +SERVICE_DEVICE_REFRESCH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str})) async def async_setup(hass, config): @@ -48,10 +66,13 @@ async def async_setup(hass, config): """ if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: deconz_config = config[DOMAIN] - hass.async_create_task(hass.config_entries.flow.async_init( - DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, - data=deconz_config - )) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=deconz_config, + ) + ) return True @@ -92,7 +113,7 @@ async def async_setup_entry(hass, config_entry): See Dresden Elektroniks REST API documentation for details: http://dresden-elektronik.github.io/deconz-rest-doc/rest/ """ - field = call.data.get(SERVICE_FIELD, '') + field = call.data.get(SERVICE_FIELD, "") entity_id = call.data.get(SERVICE_ENTITY) data = call.data[SERVICE_DATA] @@ -104,13 +125,14 @@ async def async_setup_entry(hass, config_entry): try: field = gateway.deconz_ids[entity_id] + field except KeyError: - _LOGGER.error('Could not find the entity %s', entity_id) + _LOGGER.error("Could not find the entity %s", entity_id) return await gateway.api.async_put_state(field, data) hass.services.async_register( - DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA) + DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA + ) async def async_refresh_devices(call): """Refresh available devices from deCONZ.""" @@ -126,32 +148,47 @@ async def async_setup_entry(hass, config_entry): await gateway.api.async_load_parameters() gateway.async_add_device_callback( - 'group', [group - for group_id, group in gateway.api.groups.items() - if group_id not in groups] + "group", + [ + group + for group_id, group in gateway.api.groups.items() + if group_id not in groups + ], ) gateway.async_add_device_callback( - 'light', [light - for light_id, light in gateway.api.lights.items() - if light_id not in lights] + "light", + [ + light + for light_id, light in gateway.api.lights.items() + if light_id not in lights + ], ) gateway.async_add_device_callback( - 'scene', [scene - for scene_id, scene in gateway.api.scenes.items() - if scene_id not in scenes] + "scene", + [ + scene + for scene_id, scene in gateway.api.scenes.items() + if scene_id not in scenes + ], ) gateway.async_add_device_callback( - 'sensor', [sensor - for sensor_id, sensor in gateway.api.sensors.items() - if sensor_id not in sensors] + "sensor", + [ + sensor + for sensor_id, sensor in gateway.api.sensors.items() + if sensor_id not in sensors + ], ) hass.services.async_register( - DOMAIN, SERVICE_DEVICE_REFRESH, async_refresh_devices, - schema=SERVICE_DEVICE_REFRESCH_SCHEMA) + DOMAIN, + SERVICE_DEVICE_REFRESH, + async_refresh_devices, + schema=SERVICE_DEVICE_REFRESCH_SCHEMA, + ) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) return True @@ -183,10 +220,8 @@ async def async_populate_options(hass, config_entry): options = { CONF_MASTER_GATEWAY: master, - CONF_ALLOW_CLIP_SENSOR: config_entry.data.get( - CONF_ALLOW_CLIP_SENSOR, False), - CONF_ALLOW_DECONZ_GROUPS: config_entry.data.get( - CONF_ALLOW_DECONZ_GROUPS, True) + CONF_ALLOW_CLIP_SENSOR: config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, False), + CONF_ALLOW_DECONZ_GROUPS: config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True), } hass.config_entries.async_update_entry(config_entry, options=options) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 6fe8b4324b3..0b5d3173812 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -10,13 +10,12 @@ from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -ATTR_ORIENTATION = 'orientation' -ATTR_TILTANGLE = 'tiltangle' -ATTR_VIBRATIONSTRENGTH = 'vibrationstrength' +ATTR_ORIENTATION = "orientation" +ATTR_TILTANGLE = "tiltangle" +ATTR_VIBRATIONSTRENGTH = "vibrationstrength" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" pass @@ -32,16 +31,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if sensor.BINARY and \ - not (not gateway.allow_clip_sensor and - sensor.type.startswith('CLIP')): + if sensor.BINARY and not ( + not gateway.allow_clip_sensor and sensor.type.startswith("CLIP") + ): entities.append(DeconzBinarySensor(sensor, gateway)) async_add_entities(entities, True) - gateway.listeners.append(async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor)) + gateway.listeners.append( + async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor + ) + ) async_add_sensor(gateway.api.sensors.values()) @@ -53,7 +55,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): def async_update_callback(self, force_update=False): """Update the sensor's state.""" changed = set(self._device.changed_keys) - keys = {'battery', 'on', 'reachable', 'state'} + keys = {"battery", "on", "reachable", "state"} if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @@ -85,8 +87,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): if self._device.secondary_temperature is not None: attr[ATTR_TEMPERATURE] = self._device.secondary_temperature - if self._device.type in Presence.ZHATYPE and \ - self._device.dark is not None: + if self._device.type in Presence.ZHATYPE and self._device.dark is not None: attr[ATTR_DARK] = self._device.dark elif self._device.type in Vibration.ZHATYPE: diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 2f21b68ea09..d20833d5a82 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -3,9 +3,11 @@ from pydeconz.sensor import Thermostat from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE) -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, TEMP_CELSIUS) + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -16,8 +18,7 @@ from .gateway import get_gateway_from_config_entry SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF] -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" pass @@ -36,16 +37,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if sensor.type in Thermostat.ZHATYPE and \ - not (not gateway.allow_clip_sensor and - sensor.type.startswith('CLIP')): + if sensor.type in Thermostat.ZHATYPE and not ( + not gateway.allow_clip_sensor and sensor.type.startswith("CLIP") + ): entities.append(DeconzThermostat(sensor, gateway)) async_add_entities(entities, True) - gateway.listeners.append(async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SENSOR), async_add_climate)) + gateway.listeners.append( + async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_SENSOR), async_add_climate + ) + ) async_add_climate(gateway.api.sensors.values()) @@ -91,16 +95,16 @@ class DeconzThermostat(DeconzDevice, ClimateDevice): data = {} if ATTR_TEMPERATURE in kwargs: - data['heatsetpoint'] = kwargs[ATTR_TEMPERATURE] * 100 + data["heatsetpoint"] = kwargs[ATTR_TEMPERATURE] * 100 await self._device.async_set_config(data) async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_HEAT: - data = {'mode': 'auto'} + data = {"mode": "auto"} elif hvac_mode == HVAC_MODE_OFF: - data = {'mode': 'off'} + data = {"mode": "off"} await self._device.async_set_config(data) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 1e5eabd0a99..f4b8d3ebe02 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -5,8 +5,7 @@ import async_timeout import voluptuous as vol from pydeconz.errors import ResponseError, RequestError -from pydeconz.utils import ( - async_discovery, async_get_api_key, async_get_bridgeid) +from pydeconz.utils import async_discovery, async_get_api_key, async_get_bridgeid from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT @@ -15,16 +14,18 @@ from homeassistant.helpers import aiohttp_client from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN -DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de' -CONF_SERIAL = 'serial' -ATTR_UUID = 'udn' +DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" +CONF_SERIAL = "serial" +ATTR_UUID = "udn" @callback def configured_gateways(hass): """Return a set of all configured gateways.""" - return {entry.data[CONF_BRIDGEID]: entry for entry - in hass.config_entries.async_entries(DOMAIN)} + return { + entry.data[CONF_BRIDGEID]: entry + for entry in hass.config_entries.async_entries(DOMAIN) + } @callback @@ -89,18 +90,18 @@ class DeconzFlowHandler(config_entries.ConfigFlow): hosts.append(bridge[CONF_HOST]) return self.async_show_form( - step_id='init', - data_schema=vol.Schema({ - vol.Required(CONF_HOST): vol.In(hosts) - }) + step_id="init", + data_schema=vol.Schema({vol.Required(CONF_HOST): vol.In(hosts)}), ) return self.async_show_form( - step_id='init', - data_schema=vol.Schema({ - vol.Required(CONF_HOST): str, - vol.Required(CONF_PORT, default=DEFAULT_PORT): int, - }), + step_id="init", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + ), ) async def async_step_link(self, user_input=None): @@ -112,20 +113,16 @@ class DeconzFlowHandler(config_entries.ConfigFlow): try: with async_timeout.timeout(10): - api_key = await async_get_api_key( - session, **self.deconz_config) + api_key = await async_get_api_key(session, **self.deconz_config) except (ResponseError, RequestError, asyncio.TimeoutError): - errors['base'] = 'no_key' + errors["base"] = "no_key" else: self.deconz_config[CONF_API_KEY] = api_key return await self._create_entry() - return self.async_show_form( - step_id='link', - errors=errors, - ) + return self.async_show_form(step_id="link", errors=errors) async def _create_entry(self): """Create entry for gateway.""" @@ -134,16 +131,15 @@ class DeconzFlowHandler(config_entries.ConfigFlow): try: with async_timeout.timeout(10): - self.deconz_config[CONF_BRIDGEID] = \ - await async_get_bridgeid( - session, **self.deconz_config) + self.deconz_config[CONF_BRIDGEID] = await async_get_bridgeid( + session, **self.deconz_config + ) except asyncio.TimeoutError: - return self.async_abort(reason='no_bridges') + return self.async_abort(reason="no_bridges") return self.async_create_entry( - title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], - data=self.deconz_config + title="deCONZ-" + self.deconz_config[CONF_BRIDGEID], data=self.deconz_config ) async def _update_entry(self, entry, host): @@ -153,13 +149,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow): async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" - from homeassistant.components.ssdp import ( - ATTR_MANUFACTURERURL, ATTR_SERIAL) + from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL: - return self.async_abort(reason='not_deconz_bridge') + return self.async_abort(reason="not_deconz_bridge") - uuid = discovery_info[ATTR_UUID].replace('uuid:', '') + uuid = discovery_info[ATTR_UUID].replace("uuid:", "") gateways = { gateway.api.config.uuid: gateway for gateway in self.hass.data.get(DOMAIN, {}).values() @@ -168,12 +163,14 @@ class DeconzFlowHandler(config_entries.ConfigFlow): if uuid in gateways: entry = gateways[uuid].config_entry await self._update_entry(entry, discovery_info[CONF_HOST]) - return self.async_abort(reason='updated_instance') + return self.async_abort(reason="updated_instance") bridgeid = discovery_info[ATTR_SERIAL] - if any(bridgeid == flow['context'][CONF_BRIDGEID] - for flow in self._async_in_progress()): - return self.async_abort(reason='already_in_progress') + if any( + bridgeid == flow["context"][CONF_BRIDGEID] + for flow in self._async_in_progress() + ): + return self.async_abort(reason="already_in_progress") # pylint: disable=unsupported-assignment-operation self.context[CONF_BRIDGEID] = bridgeid @@ -215,7 +212,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow): if bridgeid in gateway_entries: entry = gateway_entries[bridgeid] await self._update_entry(entry, user_input[CONF_HOST]) - return self.async_abort(reason='updated_instance') + return self.async_abort(reason="updated_instance") self._hassio_discovery = user_input @@ -228,14 +225,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow): CONF_HOST: self._hassio_discovery[CONF_HOST], CONF_PORT: self._hassio_discovery[CONF_PORT], CONF_BRIDGEID: self._hassio_discovery[CONF_SERIAL], - CONF_API_KEY: self._hassio_discovery[CONF_API_KEY] + CONF_API_KEY: self._hassio_discovery[CONF_API_KEY], } return await self._create_entry() return self.async_show_form( - step_id='hassio_confirm', - description_placeholders={ - 'addon': self._hassio_discovery['addon'] - } + step_id="hassio_confirm", + description_placeholders={"addon": self._hassio_discovery["addon"]}, ) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index bf0f5884073..ba6172120ec 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -1,38 +1,45 @@ """Constants for the deCONZ component.""" import logging -_LOGGER = logging.getLogger('.') +_LOGGER = logging.getLogger(".") -DOMAIN = 'deconz' +DOMAIN = "deconz" DEFAULT_PORT = 80 DEFAULT_ALLOW_CLIP_SENSOR = False DEFAULT_ALLOW_DECONZ_GROUPS = False -CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' -CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' -CONF_BRIDGEID = 'bridgeid' -CONF_MASTER_GATEWAY = 'master' +CONF_ALLOW_CLIP_SENSOR = "allow_clip_sensor" +CONF_ALLOW_DECONZ_GROUPS = "allow_deconz_groups" +CONF_BRIDGEID = "bridgeid" +CONF_MASTER_GATEWAY = "master" -SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'cover', - 'light', 'scene', 'sensor', 'switch'] +SUPPORTED_PLATFORMS = [ + "binary_sensor", + "climate", + "cover", + "light", + "scene", + "sensor", + "switch", +] -NEW_GROUP = 'group' -NEW_LIGHT = 'light' -NEW_SCENE = 'scene' -NEW_SENSOR = 'sensor' +NEW_GROUP = "group" +NEW_LIGHT = "light" +NEW_SCENE = "scene" +NEW_SENSOR = "sensor" NEW_DEVICE = { - NEW_GROUP: 'deconz_new_group_{}', - NEW_LIGHT: 'deconz_new_light_{}', - NEW_SCENE: 'deconz_new_scene_{}', - NEW_SENSOR: 'deconz_new_sensor_{}' + NEW_GROUP: "deconz_new_group_{}", + NEW_LIGHT: "deconz_new_light_{}", + NEW_SCENE: "deconz_new_scene_{}", + NEW_SENSOR: "deconz_new_sensor_{}", } -ATTR_DARK = 'dark' -ATTR_OFFSET = 'offset' -ATTR_ON = 'on' -ATTR_VALVE = 'valve' +ATTR_DARK = "dark" +ATTR_OFFSET = "offset" +ATTR_ON = "on" +ATTR_VALVE = "valve" DAMPERS = ["Level controllable output"] WINDOW_COVERS = ["Window covering device"] diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index a89e7fdd595..caa46e10f99 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -1,7 +1,12 @@ """Support for deCONZ covers.""" from homeassistant.components.cover import ( - ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, - SUPPORT_SET_POSITION) + ATTR_POSITION, + CoverDevice, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_STOP, + SUPPORT_SET_POSITION, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -9,11 +14,10 @@ from .const import COVER_TYPES, DAMPERS, NEW_LIGHT, WINDOW_COVERS from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -ZIGBEE_SPEC = ['lumi.curtain'] +ZIGBEE_SPEC = ["lumi.curtain"] -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" pass @@ -41,8 +45,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) - gateway.listeners.append(async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_LIGHT), async_add_cover)) + gateway.listeners.append( + async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_LIGHT), async_add_cover + ) + ) async_add_cover(gateway.api.lights.values()) @@ -75,9 +82,9 @@ class DeconzCover(DeconzDevice, CoverDevice): def device_class(self): """Return the class of the cover.""" if self._device.type in DAMPERS: - return 'damper' + return "damper" if self._device.type in WINDOW_COVERS: - return 'window' + return "window" @property def supported_features(self): @@ -87,11 +94,11 @@ class DeconzCover(DeconzDevice, CoverDevice): async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] - data = {'on': False} + data = {"on": False} if position > 0: - data['on'] = True - data['bri'] = int(position / 100 * 255) + data["on"] = True + data["bri"] = int(position / 100 * 255) await self._device.async_set_state(data) @@ -107,7 +114,7 @@ class DeconzCover(DeconzDevice, CoverDevice): async def async_stop_cover(self, **kwargs): """Stop cover.""" - data = {'bri_inc': 0} + data = {"bri_inc": 0} await self._device.async_set_state(data) @@ -127,10 +134,10 @@ class DeconzCoverZigbeeSpec(DeconzCover): async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] - data = {'on': False} + data = {"on": False} if position < 100: - data['on'] = True - data['bri'] = 255 - int(position / 100 * 255) + data["on"] = True + data["bri"] = 255 - int(position / 100 * 255) await self._device.async_set_state(data) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 8745cb2141a..389ed11e437 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -21,8 +21,8 @@ class DeconzDevice(Entity): self._device.register_async_callback(self.async_update_callback) self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id self.unsub_dispatcher = async_dispatcher_connect( - self.hass, self.gateway.event_reachable, - self.async_update_callback) + self.hass, self.gateway.event_reachable, self.async_update_callback + ) async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" @@ -58,19 +58,18 @@ class DeconzDevice(Entity): @property def device_info(self): """Return a device description for device registry.""" - if (self._device.uniqueid is None or - self._device.uniqueid.count(':') != 7): + if self._device.uniqueid is None or self._device.uniqueid.count(":") != 7: return None - serial = self._device.uniqueid.split('-', 1)[0] + serial = self._device.uniqueid.split("-", 1)[0] bridgeid = self.gateway.api.config.bridgeid return { - 'connections': {(CONNECTION_ZIGBEE, serial)}, - 'identifiers': {(DECONZ_DOMAIN, serial)}, - 'manufacturer': self._device.manufacturer, - 'model': self._device.modelid, - 'name': self._device.name, - 'sw_version': self._device.swversion, - 'via_device': (DECONZ_DOMAIN, bridgeid), + "connections": {(CONNECTION_ZIGBEE, serial)}, + "identifiers": {(DECONZ_DOMAIN, serial)}, + "manufacturer": self._device.manufacturer, + "model": self._device.modelid, + "name": self._device.name, + "sw_version": self._device.swversion, + "via_device": (DECONZ_DOMAIN, bridgeid), } diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index f5d398fcd2f..8eca227f0cd 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -11,12 +11,22 @@ from homeassistant.core import EventOrigin, callback from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, async_dispatcher_send) + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.util import slugify from .const import ( - _LOGGER, CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, - CONF_MASTER_GATEWAY, DOMAIN, NEW_DEVICE, NEW_SENSOR, SUPPORTED_PLATFORMS) + _LOGGER, + CONF_ALLOW_CLIP_SENSOR, + CONF_ALLOW_DECONZ_GROUPS, + CONF_BRIDGEID, + CONF_MASTER_GATEWAY, + DOMAIN, + NEW_DEVICE, + NEW_SENSOR, + SUPPORTED_PLATFORMS, +) from .errors import AuthenticationRequired, CannotConnect @@ -62,16 +72,15 @@ class DeconzGateway: async def async_update_device_registry(self): """Update device registry.""" - device_registry = await \ - self.hass.helpers.device_registry.async_get_registry() + device_registry = await self.hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, connections={(CONNECTION_NETWORK_MAC, self.api.config.mac)}, identifiers={(DOMAIN, self.api.config.bridgeid)}, - manufacturer='Dresden Elektronik', + manufacturer="Dresden Elektronik", model=self.api.config.modelid, name=self.api.config.name, - sw_version=self.api.config.swversion + sw_version=self.api.config.swversion, ) async def async_setup(self): @@ -80,25 +89,31 @@ class DeconzGateway: try: self.api = await get_gateway( - hass, self.config_entry.data, self.async_add_device_callback, - self.async_connection_status_callback + hass, + self.config_entry.data, + self.async_add_device_callback, + self.async_connection_status_callback, ) except CannotConnect: raise ConfigEntryNotReady except Exception: # pylint: disable=broad-except - _LOGGER.error('Error connecting with deCONZ gateway') + _LOGGER.error("Error connecting with deCONZ gateway") return False for component in SUPPORTED_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup( - self.config_entry, component)) + self.config_entry, component + ) + ) - self.listeners.append(async_dispatcher_connect( - hass, self.async_event_new_device(NEW_SENSOR), - self.async_add_remote)) + self.listeners.append( + async_dispatcher_connect( + hass, self.async_event_new_device(NEW_SENSOR), self.async_add_remote + ) + ) self.async_add_remote(self.api.sensors.values()) @@ -123,7 +138,7 @@ class DeconzGateway: @property def event_reachable(self): """Gateway specific event to signal a change in connection status.""" - return 'deconz_reachable_{}'.format(self.bridgeid) + return "deconz_reachable_{}".format(self.bridgeid) @callback def async_connection_status_callback(self, available): @@ -142,15 +157,16 @@ class DeconzGateway: if not isinstance(device, list): device = [device] async_dispatcher_send( - self.hass, self.async_event_new_device(device_type), device) + self.hass, self.async_event_new_device(device_type), device + ) @callback def async_add_remote(self, sensors): """Set up remote from deCONZ.""" for sensor in sensors: - if sensor.type in Switch.ZHATYPE and \ - not (not self.allow_clip_sensor and - sensor.type.startswith('CLIP')): + if sensor.type in Switch.ZHATYPE and not ( + not self.allow_clip_sensor and sensor.type.startswith("CLIP") + ): self.events.append(DeconzEvent(self.hass, sensor)) @callback @@ -171,7 +187,8 @@ class DeconzGateway: for component in SUPPORTED_PLATFORMS: await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, component) + self.config_entry, component + ) for unsub_dispatcher in self.listeners: unsub_dispatcher() @@ -185,14 +202,19 @@ class DeconzGateway: return True -async def get_gateway(hass, config, async_add_device_callback, - async_connection_status_callback): +async def get_gateway( + hass, config, async_add_device_callback, async_connection_status_callback +): """Create a gateway object and verify configuration.""" session = aiohttp_client.async_get_clientsession(hass) - deconz = DeconzSession(hass.loop, session, **config, - async_add_device=async_add_device_callback, - connection_status=async_connection_status_callback) + deconz = DeconzSession( + hass.loop, + session, + **config, + async_add_device=async_add_device_callback, + connection_status=async_connection_status_callback, + ) try: with async_timeout.timeout(10): await deconz.async_load_parameters() @@ -203,8 +225,7 @@ async def get_gateway(hass, config, async_add_device_callback, raise AuthenticationRequired except (asyncio.TimeoutError, errors.RequestError): - _LOGGER.error( - "Error connecting to deCONZ gateway at %s", config[CONF_HOST]) + _LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST]) raise CannotConnect @@ -220,7 +241,7 @@ class DeconzEvent: self._hass = hass self._device = device self._device.register_async_callback(self.async_update_callback) - self._event = 'deconz_{}'.format(CONF_EVENT) + self._event = "deconz_{}".format(CONF_EVENT) self._id = slugify(self._device.name) _LOGGER.debug("deCONZ event created: %s", self._id) @@ -233,6 +254,6 @@ class DeconzEvent: @callback def async_update_callback(self, force_update=False): """Fire the event if reason is that state is updated.""" - if 'state' in self._device.changed_keys: + if "state" in self._device.changed_keys: data = {CONF_ID: self._id, CONF_EVENT: self._device.state} self._hass.bus.async_fire(self._event, data, EventOrigin.remote) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index a3328ca8042..b68aa6f0779 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -1,20 +1,38 @@ """Support for deCONZ lights.""" from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, - ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - SUPPORT_FLASH, SUPPORT_TRANSITION, Light) + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_FLASH, + ATTR_HS_COLOR, + ATTR_TRANSITION, + EFFECT_COLORLOOP, + FLASH_LONG, + FLASH_SHORT, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_FLASH, + SUPPORT_TRANSITION, + Light, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util -from .const import COVER_TYPES, NEW_GROUP, NEW_LIGHT, SWITCH_TYPES +from .const import ( + COVER_TYPES, + DOMAIN as DECONZ_DOMAIN, + NEW_GROUP, + NEW_LIGHT, + SWITCH_TYPES, +) from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" pass @@ -34,8 +52,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) - gateway.listeners.append(async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_LIGHT), async_add_light)) + gateway.listeners.append( + async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_LIGHT), async_add_light + ) + ) @callback def async_add_group(groups): @@ -44,12 +65,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for group in groups: if group.lights and gateway.allow_deconz_groups: - entities.append(DeconzLight(group, gateway)) + entities.append(DeconzGroup(group, gateway)) async_add_entities(entities, True) - gateway.listeners.append(async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_GROUP), async_add_group)) + gateway.listeners.append( + async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_GROUP), async_add_group + ) + ) async_add_light(gateway.api.lights.values()) async_add_group(gateway.api.groups.values()) @@ -59,7 +83,7 @@ class DeconzLight(DeconzDevice, Light): """Representation of a deCONZ light.""" def __init__(self, device, gateway): - """Set up light and add update callback to get data from websocket.""" + """Set up light.""" super().__init__(device, gateway) self._features = SUPPORT_BRIGHTNESS @@ -88,7 +112,7 @@ class DeconzLight(DeconzDevice, Light): @property def color_temp(self): """Return the CT color value.""" - if self._device.colormode != 'ct': + if self._device.colormode != "ct": return None return self._device.ct @@ -96,7 +120,7 @@ class DeconzLight(DeconzDevice, Light): @property def hs_color(self): """Return the hs color value.""" - if self._device.colormode in ('xy', 'hs') and self._device.xy: + if self._device.colormode in ("xy", "hs") and self._device.xy: return color_util.color_xy_to_hs(*self._device.xy) return None @@ -112,51 +136,51 @@ class DeconzLight(DeconzDevice, Light): async def async_turn_on(self, **kwargs): """Turn on light.""" - data = {'on': True} + data = {"on": True} if ATTR_COLOR_TEMP in kwargs: - data['ct'] = kwargs[ATTR_COLOR_TEMP] + data["ct"] = kwargs[ATTR_COLOR_TEMP] if ATTR_HS_COLOR in kwargs: - data['xy'] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) if ATTR_BRIGHTNESS in kwargs: - data['bri'] = kwargs[ATTR_BRIGHTNESS] + data["bri"] = kwargs[ATTR_BRIGHTNESS] if ATTR_TRANSITION in kwargs: - data['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) + data["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) if ATTR_FLASH in kwargs: if kwargs[ATTR_FLASH] == FLASH_SHORT: - data['alert'] = 'select' - del data['on'] + data["alert"] = "select" + del data["on"] elif kwargs[ATTR_FLASH] == FLASH_LONG: - data['alert'] = 'lselect' - del data['on'] + data["alert"] = "lselect" + del data["on"] if ATTR_EFFECT in kwargs: if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: - data['effect'] = 'colorloop' + data["effect"] = "colorloop" else: - data['effect'] = 'none' + data["effect"] = "none" await self._device.async_set_state(data) async def async_turn_off(self, **kwargs): """Turn off light.""" - data = {'on': False} + data = {"on": False} if ATTR_TRANSITION in kwargs: - data['bri'] = 0 - data['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) + data["bri"] = 0 + data["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) if ATTR_FLASH in kwargs: if kwargs[ATTR_FLASH] == FLASH_SHORT: - data['alert'] = 'select' - del data['on'] + data["alert"] = "select" + del data["on"] elif kwargs[ATTR_FLASH] == FLASH_LONG: - data['alert'] = 'lselect' - del data['on'] + data["alert"] = "lselect" + del data["on"] await self._device.async_set_state(data) @@ -164,9 +188,39 @@ class DeconzLight(DeconzDevice, Light): def device_state_attributes(self): """Return the device state attributes.""" attributes = {} - attributes['is_deconz_group'] = self._device.type == 'LightGroup' + attributes["is_deconz_group"] = self._device.type == "LightGroup" - if self._device.type == 'LightGroup': - attributes['all_on'] = self._device.all_on + if self._device.type == "LightGroup": + attributes["all_on"] = self._device.all_on return attributes + + +class DeconzGroup(DeconzLight): + """Representation of a deCONZ group.""" + + def __init__(self, device, gateway): + """Set up group and create an unique id.""" + super().__init__(device, gateway) + + self._unique_id = "{}-{}".format( + self.gateway.api.config.bridgeid, self._device.deconz_id + ) + + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return self._unique_id + + @property + def device_info(self): + """Return a device description for device registry.""" + bridgeid = self.gateway.api.config.bridgeid + + return { + "identifiers": {(DECONZ_DOMAIN, self.unique_id)}, + "manufacturer": "Dresden Elektronik", + "model": "deCONZ group", + "name": self._device.name, + "via_device": (DECONZ_DOMAIN, bridgeid), + } diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 1c7c0ac8aea..fe8f49d260a 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/deconz", "requirements": [ - "pydeconz==60" + "pydeconz==62" ], "ssdp": { "manufacturer": [ diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index c8cfa9674c5..ede60e3ef45 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -7,8 +7,7 @@ from .const import NEW_SCENE from .gateway import get_gateway_from_config_entry -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" pass @@ -27,8 +26,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) - gateway.listeners.append(async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SCENE), async_add_scene)) + gateway.listeners.append( + async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_SCENE), async_add_scene + ) + ) async_add_scene(gateway.api.scenes.values()) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index cb60998137f..dad3c25cc38 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,8 +1,12 @@ """Support for deCONZ sensors.""" -from pydeconz.sensor import LightLevel, Switch +from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) + ATTR_BATTERY_LEVEL, + ATTR_TEMPERATURE, + ATTR_VOLTAGE, + DEVICE_CLASS_BATTERY, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify @@ -11,13 +15,13 @@ from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -ATTR_CURRENT = 'current' -ATTR_DAYLIGHT = 'daylight' -ATTR_EVENT_ID = 'event_id' +ATTR_CURRENT = "current" +ATTR_POWER = "power" +ATTR_DAYLIGHT = "daylight" +ATTR_EVENT_ID = "event_id" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" pass @@ -33,9 +37,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if not sensor.BINARY and \ - not (not gateway.allow_clip_sensor and - sensor.type.startswith('CLIP')): + if not sensor.BINARY and not ( + not gateway.allow_clip_sensor and sensor.type.startswith("CLIP") + ): if sensor.type in Switch.ZHATYPE: if sensor.battery: @@ -46,8 +50,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) - gateway.listeners.append(async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor)) + gateway.listeners.append( + async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor + ) + ) async_add_sensor(gateway.api.sensors.values()) @@ -59,7 +66,7 @@ class DeconzSensor(DeconzDevice): def async_update_callback(self, force_update=False): """Update the sensor's state.""" changed = set(self._device.changed_keys) - keys = {'battery', 'on', 'reachable', 'state'} + keys = {"battery", "on", "reachable", "state"} if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @@ -96,17 +103,19 @@ class DeconzSensor(DeconzDevice): if self._device.secondary_temperature is not None: attr[ATTR_TEMPERATURE] = self._device.secondary_temperature - if self._device.type in LightLevel.ZHATYPE and \ - self._device.dark is not None: + if self._device.type in Consumption.ZHATYPE: + attr[ATTR_POWER] = self._device.power + + elif self._device.type in Daylight.ZHATYPE: + attr[ATTR_DAYLIGHT] = self._device.daylight + + elif self._device.type in LightLevel.ZHATYPE and self._device.dark is not None: attr[ATTR_DARK] = self._device.dark - if self.unit_of_measurement == 'W': + elif self._device.type in Power.ZHATYPE: attr[ATTR_CURRENT] = self._device.current attr[ATTR_VOLTAGE] = self._device.voltage - if self._device.SENSOR_CLASS == 'daylight': - attr[ATTR_DAYLIGHT] = self._device.daylight - return attr @@ -117,14 +126,14 @@ class DeconzBattery(DeconzDevice): """Register dispatcher callback for update of battery state.""" super().__init__(device, gateway) - self._name = '{} {}'.format(self._device.name, 'Battery Level') + self._name = "{} {}".format(self._device.name, "Battery Level") self._unit_of_measurement = "%" @callback def async_update_callback(self, force_update=False): """Update the battery's state, if needed.""" changed = set(self._device.changed_keys) - keys = {'battery', 'reachable'} + keys = {"battery", "reachable"} if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @@ -151,7 +160,5 @@ class DeconzBattery(DeconzDevice): @property def device_state_attributes(self): """Return the state attributes of the battery.""" - attr = { - ATTR_EVENT_ID: slugify(self._device.name), - } + attr = {ATTR_EVENT_ID: slugify(self._device.name)} return attr diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index dd06dba9583..7ce40789802 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -8,8 +8,7 @@ from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" pass @@ -36,8 +35,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) - gateway.listeners.append(async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_LIGHT), async_add_switch)) + gateway.listeners.append( + async_dispatcher_connect( + hass, gateway.async_event_new_device(NEW_LIGHT), async_add_switch + ) + ) async_add_switch(gateway.api.lights.values()) @@ -52,12 +54,12 @@ class DeconzPowerPlug(DeconzDevice, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn on switch.""" - data = {'on': True} + data = {"on": True} await self._device.async_set_state(data) async def async_turn_off(self, **kwargs): """Turn off switch.""" - data = {'on': False} + data = {"on": False} await self._device.async_set_state(data) @@ -67,14 +69,14 @@ class DeconzSiren(DeconzDevice, SwitchDevice): @property def is_on(self): """Return true if switch is on.""" - return self._device.alert == 'lselect' + return self._device.alert == "lselect" async def async_turn_on(self, **kwargs): """Turn on switch.""" - data = {'alert': 'lselect'} + data = {"alert": "lselect"} await self._device.async_set_state(data) async def async_turn_off(self, **kwargs): """Turn off switch.""" - data = {'alert': 'none'} + data = {"alert": "none"} await self._device.async_set_state(data) diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 2f6c050b79e..cad98f9d8a4 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -8,26 +8,29 @@ import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + Light, + PLATFORM_SCHEMA, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -SUPPORT_DECORA_LED = (SUPPORT_BRIGHTNESS) +SUPPORT_DECORA_LED = SUPPORT_BRIGHTNESS -DEVICE_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, -}) +DEVICE_SCHEMA = vol.Schema( + {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string} +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} +) def retry(method): """Retry bluetooth commands.""" + @wraps(method) def wrapper_retry(device, *args, **kwargs): """Try send command and retry on error.""" @@ -41,12 +44,14 @@ def retry(method): return None try: return method(device, *args, **kwargs) - except (decora.decoraException, AttributeError, - bluepy.btle.BTLEException): - _LOGGER.warning("Decora connect error for device %s. " - "Reconnecting...", device.name) + except (decora.decoraException, AttributeError, bluepy.btle.BTLEException): + _LOGGER.warning( + "Decora connect error for device %s. " "Reconnecting...", + device.name, + ) # pylint: disable=protected-access device._switch.connect() + return wrapper_retry @@ -55,9 +60,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): lights = [] for address, device_config in config[CONF_DEVICES].items(): device = {} - device['name'] = device_config[CONF_NAME] - device['key'] = device_config[CONF_API_KEY] - device['address'] = address + device["name"] = device_config[CONF_NAME] + device["key"] = device_config[CONF_API_KEY] + device["address"] = address light = DecoraLight(device) lights.append(light) @@ -70,10 +75,10 @@ class DecoraLight(Light): def __init__(self, device): """Initialize the light.""" # pylint: disable=no-member - decora = importlib.import_module('decora') + decora = importlib.import_module("decora") - self._name = device['name'] - self._address = device['address'] + self._name = device["name"] + self._address = device["address"] self._key = device["key"] self._switch = decora.decora(self._address, self._key) self._brightness = 0 diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 390af765b62..77f3a6387ed 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -5,23 +5,25 @@ import logging import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_TRANSITION, Light, - PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION) -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, - EVENT_HOMEASSISTANT_STOP) + ATTR_BRIGHTNESS, + ATTR_TRANSITION, + Light, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + SUPPORT_TRANSITION, +) +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) # Validation of the user's configuration -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} +) -NOTIFICATION_ID = 'leviton_notification' -NOTIFICATION_TITLE = 'myLeviton Decora Setup' +NOTIFICATION_ID = "leviton_notification" +NOTIFICATION_TITLE = "myLeviton Decora Setup" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -41,10 +43,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # If login failed, notify user. if success is None: - msg = 'Failed to log into myLeviton Services. Check credentials.' + msg = "Failed to log into myLeviton Services. Check credentials." _LOGGER.error(msg) hass.components.persistent_notification.create( - msg, title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) + msg, title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID + ) return False # Gather all the available devices... @@ -52,8 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): all_switches = [] for permission in perms: if permission.residentialAccountId is not None: - acct = ResidentialAccount( - session, permission.residentialAccountId) + acct = ResidentialAccount(session, permission.residentialAccountId) for residence in acct.get_residences(): for switch in residence.get_iot_switches(): all_switches.append(switch) @@ -64,7 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(DecoraWifiLight(sw) for sw in all_switches) except ValueError: - _LOGGER.error('Failed to communicate with myLeviton Service.') + _LOGGER.error("Failed to communicate with myLeviton Service.") # Listen for the stop event and log out. def logout(event): @@ -73,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if session is not None: Person.logout(session) except ValueError: - _LOGGER.error('Failed to log out of myLeviton Service.') + _LOGGER.error("Failed to log out of myLeviton Service.") hass.bus.listen(EVENT_HOMEASSISTANT_STOP, logout) @@ -105,39 +107,39 @@ class DecoraWifiLight(Light): @property def is_on(self): """Return true if switch is on.""" - return self._switch.power == 'ON' + return self._switch.power == "ON" def turn_on(self, **kwargs): """Instruct the switch to turn on & adjust brightness.""" - attribs = {'power': 'ON'} + attribs = {"power": "ON"} if ATTR_BRIGHTNESS in kwargs: - min_level = self._switch.data.get('minLevel', 0) - max_level = self._switch.data.get('maxLevel', 100) + min_level = self._switch.data.get("minLevel", 0) + max_level = self._switch.data.get("maxLevel", 100) brightness = int(kwargs[ATTR_BRIGHTNESS] * max_level / 255) brightness = max(brightness, min_level) - attribs['brightness'] = brightness + attribs["brightness"] = brightness if ATTR_TRANSITION in kwargs: transition = int(kwargs[ATTR_TRANSITION]) - attribs['fadeOnTime'] = attribs['fadeOffTime'] = transition + attribs["fadeOnTime"] = attribs["fadeOffTime"] = transition try: self._switch.update_attributes(attribs) except ValueError: - _LOGGER.error('Failed to turn on myLeviton switch.') + _LOGGER.error("Failed to turn on myLeviton switch.") def turn_off(self, **kwargs): """Instruct the switch to turn off.""" - attribs = {'power': 'OFF'} + attribs = {"power": "OFF"} try: self._switch.update_attributes(attribs) except ValueError: - _LOGGER.error('Failed to turn off myLeviton switch.') + _LOGGER.error("Failed to turn off myLeviton switch.") def update(self): """Fetch new state data for this switch.""" try: self._switch.refresh() except ValueError: - _LOGGER.error('Failed to update myLeviton switch data.') + _LOGGER.error("Failed to update myLeviton switch data.") diff --git a/homeassistant/components/default_config/__init__.py b/homeassistant/components/default_config/__init__.py index 23add299b2f..506904a500a 100644 --- a/homeassistant/components/default_config/__init__.py +++ b/homeassistant/components/default_config/__init__.py @@ -6,7 +6,7 @@ except ImportError: from homeassistant.setup import async_setup_component -DOMAIN = 'default_config' +DOMAIN = "default_config" async def async_setup(hass, config): @@ -14,4 +14,4 @@ async def async_setup(hass, config): if av is None: return True - return await async_setup_component(hass, 'stream', config) + return await async_setup_component(hass, "stream", config) diff --git a/homeassistant/components/delijn/__init__.py b/homeassistant/components/delijn/__init__.py new file mode 100644 index 00000000000..cdec126589b --- /dev/null +++ b/homeassistant/components/delijn/__init__.py @@ -0,0 +1 @@ +"""The delijn component.""" diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json new file mode 100644 index 00000000000..90e1a4e3b15 --- /dev/null +++ b/homeassistant/components/delijn/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "delijn", + "name": "De Lijn", + "documentation": "https://www.home-assistant.io/components/delijn", + "dependencies": [], + "codeowners": ["@bollewolle"], + "requirements": ["pydelijn==0.5.1"] +} diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py new file mode 100644 index 00000000000..2cd238d0787 --- /dev/null +++ b/homeassistant/components/delijn/sensor.py @@ -0,0 +1,117 @@ +"""Support for De Lijn (Flemish public transport) information.""" +import logging + +from pydelijn.api import Passages +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Data provided by data.delijn.be" + +CONF_NEXT_DEPARTURE = "next_departure" +CONF_STOP_ID = "stop_id" +CONF_API_KEY = "api_key" +CONF_NUMBER_OF_DEPARTURES = "number_of_departures" + +DEFAULT_NAME = "De Lijn" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_NEXT_DEPARTURE): [ + { + vol.Required(CONF_STOP_ID): cv.string, + vol.Optional(CONF_NUMBER_OF_DEPARTURES, default=5): cv.positive_int, + } + ], + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Create the sensor.""" + api_key = config[CONF_API_KEY] + name = DEFAULT_NAME + + session = async_get_clientsession(hass) + + sensors = [] + for nextpassage in config[CONF_NEXT_DEPARTURE]: + stop_id = nextpassage[CONF_STOP_ID] + number_of_departures = nextpassage[CONF_NUMBER_OF_DEPARTURES] + line = Passages( + hass.loop, stop_id, number_of_departures, api_key, session, True + ) + await line.get_passages() + if line.passages is None: + _LOGGER.warning("No data received from De Lijn") + return + sensors.append(DeLijnPublicTransportSensor(line, name)) + + async_add_entities(sensors, True) + + +class DeLijnPublicTransportSensor(Entity): + """Representation of a Ruter sensor.""" + + def __init__(self, line, name): + """Initialize the sensor.""" + self.line = line + self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._name = name + self._state = None + + async def async_update(self): + """Get the latest data from the De Lijn API.""" + await self.line.get_passages() + if self.line.passages is None: + _LOGGER.warning("No data received from De Lijn") + return + try: + first = self.line.passages[0] + if first["due_at_realtime"] is not None: + first_passage = first["due_at_realtime"] + else: + first_passage = first["due_at_schedule"] + self._state = first_passage + self._name = first["stopname"] + self._attributes["stopname"] = first["stopname"] + self._attributes["line_number_public"] = first["line_number_public"] + self._attributes["line_transport_type"] = first["line_transport_type"] + self._attributes["final_destination"] = first["final_destination"] + self._attributes["due_at_schedule"] = first["due_at_schedule"] + self._attributes["due_at_realtime"] = first["due_at_realtime"] + self._attributes["next_passages"] = self.line.passages + except (KeyError, IndexError) as error: + _LOGGER.debug("Error getting data from De Lijn: %s", error) + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Return the icon of the sensor.""" + return "mdi:bus" + + @property + def device_state_attributes(self): + """Return attributes for the sensor.""" + return self._attributes diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 1002ae51077..8b42b6175ce 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -6,33 +6,42 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PORT, - CONF_MONITORED_VARIABLES, STATE_IDLE) + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + CONF_NAME, + CONF_PORT, + CONF_MONITORED_VARIABLES, + STATE_IDLE, +) from homeassistant.helpers.entity import Entity from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) _THROTTLED_REFRESH = None -DEFAULT_NAME = 'Deluge' +DEFAULT_NAME = "Deluge" DEFAULT_PORT = 58846 DHT_UPLOAD = 1000 DHT_DOWNLOAD = 1000 SENSOR_TYPES = { - 'current_status': ['Status', None], - 'download_speed': ['Down Speed', 'kB/s'], - 'upload_speed': ['Up Speed', 'kB/s'], + "current_status": ["Status", None], + "download_speed": ["Down Speed", "kB/s"], + "upload_speed": ["Up Speed", "kB/s"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -75,7 +84,7 @@ class DelugeSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self.client_name, self._name) + return "{} {}".format(self.client_name, self._name) @property def state(self): @@ -95,40 +104,45 @@ class DelugeSensor(Entity): def update(self): """Get the latest data from Deluge and updates the state.""" from deluge_client import FailedToReconnectException + try: - self.data = self.client.call('core.get_session_status', - ['upload_rate', 'download_rate', - 'dht_upload_rate', - 'dht_download_rate']) + self.data = self.client.call( + "core.get_session_status", + [ + "upload_rate", + "download_rate", + "dht_upload_rate", + "dht_download_rate", + ], + ) self._available = True except FailedToReconnectException: _LOGGER.error("Connection to Deluge Daemon Lost") self._available = False return - upload = self.data[b'upload_rate'] - self.data[b'dht_upload_rate'] - download = self.data[b'download_rate'] - self.data[ - b'dht_download_rate'] + upload = self.data[b"upload_rate"] - self.data[b"dht_upload_rate"] + download = self.data[b"download_rate"] - self.data[b"dht_download_rate"] - if self.type == 'current_status': + if self.type == "current_status": if self.data: if upload > 0 and download > 0: - self._state = 'Up/Down' + self._state = "Up/Down" elif upload > 0 and download == 0: - self._state = 'Seeding' + self._state = "Seeding" elif upload == 0 and download > 0: - self._state = 'Downloading' + self._state = "Downloading" else: self._state = STATE_IDLE else: self._state = None if self.data: - if self.type == 'download_speed': + if self.type == "download_speed": kb_spd = float(download) kb_spd = kb_spd / 1024 self._state = round(kb_spd, 2 if kb_spd < 0.1 else 1) - elif self.type == 'upload_speed': + elif self.type == "upload_speed": kb_spd = float(upload) kb_spd = kb_spd / 1024 self._state = round(kb_spd, 2 if kb_spd < 0.1 else 1) diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index d72ce9a5308..981ef129b47 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -6,23 +6,31 @@ import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, CONF_PASSWORD, CONF_USERNAME, STATE_OFF, - STATE_ON) + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_PASSWORD, + CONF_USERNAME, + STATE_OFF, + STATE_ON, +) from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Deluge Switch' +DEFAULT_NAME = "Deluge Switch" DEFAULT_PORT = 58846 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -77,20 +85,22 @@ class DelugeSwitch(ToggleEntity): def turn_on(self, **kwargs): """Turn the device on.""" - torrent_ids = self.deluge_client.call('core.get_session_state') - self.deluge_client.call('core.resume_torrent', torrent_ids) + torrent_ids = self.deluge_client.call("core.get_session_state") + self.deluge_client.call("core.resume_torrent", torrent_ids) def turn_off(self, **kwargs): """Turn the device off.""" - torrent_ids = self.deluge_client.call('core.get_session_state') - self.deluge_client.call('core.pause_torrent', torrent_ids) + torrent_ids = self.deluge_client.call("core.get_session_state") + self.deluge_client.call("core.pause_torrent", torrent_ids) def update(self): """Get the latest data from deluge and updates the state.""" from deluge_client import FailedToReconnectException + try: - torrent_list = self.deluge_client.call('core.get_torrents_status', - {}, ['paused']) + torrent_list = self.deluge_client.call( + "core.get_torrents_status", {}, ["paused"] + ) self._available = True except FailedToReconnectException: _LOGGER.error("Connection to Deluge Daemon Lost") diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index c61673afda6..f8b61167ef7 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -7,27 +7,27 @@ from homeassistant import bootstrap import homeassistant.core as ha from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START -DOMAIN = 'demo' +DOMAIN = "demo" _LOGGER = logging.getLogger(__name__) COMPONENTS_WITH_DEMO_PLATFORM = [ - 'air_quality', - 'alarm_control_panel', - 'binary_sensor', - 'calendar', - 'camera', - 'climate', - 'cover', - 'device_tracker', - 'fan', - 'image_processing', - 'light', - 'lock', - 'media_player', - 'notify', - 'sensor', - 'switch', - 'tts', - 'mailbox', + "air_quality", + "alarm_control_panel", + "binary_sensor", + "calendar", + "camera", + "climate", + "cover", + "device_tracker", + "fan", + "image_processing", + "light", + "lock", + "media_player", + "notify", + "sensor", + "switch", + "tts", + "mailbox", ] @@ -41,9 +41,9 @@ async def async_setup(hass, config): # Set up demo platforms for component in COMPONENTS_WITH_DEMO_PLATFORM: - hass.async_create_task(hass.helpers.discovery.async_load_platform( - component, DOMAIN, {}, config, - )) + hass.async_create_task( + hass.helpers.discovery.async_load_platform(component, DOMAIN, {}, config) + ) # Set up sun if not hass.config.latitude: @@ -52,45 +52,77 @@ async def async_setup(hass, config): if not hass.config.longitude: hass.config.longitude = 117.22743 - tasks = [ - bootstrap.async_setup_component(hass, 'sun', config) - ] + tasks = [bootstrap.async_setup_component(hass, "sun", config)] # Set up input select - tasks.append(bootstrap.async_setup_component( - hass, 'input_select', - {'input_select': - {'living_room_preset': {'options': ['Visitors', - 'Visitors with kids', - 'Home Alone']}, - 'who_cooks': {'icon': 'mdi:panda', - 'initial': 'Anne Therese', - 'name': 'Cook today', - 'options': ['Paulus', 'Anne Therese']}}})) + tasks.append( + bootstrap.async_setup_component( + hass, + "input_select", + { + "input_select": { + "living_room_preset": { + "options": ["Visitors", "Visitors with kids", "Home Alone"] + }, + "who_cooks": { + "icon": "mdi:panda", + "initial": "Anne Therese", + "name": "Cook today", + "options": ["Paulus", "Anne Therese"], + }, + } + }, + ) + ) # Set up input boolean - tasks.append(bootstrap.async_setup_component( - hass, 'input_boolean', - {'input_boolean': {'notify': { - 'icon': 'mdi:car', - 'initial': False, - 'name': 'Notify Anne Therese is home'}}})) + tasks.append( + bootstrap.async_setup_component( + hass, + "input_boolean", + { + "input_boolean": { + "notify": { + "icon": "mdi:car", + "initial": False, + "name": "Notify Anne Therese is home", + } + } + }, + ) + ) # Set up input boolean - tasks.append(bootstrap.async_setup_component( - hass, 'input_number', - {'input_number': { - 'noise_allowance': {'icon': 'mdi:bell-ring', - 'min': 0, - 'max': 10, - 'name': 'Allowed Noise', - 'unit_of_measurement': 'dB'}}})) + tasks.append( + bootstrap.async_setup_component( + hass, + "input_number", + { + "input_number": { + "noise_allowance": { + "icon": "mdi:bell-ring", + "min": 0, + "max": 10, + "name": "Allowed Noise", + "unit_of_measurement": "dB", + } + } + }, + ) + ) # Set up weblink - tasks.append(bootstrap.async_setup_component( - hass, 'weblink', - {'weblink': {'entities': [{'name': 'Router', - 'url': 'http://192.168.1.1'}]}})) + tasks.append( + bootstrap.async_setup_component( + hass, + "weblink", + { + "weblink": { + "entities": [{"name": "Router", "url": "http://192.168.1.1"}] + } + }, + ) + ) results = await asyncio.gather(*tasks) @@ -99,8 +131,8 @@ async def async_setup(hass, config): # Set up example persistent notification hass.components.persistent_notification.async_create( - 'This is an example of a persistent notification.', - title='Example Notification') + "This is an example of a persistent notification.", title="Example Notification" + ) # Set up configurator configurator_ids = [] @@ -113,20 +145,23 @@ async def async_setup(hass, config): # First time it is called, pretend it failed. if len(configurator_ids) == 1: configurator.notify_errors( - configurator_ids[0], - "Failed to register, please try again.") + configurator_ids[0], "Failed to register, please try again." + ) configurator_ids.append(0) else: configurator.request_done(configurator_ids[0]) request_id = configurator.async_request_config( - "Philips Hue", hue_configuration_callback, - description=("Press the button on the bridge to register Philips " - "Hue with Home Assistant."), + "Philips Hue", + hue_configuration_callback, + description=( + "Press the button on the bridge to register Philips " + "Hue with Home Assistant." + ), description_image="/static/images/config_philips_hue.jpg", - fields=[{'id': 'username', 'name': 'Username'}], - submit_caption="I have pressed the button" + fields=[{"id": "username", "name": "Username"}], + submit_caption="I have pressed the button", ) configurator_ids.append(request_id) @@ -141,55 +176,75 @@ async def async_setup(hass, config): async def finish_setup(hass, config): """Finish set up once demo platforms are set up.""" - lights = sorted(hass.states.async_entity_ids('light')) - switches = sorted(hass.states.async_entity_ids('switch')) + lights = sorted(hass.states.async_entity_ids("light")) + switches = sorted(hass.states.async_entity_ids("switch")) # Set up history graph await bootstrap.async_setup_component( - hass, 'history_graph', - {'history_graph': {'switches': { - 'name': 'Recent Switches', - 'entities': switches, - 'hours_to_show': 1, - 'refresh': 60 - }}} + hass, + "history_graph", + { + "history_graph": { + "switches": { + "name": "Recent Switches", + "entities": switches, + "hours_to_show": 1, + "refresh": 60, + } + } + }, ) # Set up scripts await bootstrap.async_setup_component( - hass, 'script', - {'script': { - 'demo': { - 'alias': 'Toggle {}'.format(lights[0].split('.')[1]), - 'sequence': [{ - 'service': 'light.turn_off', - 'data': {ATTR_ENTITY_ID: lights[0]} - }, { - 'delay': {'seconds': 5} - }, { - 'service': 'light.turn_on', - 'data': {ATTR_ENTITY_ID: lights[0]} - }, { - 'delay': {'seconds': 5} - }, { - 'service': 'light.turn_off', - 'data': {ATTR_ENTITY_ID: lights[0]} - }] - }}}) + hass, + "script", + { + "script": { + "demo": { + "alias": "Toggle {}".format(lights[0].split(".")[1]), + "sequence": [ + { + "service": "light.turn_off", + "data": {ATTR_ENTITY_ID: lights[0]}, + }, + {"delay": {"seconds": 5}}, + { + "service": "light.turn_on", + "data": {ATTR_ENTITY_ID: lights[0]}, + }, + {"delay": {"seconds": 5}}, + { + "service": "light.turn_off", + "data": {ATTR_ENTITY_ID: lights[0]}, + }, + ], + } + } + }, + ) # Set up scenes await bootstrap.async_setup_component( - hass, 'scene', - {'scene': [ - {'name': 'Romantic lights', - 'entities': { - lights[0]: True, - lights[1]: {'state': 'on', 'xy_color': [0.33, 0.66], - 'brightness': 200}, - }}, - {'name': 'Switch on and off', - 'entities': { - switches[0]: True, - switches[1]: False, - }}, - ]}) + hass, + "scene", + { + "scene": [ + { + "name": "Romantic lights", + "entities": { + lights[0]: True, + lights[1]: { + "state": "on", + "xy_color": [0.33, 0.66], + "brightness": 200, + }, + }, + }, + { + "name": "Switch on and off", + "entities": {switches[0]: True, switches[1]: False}, + }, + ] + }, + ) diff --git a/homeassistant/components/demo/air_quality.py b/homeassistant/components/demo/air_quality.py index 77e5c0b2b1a..0b41d9c87e9 100644 --- a/homeassistant/components/demo/air_quality.py +++ b/homeassistant/components/demo/air_quality.py @@ -4,10 +4,9 @@ from homeassistant.components.air_quality import AirQualityEntity def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Air Quality.""" - add_entities([ - DemoAirQuality('Home', 14, 23, 100), - DemoAirQuality('Office', 4, 16, None) - ]) + add_entities( + [DemoAirQuality("Home", 14, 23, 100), DemoAirQuality("Office", 4, 16, None)] + ) class DemoAirQuality(AirQualityEntity): @@ -23,7 +22,7 @@ class DemoAirQuality(AirQualityEntity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format('Demo Air Quality', self._name) + return "{} {}".format("Demo Air Quality", self._name) @property def should_poll(self): @@ -48,4 +47,4 @@ class DemoAirQuality(AirQualityEntity): @property def attribution(self): """Return the attribution.""" - return 'Powered by Home Assistant' + return "Powered by Home Assistant" diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index a960848eee7..378ba3b18dd 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -2,43 +2,58 @@ import datetime from homeassistant.components.manual.alarm_control_panel import ManualAlarm from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME, - CONF_PENDING_TIME, CONF_TRIGGER_TIME) + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + CONF_DELAY_TIME, + CONF_PENDING_TIME, + CONF_TRIGGER_TIME, +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo alarm control panel platform.""" - async_add_entities([ - ManualAlarm(hass, 'Alarm', '1234', None, True, False, { - STATE_ALARM_ARMED_AWAY: { - CONF_DELAY_TIME: datetime.timedelta(seconds=0), - CONF_PENDING_TIME: datetime.timedelta(seconds=5), - CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), - }, - STATE_ALARM_ARMED_HOME: { - CONF_DELAY_TIME: datetime.timedelta(seconds=0), - CONF_PENDING_TIME: datetime.timedelta(seconds=5), - CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), - }, - STATE_ALARM_ARMED_NIGHT: { - CONF_DELAY_TIME: datetime.timedelta(seconds=0), - CONF_PENDING_TIME: datetime.timedelta(seconds=5), - CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), - }, - STATE_ALARM_DISARMED: { - CONF_DELAY_TIME: datetime.timedelta(seconds=0), - CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), - }, - STATE_ALARM_ARMED_CUSTOM_BYPASS: { - CONF_DELAY_TIME: datetime.timedelta(seconds=0), - CONF_PENDING_TIME: datetime.timedelta(seconds=5), - CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), - }, - STATE_ALARM_TRIGGERED: { - CONF_PENDING_TIME: datetime.timedelta(seconds=5), - }, - }), - ]) + async_add_entities( + [ + ManualAlarm( + hass, + "Alarm", + "1234", + None, + True, + False, + { + STATE_ALARM_ARMED_AWAY: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_ARMED_HOME: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_ARMED_NIGHT: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_DISARMED: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_ARMED_CUSTOM_BYPASS: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_TRIGGERED: { + CONF_PENDING_TIME: datetime.timedelta(seconds=5) + }, + }, + ) + ] + ) diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index 437497e4fac..96c2b66fa5b 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -4,10 +4,12 @@ from homeassistant.components.binary_sensor import BinarySensorDevice def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo binary sensor platform.""" - add_entities([ - DemoBinarySensor('Basement Floor Wet', False, 'moisture'), - DemoBinarySensor('Movement Backyard', True, 'motion'), - ]) + add_entities( + [ + DemoBinarySensor("Basement Floor Wet", False, "moisture"), + DemoBinarySensor("Movement Backyard", True, "motion"), + ] + ) class DemoBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 9a685357b10..4ae836466f0 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -10,10 +10,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo Calendar platform.""" calendar_data_future = DemoGoogleCalendarDataFuture() calendar_data_current = DemoGoogleCalendarDataCurrent() - add_entities([ - DemoGoogleCalendar(hass, calendar_data_future, 'Calendar 1'), - DemoGoogleCalendar(hass, calendar_data_current, 'Calendar 2'), - ]) + add_entities( + [ + DemoGoogleCalendar(hass, calendar_data_future, "Calendar 1"), + DemoGoogleCalendar(hass, calendar_data_current, "Calendar 2"), + ] + ) class DemoGoogleCalendarData: @@ -24,9 +26,9 @@ class DemoGoogleCalendarData: async def async_get_events(self, hass, start_date, end_date): """Get all events in a specific time frame.""" event = copy.copy(self.event) - event['title'] = event['summary'] - event['start'] = get_date(event['start']).isoformat() - event['end'] = get_date(event['end']).isoformat() + event["title"] = event["summary"] + event["start"] = get_date(event["start"]).isoformat() + event["end"] = get_date(event["end"]).isoformat() return [event] @@ -35,17 +37,15 @@ class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData): def __init__(self): """Set the event to a future event.""" - one_hour_from_now = dt_util.now() \ - + dt_util.dt.timedelta(minutes=30) + one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30) self.event = { - 'start': { - 'dateTime': one_hour_from_now.isoformat() + "start": {"dateTime": one_hour_from_now.isoformat()}, + "end": { + "dateTime": ( + one_hour_from_now + dt_util.dt.timedelta(minutes=60) + ).isoformat() }, - 'end': { - 'dateTime': (one_hour_from_now + dt_util.dt. - timedelta(minutes=60)).isoformat() - }, - 'summary': 'Future Event', + "summary": "Future Event", } @@ -54,17 +54,15 @@ class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData): def __init__(self): """Set the event data.""" - middle_of_event = dt_util.now() \ - - dt_util.dt.timedelta(minutes=30) + middle_of_event = dt_util.now() - dt_util.dt.timedelta(minutes=30) self.event = { - 'start': { - 'dateTime': middle_of_event.isoformat() + "start": {"dateTime": middle_of_event.isoformat()}, + "end": { + "dateTime": ( + middle_of_event + dt_util.dt.timedelta(minutes=60) + ).isoformat() }, - 'end': { - 'dateTime': (middle_of_event + dt_util.dt. - timedelta(minutes=60)).isoformat() - }, - 'summary': 'Current Event', + "summary": "Current Event", } diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 95c7df58200..7ac5fc17c69 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -7,12 +7,9 @@ from homeassistant.components.camera import SUPPORT_ON_OFF, Camera _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo camera platform.""" - async_add_entities([ - DemoCamera('Demo camera') - ]) + async_add_entities([DemoCamera("Demo camera")]) class DemoCamera(Camera): @@ -31,10 +28,10 @@ class DemoCamera(Camera): self._images_index = (self._images_index + 1) % 4 image_path = os.path.join( - os.path.dirname(__file__), - 'demo_{}.jpg'.format(self._images_index)) - _LOGGER.debug('Loading camera_image: %s', image_path) - with open(image_path, 'rb') as file: + os.path.dirname(__file__), "demo_{}.jpg".format(self._images_index) + ) + _LOGGER.debug("Loading camera_image: %s", image_path) + with open(image_path, "rb") as file: return file.read() @property diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index 4e8654ac16b..f5117b7986c 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -1,12 +1,24 @@ """Demo platform that offers a fake climate device.""" from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, HVAC_MODES, SUPPORT_AUX_HEAT, - SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, - SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, HVAC_MODE_AUTO) + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + HVAC_MODES, + SUPPORT_AUX_HEAT, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, + HVAC_MODE_AUTO, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT SUPPORT_FLAGS = 0 @@ -14,108 +26,105 @@ SUPPORT_FLAGS = 0 def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo climate devices.""" - add_entities([ - DemoClimate( - name='HeatPump', - target_temperature=68, - unit_of_measurement=TEMP_FAHRENHEIT, - preset=None, - current_temperature=77, - fan_mode=None, - target_humidity=None, - current_humidity=None, - swing_mode=None, - hvac_mode=HVAC_MODE_HEAT, - hvac_action=CURRENT_HVAC_HEAT, - aux=None, - target_temp_high=None, - target_temp_low=None, - hvac_modes=[HVAC_MODE_HEAT, HVAC_MODE_OFF] - ), - DemoClimate( - name='Hvac', - target_temperature=21, - unit_of_measurement=TEMP_CELSIUS, - preset=None, - current_temperature=22, - fan_mode='On High', - target_humidity=67, - current_humidity=54, - swing_mode='Off', - hvac_mode=HVAC_MODE_COOL, - hvac_action=CURRENT_HVAC_COOL, - aux=False, - target_temp_high=None, - target_temp_low=None, - hvac_modes=[mode for mode in HVAC_MODES - if mode != HVAC_MODE_HEAT_COOL] - ), - DemoClimate( - name='Ecobee', - target_temperature=None, - unit_of_measurement=TEMP_CELSIUS, - preset='home', - preset_modes=['home', 'eco'], - current_temperature=23, - fan_mode='Auto Low', - target_humidity=None, - current_humidity=None, - swing_mode='Auto', - hvac_mode=HVAC_MODE_HEAT_COOL, - hvac_action=None, - aux=None, - target_temp_high=24, - target_temp_low=21, - hvac_modes=[HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, - HVAC_MODE_HEAT]) - ]) + add_entities( + [ + DemoClimate( + name="HeatPump", + target_temperature=68, + unit_of_measurement=TEMP_FAHRENHEIT, + preset=None, + current_temperature=77, + fan_mode=None, + target_humidity=None, + current_humidity=None, + swing_mode=None, + hvac_mode=HVAC_MODE_HEAT, + hvac_action=CURRENT_HVAC_HEAT, + aux=None, + target_temp_high=None, + target_temp_low=None, + hvac_modes=[HVAC_MODE_HEAT, HVAC_MODE_OFF], + ), + DemoClimate( + name="Hvac", + target_temperature=21, + unit_of_measurement=TEMP_CELSIUS, + preset=None, + current_temperature=22, + fan_mode="On High", + target_humidity=67, + current_humidity=54, + swing_mode="Off", + hvac_mode=HVAC_MODE_COOL, + hvac_action=CURRENT_HVAC_COOL, + aux=False, + target_temp_high=None, + target_temp_low=None, + hvac_modes=[mode for mode in HVAC_MODES if mode != HVAC_MODE_HEAT_COOL], + ), + DemoClimate( + name="Ecobee", + target_temperature=None, + unit_of_measurement=TEMP_CELSIUS, + preset="home", + preset_modes=["home", "eco"], + current_temperature=23, + fan_mode="Auto Low", + target_humidity=None, + current_humidity=None, + swing_mode="Auto", + hvac_mode=HVAC_MODE_HEAT_COOL, + hvac_action=None, + aux=None, + target_temp_high=24, + target_temp_low=21, + hvac_modes=[HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT], + ), + ] + ) class DemoClimate(ClimateDevice): """Representation of a demo climate device.""" def __init__( - self, - name, - target_temperature, - unit_of_measurement, - preset, - current_temperature, - fan_mode, - target_humidity, - current_humidity, - swing_mode, - hvac_mode, - hvac_action, - aux, - target_temp_high, - target_temp_low, - hvac_modes, - preset_modes=None, + self, + name, + target_temperature, + unit_of_measurement, + preset, + current_temperature, + fan_mode, + target_humidity, + current_humidity, + swing_mode, + hvac_mode, + hvac_action, + aux, + target_temp_high, + target_temp_low, + hvac_modes, + preset_modes=None, ): """Initialize the climate device.""" self._name = name self._support_flags = SUPPORT_FLAGS if target_temperature is not None: - self._support_flags = \ - self._support_flags | SUPPORT_TARGET_TEMPERATURE + self._support_flags = self._support_flags | SUPPORT_TARGET_TEMPERATURE if preset is not None: self._support_flags = self._support_flags | SUPPORT_PRESET_MODE if fan_mode is not None: self._support_flags = self._support_flags | SUPPORT_FAN_MODE if target_humidity is not None: - self._support_flags = \ - self._support_flags | SUPPORT_TARGET_HUMIDITY + self._support_flags = self._support_flags | SUPPORT_TARGET_HUMIDITY if swing_mode is not None: self._support_flags = self._support_flags | SUPPORT_SWING_MODE if hvac_action is not None: self._support_flags = self._support_flags if aux is not None: self._support_flags = self._support_flags | SUPPORT_AUX_HEAT - if (HVAC_MODE_HEAT_COOL in hvac_modes or - HVAC_MODE_AUTO in hvac_modes): - self._support_flags = \ - self._support_flags | SUPPORT_TARGET_TEMPERATURE_RANGE + if HVAC_MODE_HEAT_COOL in hvac_modes or HVAC_MODE_AUTO in hvac_modes: + self._support_flags = self._support_flags | SUPPORT_TARGET_TEMPERATURE_RANGE self._target_temperature = target_temperature self._target_humidity = target_humidity self._unit_of_measurement = unit_of_measurement @@ -128,9 +137,9 @@ class DemoClimate(ClimateDevice): self._hvac_mode = hvac_mode self._aux = aux self._current_swing_mode = swing_mode - self._fan_modes = ['On Low', 'On High', 'Auto Low', 'Auto High', 'Off'] + self._fan_modes = ["On Low", "On High", "Auto Low", "Auto High", "Off"] self._hvac_modes = hvac_modes - self._swing_modes = ['Auto', '1', '2', '3', 'Off'] + self._swing_modes = ["Auto", "1", "2", "3", "Off"] self._target_temperature_high = target_temp_high self._target_temperature_low = target_temp_low @@ -238,8 +247,10 @@ class DemoClimate(ClimateDevice): """Set new target temperatures.""" if kwargs.get(ATTR_TEMPERATURE) is not None: self._target_temperature = kwargs.get(ATTR_TEMPERATURE) - if kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None and \ - kwargs.get(ATTR_TARGET_TEMP_LOW) is not None: + if ( + kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None + and kwargs.get(ATTR_TARGET_TEMP_LOW) is not None + ): self._target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) self._target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) self.async_write_ha_state() diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index aa2931a987a..180312eefa3 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -2,26 +2,43 @@ from homeassistant.helpers.event import track_utc_time_change from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_OPEN, - CoverDevice) + ATTR_POSITION, + ATTR_TILT_POSITION, + SUPPORT_CLOSE, + SUPPORT_OPEN, + CoverDevice, +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo covers.""" - add_entities([ - DemoCover(hass, 'Kitchen Window'), - DemoCover(hass, 'Hall Window', 10), - DemoCover(hass, 'Living Room Window', 70, 50), - DemoCover(hass, 'Garage Door', device_class='garage', - supported_features=(SUPPORT_OPEN | SUPPORT_CLOSE)), - ]) + add_entities( + [ + DemoCover(hass, "Kitchen Window"), + DemoCover(hass, "Hall Window", 10), + DemoCover(hass, "Living Room Window", 70, 50), + DemoCover( + hass, + "Garage Door", + device_class="garage", + supported_features=(SUPPORT_OPEN | SUPPORT_CLOSE), + ), + ] + ) class DemoCover(CoverDevice): """Representation of a demo cover.""" - def __init__(self, hass, name, position=None, tilt_position=None, - device_class=None, supported_features=None): + def __init__( + self, + hass, + name, + position=None, + tilt_position=None, + device_class=None, + supported_features=None, + ): """Initialize the cover.""" self.hass = hass self._name = name @@ -178,7 +195,8 @@ class DemoCover(CoverDevice): """Listen for changes in cover.""" if self._unsub_listener_cover is None: self._unsub_listener_cover = track_utc_time_change( - self.hass, self._time_changed_cover) + self.hass, self._time_changed_cover + ) def _time_changed_cover(self, now): """Track time changes.""" @@ -198,7 +216,8 @@ class DemoCover(CoverDevice): """Listen for changes in cover tilt.""" if self._unsub_listener_cover_tilt is None: self._unsub_listener_cover_tilt = track_utc_time_change( - self.hass, self._time_changed_cover_tilt) + self.hass, self._time_changed_cover_tilt + ) def _time_changed_cover_tilt(self, now): """Track time changes.""" diff --git a/homeassistant/components/demo/device_tracker.py b/homeassistant/components/demo/device_tracker.py index ff038d7009e..fba8095efd6 100644 --- a/homeassistant/components/demo/device_tracker.py +++ b/homeassistant/components/demo/device_tracker.py @@ -6,6 +6,7 @@ from homeassistant.components.device_tracker import DOMAIN def setup_scanner(hass, config, see, discovery_info=None): """Set up the demo tracker.""" + def offset(): """Return random offset.""" return (random.randrange(500, 2000)) / 2e5 * random.choice((-1, 1)) @@ -15,27 +16,26 @@ def setup_scanner(hass, config, see, discovery_info=None): see( dev_id=dev_id, host_name=name, - gps=(hass.config.latitude + offset(), - hass.config.longitude + offset()), + gps=(hass.config.latitude + offset(), hass.config.longitude + offset()), gps_accuracy=random.randrange(50, 150), - battery=random.randrange(10, 90) + battery=random.randrange(10, 90), ) def observe(call=None): """Observe three entities.""" - random_see('demo_paulus', 'Paulus') - random_see('demo_anne_therese', 'Anne Therese') + random_see("demo_paulus", "Paulus") + random_see("demo_anne_therese", "Anne Therese") observe() see( - dev_id='demo_home_boy', - host_name='Home Boy', + dev_id="demo_home_boy", + host_name="Home Boy", gps=[hass.config.latitude - 0.00002, hass.config.longitude + 0.00002], gps_accuracy=20, - battery=53 + battery=53, ) - hass.services.register(DOMAIN, 'demo', observe) + hass.services.register(DOMAIN, "demo", observe) return True diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 4710bbecfe1..cdeed5dbfec 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -2,8 +2,14 @@ from homeassistant.const import STATE_OFF from homeassistant.components.fan import ( - SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, - SUPPORT_SET_SPEED, FanEntity) + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SUPPORT_DIRECTION, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, + FanEntity, +) FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION LIMITED_SUPPORT = SUPPORT_SET_SPEED @@ -11,10 +17,12 @@ LIMITED_SUPPORT = SUPPORT_SET_SPEED def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the demo fan platform.""" - add_entities_callback([ - DemoFan(hass, "Living Room Fan", FULL_SUPPORT), - DemoFan(hass, "Ceiling Fan", LIMITED_SUPPORT), - ]) + add_entities_callback( + [ + DemoFan(hass, "Living Room Fan", FULL_SUPPORT), + DemoFan(hass, "Ceiling Fan", LIMITED_SUPPORT), + ] + ) class DemoFan(FanEntity): diff --git a/homeassistant/components/demo/geo_location.py b/homeassistant/components/demo/geo_location.py index 6b91faac92f..6a7aa7ddce1 100644 --- a/homeassistant/components/demo/geo_location.py +++ b/homeassistant/components/demo/geo_location.py @@ -12,17 +12,30 @@ from homeassistant.components.geo_location import GeolocationEvent _LOGGER = logging.getLogger(__name__) AVG_KM_PER_DEGREE = 111.0 -DEFAULT_UNIT_OF_MEASUREMENT = 'km' +DEFAULT_UNIT_OF_MEASUREMENT = "km" DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) MAX_RADIUS_IN_KM = 50 NUMBER_OF_DEMO_DEVICES = 5 -EVENT_NAMES = ["Bushfire", "Hazard Reduction", "Grass Fire", "Burn off", - "Structure Fire", "Fire Alarm", "Thunderstorm", "Tornado", - "Cyclone", "Waterspout", "Dust Storm", "Blizzard", "Ice Storm", - "Earthquake", "Tsunami"] +EVENT_NAMES = [ + "Bushfire", + "Hazard Reduction", + "Grass Fire", + "Burn off", + "Structure Fire", + "Fire Alarm", + "Thunderstorm", + "Tornado", + "Cyclone", + "Waterspout", + "Dust Storm", + "Blizzard", + "Ice Storm", + "Earthquake", + "Tsunami", +] -SOURCE = 'demo' +SOURCE = "demo" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -47,24 +60,26 @@ class DemoManager: home_longitude = self._hass.config.longitude # Approx. 111km per degree (north-south). - radius_in_degrees = random.random() * MAX_RADIUS_IN_KM / \ - AVG_KM_PER_DEGREE + radius_in_degrees = random.random() * MAX_RADIUS_IN_KM / AVG_KM_PER_DEGREE radius_in_km = radius_in_degrees * AVG_KM_PER_DEGREE angle = random.random() * 2 * pi # Compute coordinates based on radius and angle. Adjust longitude value # based on HA's latitude. latitude = home_latitude + radius_in_degrees * sin(angle) - longitude = home_longitude + radius_in_degrees * cos(angle) / \ - cos(radians(home_latitude)) + longitude = home_longitude + radius_in_degrees * cos(angle) / cos( + radians(home_latitude) + ) event_name = random.choice(EVENT_NAMES) - return DemoGeolocationEvent(event_name, radius_in_km, latitude, - longitude, DEFAULT_UNIT_OF_MEASUREMENT) + return DemoGeolocationEvent( + event_name, radius_in_km, latitude, longitude, DEFAULT_UNIT_OF_MEASUREMENT + ) def _init_regular_updates(self): """Schedule regular updates based on configured time interval.""" - track_time_interval(self._hass, lambda now: self._update(), - DEFAULT_UPDATE_INTERVAL) + track_time_interval( + self._hass, lambda now: self._update(), DEFAULT_UPDATE_INTERVAL + ) def _update(self, count=1): """Remove events and add new random events.""" @@ -89,8 +104,7 @@ class DemoManager: class DemoGeolocationEvent(GeolocationEvent): """This represents a demo geolocation event.""" - def __init__(self, name, distance, latitude, longitude, - unit_of_measurement): + def __init__(self, name, distance, latitude, longitude, unit_of_measurement): """Initialize entity with data provided.""" self._name = name self._distance = distance diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index acb97e4ebd6..348045e47b2 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -1,19 +1,24 @@ """Support for the demo image processing.""" from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, ATTR_CONFIDENCE, ATTR_NAME, ATTR_AGE, - ATTR_GENDER - ) + ImageProcessingFaceEntity, + ATTR_CONFIDENCE, + ATTR_NAME, + ATTR_AGE, + ATTR_GENDER, +) from homeassistant.components.openalpr_local.image_processing import ( - ImageProcessingAlprEntity) + ImageProcessingAlprEntity, +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the demo image processing platform.""" - add_entities([ - DemoImageProcessingAlpr('camera.demo_camera', "Demo Alpr"), - DemoImageProcessingFace( - 'camera.demo_camera', "Demo Face") - ]) + add_entities( + [ + DemoImageProcessingAlpr("camera.demo_camera", "Demo Alpr"), + DemoImageProcessingFace("camera.demo_camera", "Demo Face"), + ] + ) class DemoImageProcessingAlpr(ImageProcessingAlprEntity): @@ -44,10 +49,10 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): def process_image(self, image): """Process image.""" demo_data = { - 'AC3829': 98.3, - 'BE392034': 95.5, - 'CD02394': 93.4, - 'DF923043': 90.8 + "AC3829": 98.3, + "BE392034": 95.5, + "CD02394": 93.4, + "DF923043": 90.8, } self.process_plates(demo_data, 1) @@ -83,19 +88,12 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity): demo_data = [ { ATTR_CONFIDENCE: 98.34, - ATTR_NAME: 'Hans', + ATTR_NAME: "Hans", ATTR_AGE: 16.0, - ATTR_GENDER: 'male', - }, - { - ATTR_NAME: 'Helena', - ATTR_AGE: 28.0, - ATTR_GENDER: 'female', - }, - { - ATTR_CONFIDENCE: 62.53, - ATTR_NAME: 'Luna', + ATTR_GENDER: "male", }, + {ATTR_NAME: "Helena", ATTR_AGE: 28.0, ATTR_GENDER: "female"}, + {ATTR_CONFIDENCE: 62.53, ATTR_NAME: "Luna"}, ] self.process_faces(demo_data, 4) diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 285866c6eb8..f8b1b511511 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -2,41 +2,68 @@ import random from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, - ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_EFFECT, SUPPORT_WHITE_VALUE, Light) + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_WHITE_VALUE, + Light, +) -LIGHT_COLORS = [ - (56, 86), - (345, 75), -] +LIGHT_COLORS = [(56, 86), (345, 75)] -LIGHT_EFFECT_LIST = ['rainbow', 'none'] +LIGHT_EFFECT_LIST = ["rainbow", "none"] LIGHT_TEMPS = [240, 380] -SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | - SUPPORT_COLOR | SUPPORT_WHITE_VALUE) +SUPPORT_DEMO = ( + SUPPORT_BRIGHTNESS + | SUPPORT_COLOR_TEMP + | SUPPORT_EFFECT + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE +) def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the demo light platform.""" - add_entities_callback([ - DemoLight(1, "Bed Light", False, True, effect_list=LIGHT_EFFECT_LIST, - effect=LIGHT_EFFECT_LIST[0]), - DemoLight(2, "Ceiling Lights", True, True, - LIGHT_COLORS[0], LIGHT_TEMPS[1]), - DemoLight(3, "Kitchen Lights", True, True, - LIGHT_COLORS[1], LIGHT_TEMPS[0]) - ]) + add_entities_callback( + [ + DemoLight( + 1, + "Bed Light", + False, + True, + effect_list=LIGHT_EFFECT_LIST, + effect=LIGHT_EFFECT_LIST[0], + ), + DemoLight(2, "Ceiling Lights", True, True, LIGHT_COLORS[0], LIGHT_TEMPS[1]), + DemoLight(3, "Kitchen Lights", True, True, LIGHT_COLORS[1], LIGHT_TEMPS[0]), + ] + ) class DemoLight(Light): """Representation of a demo light.""" - def __init__(self, unique_id, name, state, available=False, hs_color=None, - ct=None, brightness=180, white=200, effect_list=None, - effect=None): + def __init__( + self, + unique_id, + name, + state, + available=False, + hs_color=None, + ct=None, + brightness=180, + white=200, + effect_list=None, + effect=None, + ): """Initialize the light.""" self._unique_id = unique_id self._name = name diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index cd15a434138..d8fb244b9bc 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -6,11 +6,13 @@ from homeassistant.components.lock import SUPPORT_OPEN, LockDevice def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo lock platform.""" - add_entities([ - DemoLock('Front Door', STATE_LOCKED), - DemoLock('Kitchen Door', STATE_UNLOCKED), - DemoLock('Openable Lock', STATE_LOCKED, True) - ]) + add_entities( + [ + DemoLock("Front Door", STATE_LOCKED), + DemoLock("Kitchen Door", STATE_UNLOCKED), + DemoLock("Openable Lock", STATE_LOCKED, True), + ] + ) class DemoLock(LockDevice): diff --git a/homeassistant/components/demo/mailbox.py b/homeassistant/components/demo/mailbox.py index fcffc44eefb..77030623c9d 100644 --- a/homeassistant/components/demo/mailbox.py +++ b/homeassistant/components/demo/mailbox.py @@ -3,13 +3,12 @@ from hashlib import sha1 import logging import os -from homeassistant.components.mailbox import ( - CONTENT_TYPE_MPEG, Mailbox, StreamError) +from homeassistant.components.mailbox import CONTENT_TYPE_MPEG, Mailbox, StreamError from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) -MAILBOX_NAME = 'DemoMailbox' +MAILBOX_NAME = "DemoMailbox" async def async_get_handler(hass, config, discovery_info=None): @@ -26,19 +25,17 @@ class DemoMailbox(Mailbox): self._messages = {} txt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " for idx in range(0, 10): - msgtime = int(dt.as_timestamp( - dt.utcnow()) - 3600 * 24 * (10 - idx)) - msgtxt = "Message {}. {}".format( - idx + 1, txt * (1 + idx * (idx % 2))) - msgsha = sha1(msgtxt.encode('utf-8')).hexdigest() + msgtime = int(dt.as_timestamp(dt.utcnow()) - 3600 * 24 * (10 - idx)) + msgtxt = "Message {}. {}".format(idx + 1, txt * (1 + idx * (idx % 2))) + msgsha = sha1(msgtxt.encode("utf-8")).hexdigest() msg = { - 'info': { - 'origtime': msgtime, - 'callerid': 'John Doe <212-555-1212>', - 'duration': '10', + "info": { + "origtime": msgtime, + "callerid": "John Doe <212-555-1212>", + "duration": "10", }, - 'text': msgtxt, - 'sha': msgsha, + "text": msgtxt, + "sha": msgsha, } self._messages[msgsha] = msg @@ -62,16 +59,17 @@ class DemoMailbox(Mailbox): if msgid not in self._messages: raise StreamError("Message not found") - audio_path = os.path.join( - os.path.dirname(__file__), 'tts.mp3') - with open(audio_path, 'rb') as file: + audio_path = os.path.join(os.path.dirname(__file__), "tts.mp3") + with open(audio_path, "rb") as file: return file.read() async def async_get_messages(self): """Return a list of the current messages.""" - return sorted(self._messages.values(), - key=lambda item: item['info']['origtime'], - reverse=True) + return sorted( + self._messages.values(), + key=lambda item: item["info"]["origtime"], + reverse=True, + ) def async_delete(self, msgid): """Delete the specified messages.""" diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index e293632b71e..e3f69be3020 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -1,50 +1,92 @@ """Demo implementation of the media player.""" from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, - SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, - SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP) + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_TVSHOW, + SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_SELECT_SOUND_MODE, + SUPPORT_SELECT_SOURCE, + SUPPORT_SHUFFLE_SET, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING import homeassistant.util.dt as dt_util def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the media player demo platform.""" - add_entities([ - DemoYoutubePlayer( - 'Living Room', 'eyU3bRy2x44', - '♥♥ The Best Fireplace Video (3 hours)', 300), - DemoYoutubePlayer( - 'Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours', 360000), - DemoMusicPlayer(), DemoTVShowPlayer(), - ]) + add_entities( + [ + DemoYoutubePlayer( + "Living Room", + "eyU3bRy2x44", + "♥♥ The Best Fireplace Video (3 hours)", + 300, + ), + DemoYoutubePlayer( + "Bedroom", "kxopViU98Xo", "Epic sax guy 10 hours", 360000 + ), + DemoMusicPlayer(), + DemoTVShowPlayer(), + ] + ) -YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/hqdefault.jpg' -SOUND_MODE_LIST = ['Dummy Music', 'Dummy Movie'] -DEFAULT_SOUND_MODE = 'Dummy Music' +YOUTUBE_COVER_URL_FORMAT = "https://img.youtube.com/vi/{}/hqdefault.jpg" +SOUND_MODE_LIST = ["Dummy Music", "Dummy Movie"] +DEFAULT_SOUND_MODE = "Dummy Music" -YOUTUBE_PLAYER_SUPPORT = \ - SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | \ - SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOUND_MODE | SUPPORT_SELECT_SOURCE | \ - SUPPORT_SEEK +YOUTUBE_PLAYER_SUPPORT = ( + SUPPORT_PAUSE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PLAY_MEDIA + | SUPPORT_PLAY + | SUPPORT_SHUFFLE_SET + | SUPPORT_SELECT_SOUND_MODE + | SUPPORT_SELECT_SOURCE + | SUPPORT_SEEK +) -MUSIC_PLAYER_SUPPORT = \ - SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST | \ - SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | SUPPORT_VOLUME_STEP | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_SELECT_SOUND_MODE +MUSIC_PLAYER_SUPPORT = ( + SUPPORT_PAUSE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_CLEAR_PLAYLIST + | SUPPORT_PLAY + | SUPPORT_SHUFFLE_SET + | SUPPORT_VOLUME_STEP + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_SELECT_SOUND_MODE +) -NETFLIX_PLAYER_SUPPORT = \ - SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_SELECT_SOUND_MODE +NETFLIX_PLAYER_SUPPORT = ( + SUPPORT_PAUSE + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE + | SUPPORT_PLAY + | SUPPORT_SHUFFLE_SET + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_SELECT_SOUND_MODE +) class AbstractDemoPlayer(MediaPlayerDevice): @@ -170,7 +212,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): self.youtube_id = youtube_id self._media_title = media_title self._duration = duration - self._progress = int(duration * .15) + self._progress = int(duration * 0.15) self._progress_updated_at = dt_util.utcnow() @property @@ -217,8 +259,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): position = self._progress if self._player_state == STATE_PLAYING: - position += (dt_util.utcnow() - - self._progress_updated_at).total_seconds() + position += (dt_util.utcnow() - self._progress_updated_at).total_seconds() return position @@ -249,36 +290,37 @@ class DemoMusicPlayer(AbstractDemoPlayer): # We only implement the methods that we support tracks = [ - ('Technohead', 'I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)'), - ('Paul Elstak', 'Luv U More'), - ('Dune', 'Hardcore Vibes'), - ('Nakatomi', 'Children Of The Night'), - ('Party Animals', - 'Have You Ever Been Mellow? (Flamman & Abraxas Radio Mix)'), - ('Rob G.*', 'Ecstasy, You Got What I Need'), - ('Lipstick', "I'm A Raver"), - ('4 Tune Fairytales', 'My Little Fantasy (Radio Edit)'), - ('Prophet', "The Big Boys Don't Cry"), - ('Lovechild', 'All Out Of Love (DJ Weirdo & Sim Remix)'), - ('Stingray & Sonic Driver', 'Cold As Ice (El Bruto Remix)'), - ('Highlander', 'Hold Me Now (Bass-D & King Matthew Remix)'), - ('Juggernaut', 'Ruffneck Rules Da Artcore Scene (12" Edit)'), - ('Diss Reaction', 'Jiiieehaaaa '), - ('Flamman And Abraxas', 'Good To Go (Radio Mix)'), - ('Critical Mass', 'Dancing Together'), - ('Charly Lownoise & Mental Theo', - 'Ultimate Sex Track (Bass-D & King Matthew Remix)'), + ("Technohead", "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)"), + ("Paul Elstak", "Luv U More"), + ("Dune", "Hardcore Vibes"), + ("Nakatomi", "Children Of The Night"), + ("Party Animals", "Have You Ever Been Mellow? (Flamman & Abraxas Radio Mix)"), + ("Rob G.*", "Ecstasy, You Got What I Need"), + ("Lipstick", "I'm A Raver"), + ("4 Tune Fairytales", "My Little Fantasy (Radio Edit)"), + ("Prophet", "The Big Boys Don't Cry"), + ("Lovechild", "All Out Of Love (DJ Weirdo & Sim Remix)"), + ("Stingray & Sonic Driver", "Cold As Ice (El Bruto Remix)"), + ("Highlander", "Hold Me Now (Bass-D & King Matthew Remix)"), + ("Juggernaut", 'Ruffneck Rules Da Artcore Scene (12" Edit)'), + ("Diss Reaction", "Jiiieehaaaa "), + ("Flamman And Abraxas", "Good To Go (Radio Mix)"), + ("Critical Mass", "Dancing Together"), + ( + "Charly Lownoise & Mental Theo", + "Ultimate Sex Track (Bass-D & King Matthew Remix)", + ), ] def __init__(self): """Initialize the demo device.""" - super().__init__('Walkman') + super().__init__("Walkman") self._cur_track = 0 @property def media_content_id(self): """Return the content ID of current playing media.""" - return 'bounzz-1' + return "bounzz-1" @property def media_content_type(self): @@ -293,8 +335,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): @property def media_image_url(self): """Return the image url of current playing media.""" - return 'https://graph.facebook.com/v2.5/107771475912710/' \ - 'picture?type=large' + return "https://graph.facebook.com/v2.5/107771475912710/" "picture?type=large" @property def media_title(self): @@ -348,15 +389,15 @@ class DemoTVShowPlayer(AbstractDemoPlayer): def __init__(self): """Initialize the demo device.""" - super().__init__('Lounge room') + super().__init__("Lounge room") self._cur_episode = 1 self._episode_count = 13 - self._source = 'dvd' + self._source = "dvd" @property def media_content_id(self): """Return the content ID of current playing media.""" - return 'house-of-cards-1' + return "house-of-cards-1" @property def media_content_type(self): @@ -371,17 +412,17 @@ class DemoTVShowPlayer(AbstractDemoPlayer): @property def media_image_url(self): """Return the image url of current playing media.""" - return 'https://graph.facebook.com/v2.5/HouseofCards/picture?width=400' + return "https://graph.facebook.com/v2.5/HouseofCards/picture?width=400" @property def media_title(self): """Return the title of current playing media.""" - return 'Chapter {}'.format(self._cur_episode) + return "Chapter {}".format(self._cur_episode) @property def media_series_title(self): """Return the series title of current playing media (TV Show only).""" - return 'House of Cards' + return "House of Cards" @property def media_season(self): diff --git a/homeassistant/components/demo/notify.py b/homeassistant/components/demo/notify.py index 92aaea6882d..f390c042ce4 100644 --- a/homeassistant/components/demo/notify.py +++ b/homeassistant/components/demo/notify.py @@ -23,5 +23,5 @@ class DemoNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - kwargs['message'] = message + kwargs["message"] = message self.hass.bus.fire(EVENT_NOTIFY, kwargs) diff --git a/homeassistant/components/demo/remote.py b/homeassistant/components/demo/remote.py index b28330fdc67..4a363781e2e 100644 --- a/homeassistant/components/demo/remote.py +++ b/homeassistant/components/demo/remote.py @@ -5,10 +5,12 @@ from homeassistant.const import DEVICE_DEFAULT_NAME def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the demo remotes.""" - add_entities_callback([ - DemoRemote('Remote One', False, None), - DemoRemote('Remote Two', True, 'mdi:remote'), - ]) + add_entities_callback( + [ + DemoRemote("Remote One", False, None), + DemoRemote("Remote Two", True, "mdi:remote"), + ] + ) class DemoRemote(RemoteDevice): @@ -45,7 +47,7 @@ class DemoRemote(RemoteDevice): def device_state_attributes(self): """Return device state attributes.""" if self._last_command_sent is not None: - return {'last_command_sent': self._last_command_sent} + return {"last_command_sent": self._last_command_sent} def turn_on(self, **kwargs): """Turn the remote on.""" diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index ea35c729517..78bd88b42cf 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -1,24 +1,29 @@ """Demo platform that has a couple of fake sensors.""" from homeassistant.const import ( - ATTR_BATTERY_LEVEL, TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE) + ATTR_BATTERY_LEVEL, + TEMP_CELSIUS, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, +) from homeassistant.helpers.entity import Entity def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo sensors.""" - add_entities([ - DemoSensor('Outside Temperature', 15.6, DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS, 12), - DemoSensor('Outside Humidity', 54, DEVICE_CLASS_HUMIDITY, '%', None), - ]) + add_entities( + [ + DemoSensor( + "Outside Temperature", 15.6, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 12 + ), + DemoSensor("Outside Humidity", 54, DEVICE_CLASS_HUMIDITY, "%", None), + ] + ) class DemoSensor(Entity): """Representation of a Demo sensor.""" - def __init__(self, name, state, device_class, - unit_of_measurement, battery): + def __init__(self, name, state, device_class, unit_of_measurement, battery): """Initialize the sensor.""" self._name = name self._state = state @@ -55,6 +60,4 @@ class DemoSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" if self._battery: - return { - ATTR_BATTERY_LEVEL: self._battery, - } + return {ATTR_BATTERY_LEVEL: self._battery} diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index e7a3b1741a2..65860867ed6 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -5,10 +5,12 @@ from homeassistant.const import DEVICE_DEFAULT_NAME def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the demo switches.""" - add_entities_callback([ - DemoSwitch('Decorative Lights', True, None, True), - DemoSwitch('AC', False, 'mdi:air-conditioner', False) - ]) + add_entities_callback( + [ + DemoSwitch("Decorative Lights", True, None, True), + DemoSwitch("AC", False, "mdi:air-conditioner", False), + ] + ) class DemoSwitch(SwitchDevice): diff --git a/homeassistant/components/demo/tts.py b/homeassistant/components/demo/tts.py index bf18bc1630f..ae083e50454 100644 --- a/homeassistant/components/demo/tts.py +++ b/homeassistant/components/demo/tts.py @@ -5,15 +5,13 @@ import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider -SUPPORT_LANGUAGES = [ - 'en', 'de' -] +SUPPORT_LANGUAGES = ["en", "de"] -DEFAULT_LANG = 'en' +DEFAULT_LANG = "en" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)} +) def get_engine(hass, config): @@ -27,7 +25,7 @@ class DemoProvider(Provider): def __init__(self, lang): """Initialize demo provider.""" self._lang = lang - self.name = 'Demo' + self.name = "Demo" @property def default_language(self): @@ -42,15 +40,15 @@ class DemoProvider(Provider): @property def supported_options(self): """Return list of supported options like voice, emotionen.""" - return ['voice', 'age'] + return ["voice", "age"] def get_tts_audio(self, message, language, options=None): """Load TTS from demo.""" - filename = os.path.join(os.path.dirname(__file__), 'tts.mp3') + filename = os.path.join(os.path.dirname(__file__), "tts.mp3") try: - with open(filename, 'rb') as voice: + with open(filename, "rb") as voice: data = voice.read() except OSError: return (None, None) - return ('mp3', data) + return ("mp3", data) diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index dfb9c4e943e..ffd3e768b11 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -2,51 +2,92 @@ import logging from homeassistant.components.vacuum import ( - ATTR_CLEANED_AREA, STATE_CLEANING, STATE_DOCKED, STATE_IDLE, STATE_PAUSED, - STATE_RETURNING, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, - SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, - SUPPORT_START, SUPPORT_STATE, SUPPORT_STATUS, SUPPORT_STOP, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, StateVacuumDevice, VacuumDevice) + ATTR_CLEANED_AREA, + STATE_CLEANING, + STATE_DOCKED, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, + SUPPORT_BATTERY, + SUPPORT_CLEAN_SPOT, + SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, + SUPPORT_PAUSE, + SUPPORT_RETURN_HOME, + SUPPORT_SEND_COMMAND, + SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STATUS, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + StateVacuumDevice, + VacuumDevice, +) _LOGGER = logging.getLogger(__name__) SUPPORT_MINIMAL_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF -SUPPORT_BASIC_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_STATUS | SUPPORT_BATTERY +SUPPORT_BASIC_SERVICES = ( + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_STATUS | SUPPORT_BATTERY +) -SUPPORT_MOST_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_STOP | \ - SUPPORT_RETURN_HOME | SUPPORT_STATUS | SUPPORT_BATTERY +SUPPORT_MOST_SERVICES = ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_STOP + | SUPPORT_RETURN_HOME + | SUPPORT_STATUS + | SUPPORT_BATTERY +) -SUPPORT_ALL_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \ - SUPPORT_STOP | SUPPORT_RETURN_HOME | \ - SUPPORT_FAN_SPEED | SUPPORT_SEND_COMMAND | \ - SUPPORT_LOCATE | SUPPORT_STATUS | SUPPORT_BATTERY | \ - SUPPORT_CLEAN_SPOT +SUPPORT_ALL_SERVICES = ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PAUSE + | SUPPORT_STOP + | SUPPORT_RETURN_HOME + | SUPPORT_FAN_SPEED + | SUPPORT_SEND_COMMAND + | SUPPORT_LOCATE + | SUPPORT_STATUS + | SUPPORT_BATTERY + | SUPPORT_CLEAN_SPOT +) -SUPPORT_STATE_SERVICES = SUPPORT_STATE | SUPPORT_PAUSE | SUPPORT_STOP | \ - SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | \ - SUPPORT_BATTERY | SUPPORT_CLEAN_SPOT | SUPPORT_START +SUPPORT_STATE_SERVICES = ( + SUPPORT_STATE + | SUPPORT_PAUSE + | SUPPORT_STOP + | SUPPORT_RETURN_HOME + | SUPPORT_FAN_SPEED + | SUPPORT_BATTERY + | SUPPORT_CLEAN_SPOT + | SUPPORT_START +) -FAN_SPEEDS = ['min', 'medium', 'high', 'max'] -DEMO_VACUUM_COMPLETE = '0_Ground_floor' -DEMO_VACUUM_MOST = '1_First_floor' -DEMO_VACUUM_BASIC = '2_Second_floor' -DEMO_VACUUM_MINIMAL = '3_Third_floor' -DEMO_VACUUM_NONE = '4_Fourth_floor' -DEMO_VACUUM_STATE = '5_Fifth_floor' +FAN_SPEEDS = ["min", "medium", "high", "max"] +DEMO_VACUUM_COMPLETE = "0_Ground_floor" +DEMO_VACUUM_MOST = "1_First_floor" +DEMO_VACUUM_BASIC = "2_Second_floor" +DEMO_VACUUM_MINIMAL = "3_Third_floor" +DEMO_VACUUM_NONE = "4_Fourth_floor" +DEMO_VACUUM_STATE = "5_Fifth_floor" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo vacuums.""" - add_entities([ - DemoVacuum(DEMO_VACUUM_COMPLETE, SUPPORT_ALL_SERVICES), - DemoVacuum(DEMO_VACUUM_MOST, SUPPORT_MOST_SERVICES), - DemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES), - DemoVacuum(DEMO_VACUUM_MINIMAL, SUPPORT_MINIMAL_SERVICES), - DemoVacuum(DEMO_VACUUM_NONE, 0), - StateDemoVacuum(DEMO_VACUUM_STATE), - ]) + add_entities( + [ + DemoVacuum(DEMO_VACUUM_COMPLETE, SUPPORT_ALL_SERVICES), + DemoVacuum(DEMO_VACUUM_MOST, SUPPORT_MOST_SERVICES), + DemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES), + DemoVacuum(DEMO_VACUUM_MINIMAL, SUPPORT_MINIMAL_SERVICES), + DemoVacuum(DEMO_VACUUM_NONE, 0), + StateDemoVacuum(DEMO_VACUUM_STATE), + ] + ) class DemoVacuum(VacuumDevice): @@ -57,7 +98,7 @@ class DemoVacuum(VacuumDevice): self._name = name self._supported_features = supported_features self._state = False - self._status = 'Charging' + self._status = "Charging" self._fan_speed = FAN_SPEEDS[1] self._cleaned_area = 0 self._battery_level = 100 @@ -125,7 +166,7 @@ class DemoVacuum(VacuumDevice): self._state = True self._cleaned_area += 5.32 self._battery_level -= 2 - self._status = 'Cleaning' + self._status = "Cleaning" self.schedule_update_ha_state() def turn_off(self, **kwargs): @@ -134,7 +175,7 @@ class DemoVacuum(VacuumDevice): return self._state = False - self._status = 'Charging' + self._status = "Charging" self.schedule_update_ha_state() def stop(self, **kwargs): @@ -143,7 +184,7 @@ class DemoVacuum(VacuumDevice): return self._state = False - self._status = 'Stopping the current task' + self._status = "Stopping the current task" self.schedule_update_ha_state() def clean_spot(self, **kwargs): @@ -172,11 +213,11 @@ class DemoVacuum(VacuumDevice): self._state = not self._state if self._state: - self._status = 'Resuming the current task' + self._status = "Resuming the current task" self._cleaned_area += 1.32 self._battery_level -= 1 else: - self._status = 'Pausing the current task' + self._status = "Pausing the current task" self.schedule_update_ha_state() def set_fan_speed(self, fan_speed, **kwargs): @@ -194,7 +235,7 @@ class DemoVacuum(VacuumDevice): return self._state = False - self._status = 'Returning home...' + self._status = "Returning home..." self._battery_level += 5 self.schedule_update_ha_state() @@ -203,7 +244,7 @@ class DemoVacuum(VacuumDevice): if self.supported_features & SUPPORT_SEND_COMMAND == 0: return - self._status = 'Executing {}({})'.format(command, params) + self._status = "Executing {}({})".format(command, params) self._state = True self.schedule_update_ha_state() diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index 6ee17bf0088..d1d53e058c6 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -2,34 +2,38 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.components.water_heater import ( - SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice) + SUPPORT_AWAY_MODE, + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterDevice, +) -SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | - SUPPORT_AWAY_MODE) +SUPPORT_FLAGS_HEATER = ( + SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo water_heater devices.""" - add_entities([ - DemoWaterHeater( - 'Demo Water Heater', 119, TEMP_FAHRENHEIT, False, 'eco'), - DemoWaterHeater( - 'Demo Water Heater Celsius', 45, TEMP_CELSIUS, True, 'eco'), - ]) + add_entities( + [ + DemoWaterHeater("Demo Water Heater", 119, TEMP_FAHRENHEIT, False, "eco"), + DemoWaterHeater("Demo Water Heater Celsius", 45, TEMP_CELSIUS, True, "eco"), + ] + ) class DemoWaterHeater(WaterHeaterDevice): """Representation of a demo water_heater device.""" - def __init__(self, name, target_temperature, unit_of_measurement, - away, current_operation): + def __init__( + self, name, target_temperature, unit_of_measurement, away, current_operation + ): """Initialize the water_heater device.""" self._name = name self._support_flags = SUPPORT_FLAGS_HEATER if target_temperature is not None: - self._support_flags = \ - self._support_flags | SUPPORT_TARGET_TEMPERATURE + self._support_flags = self._support_flags | SUPPORT_TARGET_TEMPERATURE if away is not None: self._support_flags = self._support_flags | SUPPORT_AWAY_MODE if current_operation is not None: @@ -38,9 +42,15 @@ class DemoWaterHeater(WaterHeaterDevice): self._unit_of_measurement = unit_of_measurement self._away = away self._current_operation = current_operation - self._operation_list = ['eco', 'electric', 'performance', - 'high_demand', 'heat_pump', 'gas', - 'off'] + self._operation_list = [ + "eco", + "electric", + "performance", + "high_demand", + "heat_pump", + "gas", + "off", + ] @property def supported_features(self): diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index d20e91b1f93..b81a2193bb5 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -2,49 +2,91 @@ from datetime import datetime, timedelta from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, WeatherEntity) + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + WeatherEntity, +) from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT CONDITION_CLASSES = { - 'cloudy': [], - 'fog': [], - 'hail': [], - 'lightning': [], - 'lightning-rainy': [], - 'partlycloudy': [], - 'pouring': [], - 'rainy': ['shower rain'], - 'snowy': [], - 'snowy-rainy': [], - 'sunny': ['sunshine'], - 'windy': [], - 'windy-variant': [], - 'exceptional': [], + "cloudy": [], + "fog": [], + "hail": [], + "lightning": [], + "lightning-rainy": [], + "partlycloudy": [], + "pouring": [], + "rainy": ["shower rain"], + "snowy": [], + "snowy-rainy": [], + "sunny": ["sunshine"], + "windy": [], + "windy-variant": [], + "exceptional": [], } def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo weather.""" - add_entities([ - DemoWeather('South', 'Sunshine', 21.6414, 92, 1099, 0.5, TEMP_CELSIUS, - [['rainy', 1, 22, 15], ['rainy', 5, 19, 8], - ['cloudy', 0, 15, 9], ['sunny', 0, 12, 6], - ['partlycloudy', 2, 14, 7], ['rainy', 15, 18, 7], - ['fog', 0.2, 21, 12]]), - DemoWeather('North', 'Shower rain', -12, 54, 987, 4.8, TEMP_FAHRENHEIT, - [['snowy', 2, -10, -15], ['partlycloudy', 1, -13, -14], - ['sunny', 0, -18, -22], ['sunny', 0.1, -23, -23], - ['snowy', 4, -19, -20], ['sunny', 0.3, -14, -19], - ['sunny', 0, -9, -12]]) - ]) + add_entities( + [ + DemoWeather( + "South", + "Sunshine", + 21.6414, + 92, + 1099, + 0.5, + TEMP_CELSIUS, + [ + ["rainy", 1, 22, 15], + ["rainy", 5, 19, 8], + ["cloudy", 0, 15, 9], + ["sunny", 0, 12, 6], + ["partlycloudy", 2, 14, 7], + ["rainy", 15, 18, 7], + ["fog", 0.2, 21, 12], + ], + ), + DemoWeather( + "North", + "Shower rain", + -12, + 54, + 987, + 4.8, + TEMP_FAHRENHEIT, + [ + ["snowy", 2, -10, -15], + ["partlycloudy", 1, -13, -14], + ["sunny", 0, -18, -22], + ["sunny", 0.1, -23, -23], + ["snowy", 4, -19, -20], + ["sunny", 0.3, -14, -19], + ["sunny", 0, -9, -12], + ], + ), + ] + ) class DemoWeather(WeatherEntity): """Representation of a weather condition.""" - def __init__(self, name, condition, temperature, humidity, pressure, - wind_speed, temperature_unit, forecast): + def __init__( + self, + name, + condition, + temperature, + humidity, + pressure, + wind_speed, + temperature_unit, + forecast, + ): """Initialize the Demo weather.""" self._name = name self._condition = condition @@ -58,7 +100,7 @@ class DemoWeather(WeatherEntity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format('Demo Weather', self._name) + return "{} {}".format("Demo Weather", self._name) @property def should_poll(self): @@ -93,13 +135,14 @@ class DemoWeather(WeatherEntity): @property def condition(self): """Return the weather condition.""" - return [k for k, v in CONDITION_CLASSES.items() if - self._condition.lower() in v][0] + return [ + k for k, v in CONDITION_CLASSES.items() if self._condition.lower() in v + ][0] @property def attribution(self): """Return the attribution.""" - return 'Powered by Home Assistant' + return "Powered by Home Assistant" @property def forecast(self): @@ -113,7 +156,7 @@ class DemoWeather(WeatherEntity): ATTR_FORECAST_CONDITION: entry[0], ATTR_FORECAST_PRECIPITATION: entry[1], ATTR_FORECAST_TEMP: entry[2], - ATTR_FORECAST_TEMP_LOW: entry[3] + ATTR_FORECAST_TEMP_LOW: entry[3], } reftime = reftime + timedelta(hours=4) forecast_data.append(data_dict) diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 07f6fcc7f9c..7bed8423e8f 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -4,40 +4,74 @@ import telnetlib import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Music station' +DEFAULT_NAME = "Music station" -SUPPORT_DENON = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE \ +SUPPORT_DENON = ( + SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE +) +SUPPORT_MEDIA_MODES = ( + SUPPORT_PAUSE + | SUPPORT_STOP + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_PLAY +) -SUPPORT_MEDIA_MODES = SUPPORT_PAUSE | SUPPORT_STOP | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +NORMAL_INPUTS = { + "Cd": "CD", + "Dvd": "DVD", + "Blue ray": "BD", + "TV": "TV", + "Satellite / Cable": "SAT/CBL", + "Game": "GAME", + "Game2": "GAME2", + "Video Aux": "V.AUX", + "Dock": "DOCK", +} -NORMAL_INPUTS = {'Cd': 'CD', 'Dvd': 'DVD', 'Blue ray': 'BD', 'TV': 'TV', - 'Satelite / Cable': 'SAT/CBL', 'Game': 'GAME', - 'Game2': 'GAME2', 'Video Aux': 'V.AUX', 'Dock': 'DOCK'} - -MEDIA_MODES = {'Tuner': 'TUNER', 'Media server': 'SERVER', - 'Ipod dock': 'IPOD', 'Net/USB': 'NET/USB', - 'Rapsody': 'RHAPSODY', 'Napster': 'NAPSTER', - 'Pandora': 'PANDORA', 'LastFM': 'LASTFM', - 'Flickr': 'FLICKR', 'Favorites': 'FAVORITES', - 'Internet Radio': 'IRADIO', 'USB/IPOD': 'USB/IPOD'} +MEDIA_MODES = { + "Tuner": "TUNER", + "Media server": "SERVER", + "Ipod dock": "IPOD", + "Net/USB": "NET/USB", + "Rapsody": "RHAPSODY", + "Napster": "NAPSTER", + "Pandora": "PANDORA", + "LastFM": "LASTFM", + "Flickr": "FLICKR", + "Favorites": "FAVORITES", + "Internet Radio": "IRADIO", + "USB/IPOD": "USB/IPOD", +} # Sub-modes of 'NET/USB' # {'USB': 'USB', 'iPod Direct': 'IPD', 'Internet Radio': 'IRP', @@ -59,34 +93,34 @@ class DenonDevice(MediaPlayerDevice): """Initialize the Denon device.""" self._name = name self._host = host - self._pwstate = 'PWSTANDBY' + self._pwstate = "PWSTANDBY" self._volume = 0 # Initial value 60dB, changed if we get a MVMAX self._volume_max = 60 self._source_list = NORMAL_INPUTS.copy() self._source_list.update(MEDIA_MODES) self._muted = False - self._mediasource = '' - self._mediainfo = '' + self._mediasource = "" + self._mediainfo = "" self._should_setup_sources = True def _setup_sources(self, telnet): # NSFRN - Network name - nsfrn = self.telnet_request(telnet, 'NSFRN ?')[len('NSFRN '):] + nsfrn = self.telnet_request(telnet, "NSFRN ?")[len("NSFRN ") :] if nsfrn: self._name = nsfrn # SSFUN - Configured sources with names self._source_list = {} - for line in self.telnet_request(telnet, 'SSFUN ?', all_lines=True): - source, configured_name = line[len('SSFUN'):].split(" ", 1) + for line in self.telnet_request(telnet, "SSFUN ?", all_lines=True): + source, configured_name = line[len("SSFUN") :].split(" ", 1) self._source_list[configured_name] = source # SSSOD - Deleted sources - for line in self.telnet_request(telnet, 'SSSOD ?', all_lines=True): - source, status = line[len('SSSOD'):].split(" ", 1) - if status == 'DEL': + for line in self.telnet_request(telnet, "SSSOD ?", all_lines=True): + source, status = line[len("SSSOD") :].split(" ", 1) + if status == "DEL": for pretty_name, name in self._source_list.items(): if source == name: del self._source_list[pretty_name] @@ -96,24 +130,24 @@ class DenonDevice(MediaPlayerDevice): def telnet_request(cls, telnet, command, all_lines=False): """Execute `command` and return the response.""" _LOGGER.debug("Sending: %s", command) - telnet.write(command.encode('ASCII') + b'\r') + telnet.write(command.encode("ASCII") + b"\r") lines = [] while True: - line = telnet.read_until(b'\r', timeout=0.2) + line = telnet.read_until(b"\r", timeout=0.2) if not line: break - lines.append(line.decode('ASCII').strip()) + lines.append(line.decode("ASCII").strip()) _LOGGER.debug("Received: %s", line) if all_lines: return lines - return lines[0] if lines else '' + return lines[0] if lines else "" def telnet_command(self, command): """Establish a telnet connection and sends `command`.""" telnet = telnetlib.Telnet(self._host) _LOGGER.debug("Sending: %s", command) - telnet.write(command.encode('ASCII') + b'\r') + telnet.write(command.encode("ASCII") + b"\r") telnet.read_very_eager() # skip response telnet.close() @@ -128,23 +162,32 @@ class DenonDevice(MediaPlayerDevice): self._setup_sources(telnet) self._should_setup_sources = False - self._pwstate = self.telnet_request(telnet, 'PW?') - for line in self.telnet_request(telnet, 'MV?', all_lines=True): - if line.startswith('MVMAX '): + self._pwstate = self.telnet_request(telnet, "PW?") + for line in self.telnet_request(telnet, "MV?", all_lines=True): + if line.startswith("MVMAX "): # only grab two digit max, don't care about any half digit - self._volume_max = int(line[len('MVMAX '):len('MVMAX XX')]) + self._volume_max = int(line[len("MVMAX ") : len("MVMAX XX")]) continue - if line.startswith('MV'): - self._volume = int(line[len('MV'):]) - self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON') - self._mediasource = self.telnet_request(telnet, 'SI?')[len('SI'):] + if line.startswith("MV"): + self._volume = int(line[len("MV") :]) + self._muted = self.telnet_request(telnet, "MU?") == "MUON" + self._mediasource = self.telnet_request(telnet, "SI?")[len("SI") :] if self._mediasource in MEDIA_MODES.values(): self._mediainfo = "" - answer_codes = ["NSE0", "NSE1X", "NSE2X", "NSE3X", "NSE4", "NSE5", - "NSE6", "NSE7", "NSE8"] - for line in self.telnet_request(telnet, 'NSE', all_lines=True): - self._mediainfo += line[len(answer_codes.pop(0)):] + '\n' + answer_codes = [ + "NSE0", + "NSE1X", + "NSE2X", + "NSE3X", + "NSE4", + "NSE5", + "NSE6", + "NSE7", + "NSE8", + ] + for line in self.telnet_request(telnet, "NSE", all_lines=True): + self._mediainfo += line[len(answer_codes.pop(0)) :] + "\n" else: self._mediainfo = self.source @@ -159,9 +202,9 @@ class DenonDevice(MediaPlayerDevice): @property def state(self): """Return the state of the device.""" - if self._pwstate == 'PWSTANDBY': + if self._pwstate == "PWSTANDBY": return STATE_OFF - if self._pwstate == 'PWON': + if self._pwstate == "PWON": return STATE_ON return None @@ -202,49 +245,48 @@ class DenonDevice(MediaPlayerDevice): def turn_off(self): """Turn off media player.""" - self.telnet_command('PWSTANDBY') + self.telnet_command("PWSTANDBY") def volume_up(self): """Volume up media player.""" - self.telnet_command('MVUP') + self.telnet_command("MVUP") def volume_down(self): """Volume down media player.""" - self.telnet_command('MVDOWN') + self.telnet_command("MVDOWN") def set_volume_level(self, volume): """Set volume level, range 0..1.""" - self.telnet_command('MV' + - str(round(volume * self._volume_max)).zfill(2)) + self.telnet_command("MV" + str(round(volume * self._volume_max)).zfill(2)) def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" - self.telnet_command('MU' + ('ON' if mute else 'OFF')) + self.telnet_command("MU" + ("ON" if mute else "OFF")) def media_play(self): """Play media player.""" - self.telnet_command('NS9A') + self.telnet_command("NS9A") def media_pause(self): """Pause media player.""" - self.telnet_command('NS9B') + self.telnet_command("NS9B") def media_stop(self): """Pause media player.""" - self.telnet_command('NS9C') + self.telnet_command("NS9C") def media_next_track(self): """Send the next track command.""" - self.telnet_command('NS9D') + self.telnet_command("NS9D") def media_previous_track(self): """Send the previous track command.""" - self.telnet_command('NS9E') + self.telnet_command("NS9E") def turn_on(self): """Turn the media player on.""" - self.telnet_command('PWON') + self.telnet_command("PWON") def select_source(self, source): """Select input source.""" - self.telnet_command('SI' + self._source_list.get(source)) + self.telnet_command("SI" + self._source_list.get(source)) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index da416ce8045..51fc890c873 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -5,57 +5,85 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP) + MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_MUSIC, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOUND_MODE, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_TIMEOUT, CONF_ZONE, STATE_OFF, STATE_ON, - STATE_PAUSED, STATE_PLAYING) + CONF_HOST, + CONF_NAME, + CONF_TIMEOUT, + CONF_ZONE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_SOUND_MODE_RAW = 'sound_mode_raw' +ATTR_SOUND_MODE_RAW = "sound_mode_raw" -CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)' -CONF_SHOW_ALL_SOURCES = 'show_all_sources' -CONF_VALID_ZONES = ['Zone2', 'Zone3'] -CONF_ZONES = 'zones' +CONF_INVALID_ZONES_ERR = "Invalid Zone (expected Zone2 or Zone3)" +CONF_SHOW_ALL_SOURCES = "show_all_sources" +CONF_VALID_ZONES = ["Zone2", "Zone3"] +CONF_ZONES = "zones" DEFAULT_SHOW_SOURCES = False DEFAULT_TIMEOUT = 2 -KEY_DENON_CACHE = 'denonavr_hosts' +KEY_DENON_CACHE = "denonavr_hosts" -SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE | SUPPORT_VOLUME_SET +SUPPORT_DENON = ( + SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_MUTE + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE + | SUPPORT_VOLUME_SET +) -SUPPORT_MEDIA_MODES = SUPPORT_PLAY_MEDIA | \ - SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET | SUPPORT_PLAY +SUPPORT_MEDIA_MODES = ( + SUPPORT_PLAY_MEDIA + | SUPPORT_PAUSE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_VOLUME_SET + | SUPPORT_PLAY +) -DENON_ZONE_SCHEMA = vol.Schema({ - vol.Required(CONF_ZONE): vol.In(CONF_VALID_ZONES, CONF_INVALID_ZONES_ERR), - vol.Optional(CONF_NAME): cv.string, -}) +DENON_ZONE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ZONE): vol.In(CONF_VALID_ZONES, CONF_INVALID_ZONES_ERR), + vol.Optional(CONF_NAME): cv.string, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES): - cv.boolean, - vol.Optional(CONF_ZONES): - vol.All(cv.ensure_list, [DENON_ZONE_SCHEMA]), - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES): cv.boolean, + vol.Optional(CONF_ZONES): vol.All(cv.ensure_list, [DENON_ZONE_SCHEMA]), + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } +) -NewHost = namedtuple('NewHost', ['host', 'name']) +NewHost = namedtuple("NewHost", ["host", "name"]) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -92,8 +120,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # 2. option: discovery using netdisco if discovery_info is not None: - host = discovery_info.get('host') - name = discovery_info.get('name') + host = discovery_info.get("host") + name = discovery_info.get("name") new_hosts.append(NewHost(host=host, name=name)) # 3. option: discovery using denonavr library @@ -103,17 +131,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for d_receiver in d_receivers: host = d_receiver["host"] name = d_receiver["friendlyName"] - new_hosts.append( - NewHost(host=host, name=name)) + new_hosts.append(NewHost(host=host, name=name)) for entry in new_hosts: # Check if host not in cache, append it and save for later # starting if entry.host not in cache: new_device = denonavr.DenonAVR( - host=entry.host, name=entry.name, - show_all_inputs=show_all_sources, timeout=timeout, - add_zones=add_zones) + host=entry.host, + name=entry.name, + show_all_inputs=show_all_sources, + timeout=timeout, + add_zones=add_zones, + ) for new_zone in new_device.zones.values(): receivers.append(DenonDevice(new_zone)) cache.add(host) @@ -156,8 +186,9 @@ class DenonDevice(MediaPlayerDevice): self._sound_mode_list = None self._supported_features_base = SUPPORT_DENON - self._supported_features_base |= (self._sound_mode_support and - SUPPORT_SELECT_SOUND_MODE) + self._supported_features_base |= ( + self._sound_mode_support and SUPPORT_SELECT_SOUND_MODE + ) def update(self): """Get the latest status information from device.""" @@ -305,8 +336,11 @@ class DenonDevice(MediaPlayerDevice): def device_state_attributes(self): """Return device specific state attributes.""" attributes = {} - if (self._sound_mode_raw is not None and self._sound_mode_support and - self._power == 'ON'): + if ( + self._sound_mode_raw is not None + and self._sound_mode_support + and self._power == "ON" + ): attributes[ATTR_SOUND_MODE_RAW] = self._sound_mode_raw return attributes diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index c6761c58e57..db094bb9b12 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -11,23 +11,25 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -CONF_DESTINATION = 'to' -CONF_START = 'from' -CONF_OFFSET = 'offset' +CONF_DESTINATION = "to" +CONF_START = "from" +CONF_OFFSET = "offset" DEFAULT_OFFSET = timedelta(minutes=0) -CONF_ONLY_DIRECT = 'only_direct' +CONF_ONLY_DIRECT = "only_direct" DEFAULT_ONLY_DIRECT = False -ICON = 'mdi:train' +ICON = "mdi:train" SCAN_INTERVAL = timedelta(minutes=2) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DESTINATION): cv.string, - vol.Required(CONF_START): cv.string, - vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): cv.time_period, - vol.Optional(CONF_ONLY_DIRECT, default=DEFAULT_ONLY_DIRECT): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_DESTINATION): cv.string, + vol.Required(CONF_START): cv.string, + vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): cv.time_period, + vol.Optional(CONF_ONLY_DIRECT, default=DEFAULT_ONLY_DIRECT): cv.boolean, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -37,8 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): offset = config.get(CONF_OFFSET) only_direct = config.get(CONF_ONLY_DIRECT) - add_entities([DeutscheBahnSensor(start, destination, - offset, only_direct)], True) + add_entities([DeutscheBahnSensor(start, destination, offset, only_direct)], True) class DeutscheBahnSensor(Entity): @@ -46,7 +47,7 @@ class DeutscheBahnSensor(Entity): def __init__(self, start, goal, offset, only_direct): """Initialize the sensor.""" - self._name = '{} to {}'.format(start, goal) + self._name = "{} to {}".format(start, goal) self.data = SchieneData(start, goal, offset, only_direct) self._state = None @@ -70,17 +71,17 @@ class DeutscheBahnSensor(Entity): """Return the state attributes.""" connections = self.data.connections[0] if len(self.data.connections) > 1: - connections['next'] = self.data.connections[1]['departure'] + connections["next"] = self.data.connections[1]["departure"] if len(self.data.connections) > 2: - connections['next_on'] = self.data.connections[2]['departure'] + connections["next_on"] = self.data.connections[2]["departure"] return connections def update(self): """Get the latest delay from bahn.de and updates the state.""" self.data.update() - self._state = self.data.connections[0].get('departure', 'Unknown') - if self.data.connections[0].get('delay', 0) != 0: - self._state += " + {}".format(self.data.connections[0]['delay']) + self._state = self.data.connections[0].get("departure", "Unknown") + if self.data.connections[0].get("delay", 0) != 0: + self._state += " + {}".format(self.data.connections[0]["delay"]) class SchieneData: @@ -100,9 +101,11 @@ class SchieneData: def update(self): """Update the connection data.""" self.connections = self.schiene.connections( - self.start, self.goal, - dt_util.as_local(dt_util.utcnow()+self.offset), - self.only_direct) + self.start, + self.goal, + dt_util.as_local(dt_util.utcnow() + self.offset), + self.only_direct, + ) if not self.connections: self.connections = [{}] @@ -110,10 +113,9 @@ class SchieneData: for con in self.connections: # Detail info is not useful. Having a more consistent interface # simplifies usage of template sensors. - if 'details' in con: - con.pop('details') - delay = con.get('delay', {'delay_departure': 0, - 'delay_arrival': 0}) - con['delay'] = delay['delay_departure'] - con['delay_arrival'] = delay['delay_arrival'] - con['ontime'] = con.get('ontime', False) + if "details" in con: + con.pop("details") + delay = con.get("delay", {"delay_departure": 0, "delay_arrival": 0}) + con["delay"] = delay["delay_departure"] + con["delay_arrival"] = delay["delay_arrival"] + con["ontime"] = con.get("ontime", False) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 67ad51210df..b1f319b0a6a 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -9,7 +9,7 @@ from homeassistant.core import split_entity_id from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.loader import async_get_integration, IntegrationNotFound -DOMAIN = 'device_automation' +DOMAIN = "device_automation" _LOGGER = logging.getLogger(__name__) @@ -17,7 +17,8 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass, config): """Set up device automation.""" hass.components.websocket_api.async_register_command( - websocket_device_automation_list_triggers) + websocket_device_automation_list_triggers + ) return True @@ -27,16 +28,16 @@ async def _async_get_device_automation_triggers(hass, domain, device_id): try: integration = await async_get_integration(hass, domain) except IntegrationNotFound: - _LOGGER.warning('Integration %s not found', domain) + _LOGGER.warning("Integration %s not found", domain) return None try: - platform = integration.get_platform('device_automation') + platform = integration.get_platform("device_automation") except ImportError: # The domain does not have device automations return None - if hasattr(platform, 'async_get_triggers'): + if hasattr(platform, "async_get_triggers"): return await platform.async_get_triggers(hass, device_id) @@ -44,7 +45,8 @@ async def async_get_device_automation_triggers(hass, device_id): """List device triggers.""" device_registry, entity_registry = await asyncio.gather( hass.helpers.device_registry.async_get_registry(), - hass.helpers.entity_registry.async_get_registry()) + hass.helpers.entity_registry.async_get_registry(), + ) domains = set() triggers = [] @@ -57,10 +59,12 @@ async def async_get_device_automation_triggers(hass, device_id): for entity in entities: domains.add(split_entity_id(entity.entity_id)[0]) - device_triggers = await asyncio.gather(*[ - _async_get_device_automation_triggers(hass, domain, device_id) - for domain in domains - ]) + device_triggers = await asyncio.gather( + *( + _async_get_device_automation_triggers(hass, domain, device_id) + for domain in domains + ) + ) for device_trigger in device_triggers: if device_trigger is not None: triggers.extend(device_trigger) @@ -69,12 +73,14 @@ async def async_get_device_automation_triggers(hass, device_id): @websocket_api.async_response -@websocket_api.websocket_command({ - vol.Required('type'): 'device_automation/list_triggers', - vol.Required('device_id'): str, -}) +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/list_triggers", + vol.Required("device_id"): str, + } +) async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" - device_id = msg['device_id'] + device_id = msg["device_id"] triggers = await async_get_device_automation_triggers(hass, device_id) - connection.send_result(msg['id'], {'triggers': triggers}) + connection.send_result(msg["id"], {"triggers": triggers}) diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index 945f8368671..1b71b44369d 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -7,36 +7,54 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.util.dt as dt_util from homeassistant.components.light import ( - ATTR_PROFILE, ATTR_TRANSITION, DOMAIN as DOMAIN_LIGHT) + ATTR_PROFILE, + ATTR_TRANSITION, + DOMAIN as DOMAIN_LIGHT, +) from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_HOME, - STATE_NOT_HOME, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_HOME, + STATE_NOT_HOME, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, +) from homeassistant.helpers.event import ( - async_track_point_in_utc_time, async_track_state_change) + async_track_point_in_utc_time, + async_track_state_change, +) from homeassistant.helpers.sun import is_up, get_astral_event_next import homeassistant.helpers.config_validation as cv -DOMAIN = 'device_sun_light_trigger' -CONF_DEVICE_GROUP = 'device_group' -CONF_DISABLE_TURN_OFF = 'disable_turn_off' -CONF_LIGHT_GROUP = 'light_group' -CONF_LIGHT_PROFILE = 'light_profile' +DOMAIN = "device_sun_light_trigger" +CONF_DEVICE_GROUP = "device_group" +CONF_DISABLE_TURN_OFF = "disable_turn_off" +CONF_LIGHT_GROUP = "light_group" +CONF_LIGHT_PROFILE = "light_profile" DEFAULT_DISABLE_TURN_OFF = False -DEFAULT_LIGHT_PROFILE = 'relax' +DEFAULT_LIGHT_PROFILE = "relax" LIGHT_TRANSITION_TIME = timedelta(minutes=15) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_DEVICE_GROUP): cv.entity_id, - vol.Optional(CONF_DISABLE_TURN_OFF, default=DEFAULT_DISABLE_TURN_OFF): - cv.boolean, - vol.Optional(CONF_LIGHT_GROUP): cv.string, - vol.Optional(CONF_LIGHT_PROFILE, default=DEFAULT_LIGHT_PROFILE): - cv.string, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_DEVICE_GROUP): cv.entity_id, + vol.Optional( + CONF_DISABLE_TURN_OFF, default=DEFAULT_DISABLE_TURN_OFF + ): cv.boolean, + vol.Optional(CONF_LIGHT_GROUP): cv.string, + vol.Optional( + CONF_LIGHT_PROFILE, default=DEFAULT_LIGHT_PROFILE + ): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -49,10 +67,8 @@ async def async_setup(hass, config): disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF) light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS) light_profile = conf.get(CONF_LIGHT_PROFILE) - device_group = conf.get( - CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES) - device_entity_ids = group.get_entity_ids( - device_group, device_tracker.DOMAIN) + device_group = conf.get(CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES) + device_entity_ids = group.get_entity_ids(device_group, device_tracker.DOMAIN) if not device_entity_ids: logger.error("No devices found to track") @@ -83,13 +99,19 @@ async def async_setup(hass, config): return hass.async_create_task( hass.services.async_call( - DOMAIN_LIGHT, SERVICE_TURN_ON, { + DOMAIN_LIGHT, + SERVICE_TURN_ON, + { ATTR_ENTITY_ID: light_id, ATTR_TRANSITION: LIGHT_TRANSITION_TIME.seconds, - ATTR_PROFILE: light_profile})) + ATTR_PROFILE: light_profile, + }, + ) + ) def async_turn_on_factory(light_id): """Generate turn on callbacks as factory.""" + @callback def async_turn_on_light(now): """Turn on specific light.""" @@ -112,12 +134,14 @@ async def async_setup(hass, config): for index, light_id in enumerate(light_ids): async_track_point_in_utc_time( - hass, async_turn_on_factory(light_id), - start_point + index * LIGHT_TRANSITION_TIME) + hass, + async_turn_on_factory(light_id), + start_point + index * LIGHT_TRANSITION_TIME, + ) - async_track_point_in_utc_time(hass, schedule_light_turn_on, - get_astral_event_next(hass, - SUN_EVENT_SUNRISE)) + async_track_point_in_utc_time( + hass, schedule_light_turn_on, get_astral_event_next(hass, SUN_EVENT_SUNRISE) + ) # If the sun is already above horizon schedule the time-based pre-sun set # event. @@ -139,16 +163,19 @@ async def async_setup(hass, config): logger.info("Home coming event for %s. Turning lights on", entity) hass.async_create_task( hass.services.async_call( - DOMAIN_LIGHT, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: light_ids, ATTR_PROFILE: light_profile})) + DOMAIN_LIGHT, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light_ids, ATTR_PROFILE: light_profile}, + ) + ) # Are we in the time span were we would turn on the lights # if someone would be home? # Check this by seeing if current time is later then the point # in time when we would start putting the lights on. - elif (start_point and - start_point < now < get_astral_event_next(hass, - SUN_EVENT_SUNSET)): + elif start_point and start_point < now < get_astral_event_next( + hass, SUN_EVENT_SUNSET + ): # Check for every light if it would be on if someone was home # when the fading in started and turn it on if so @@ -156,8 +183,9 @@ async def async_setup(hass, config): if now > start_point + index * LIGHT_TRANSITION_TIME: hass.async_create_task( hass.services.async_call( - DOMAIN_LIGHT, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: light_id})) + DOMAIN_LIGHT, SERVICE_TURN_ON, {ATTR_ENTITY_ID: light_id} + ) + ) else: # If this light didn't happen to be turned on yet so @@ -165,8 +193,12 @@ async def async_setup(hass, config): break async_track_state_change( - hass, device_entity_ids, check_light_on_dev_state_change, - STATE_NOT_HOME, STATE_HOME) + hass, + device_entity_ids, + check_light_on_dev_state_change, + STATE_NOT_HOME, + STATE_HOME, + ) if disable_turn_off: return True @@ -177,14 +209,15 @@ async def async_setup(hass, config): if not group.is_on(light_group): return - logger.info( - "Everyone has left but there are lights on. Turning them off") + logger.info("Everyone has left but there are lights on. Turning them off") hass.async_create_task( hass.services.async_call( - DOMAIN_LIGHT, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: light_ids})) + DOMAIN_LIGHT, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: light_ids} + ) + ) async_track_state_change( - hass, device_group, turn_off_lights_when_all_leave, - STATE_HOME, STATE_NOT_HOME) + hass, device_group, turn_off_lights_when_all_leave, STATE_HOME, STATE_NOT_HOME + ) return True diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 4c67e6fa65d..84bc76e0b04 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -14,7 +14,8 @@ from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME from . import legacy, setup from .config_entry import ( # noqa # pylint: disable=unused-import - async_setup_entry, async_unload_entry + async_setup_entry, + async_unload_entry, ) from .legacy import DeviceScanner # noqa # pylint: disable=unused-import from .const import ( @@ -43,43 +44,57 @@ from .const import ( SOURCE_TYPE_ROUTER, ) -ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices') +ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format("all_devices") -SERVICE_SEE = 'see' +SERVICE_SEE = "see" -SOURCE_TYPES = (SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, - SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE) +SOURCE_TYPES = ( + SOURCE_TYPE_GPS, + SOURCE_TYPE_ROUTER, + SOURCE_TYPE_BLUETOOTH, + SOURCE_TYPE_BLUETOOTH_LE, +) -NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({ - vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, - vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, -})) -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, - vol.Optional(CONF_TRACK_NEW): cv.boolean, - vol.Optional(CONF_CONSIDER_HOME, - default=DEFAULT_CONSIDER_HOME): vol.All( - cv.time_period, cv.positive_timedelta), - vol.Optional(CONF_NEW_DEVICE_DEFAULTS, - default={}): NEW_DEVICE_DEFAULTS_SCHEMA -}) +NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( + None, + vol.Schema( + { + vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, + vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, + } + ), +) +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_TRACK_NEW): cv.boolean, + vol.Optional(CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME): vol.All( + cv.time_period, cv.positive_timedelta + ), + vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA, + } +) PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) -SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema(vol.All( - cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID), { - ATTR_MAC: cv.string, - ATTR_DEV_ID: cv.string, - ATTR_HOST_NAME: cv.string, - ATTR_LOCATION_NAME: cv.string, - ATTR_GPS: cv.gps, - ATTR_GPS_ACCURACY: cv.positive_int, - ATTR_BATTERY: cv.positive_int, - ATTR_ATTRIBUTES: dict, - ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), - ATTR_CONSIDER_HOME: cv.time_period, - # Temp workaround for iOS app introduced in 0.65 - vol.Optional('battery_status'): str, - vol.Optional('hostname'): str, - })) +SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema( + vol.All( + cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID), + { + ATTR_MAC: cv.string, + ATTR_DEV_ID: cv.string, + ATTR_HOST_NAME: cv.string, + ATTR_LOCATION_NAME: cv.string, + ATTR_GPS: cv.gps, + ATTR_GPS_ACCURACY: cv.positive_int, + ATTR_BATTERY: cv.positive_int, + ATTR_ATTRIBUTES: dict, + ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), + ATTR_CONSIDER_HOME: cv.time_period, + # Temp workaround for iOS app introduced in 0.65 + vol.Optional("battery_status"): str, + vol.Optional("hostname"): str, + }, + ) +) @bind_hass @@ -90,19 +105,31 @@ def is_on(hass: HomeAssistantType, entity_id: str = None): return hass.states.is_state(entity, STATE_HOME) -def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None, - host_name: str = None, location_name: str = None, - gps: GPSType = None, gps_accuracy=None, - battery: int = None, attributes: dict = None): +def see( + hass: HomeAssistantType, + mac: str = None, + dev_id: str = None, + host_name: str = None, + location_name: str = None, + gps: GPSType = None, + gps_accuracy=None, + battery: int = None, + attributes: dict = None, +): """Call service to notify you see device.""" - data = {key: value for key, value in - ((ATTR_MAC, mac), - (ATTR_DEV_ID, dev_id), - (ATTR_HOST_NAME, host_name), - (ATTR_LOCATION_NAME, location_name), - (ATTR_GPS, gps), - (ATTR_GPS_ACCURACY, gps_accuracy), - (ATTR_BATTERY, battery)) if value is not None} + data = { + key: value + for key, value in ( + (ATTR_MAC, mac), + (ATTR_DEV_ID, dev_id), + (ATTR_HOST_NAME, host_name), + (ATTR_LOCATION_NAME, location_name), + (ATTR_GPS, gps), + (ATTR_GPS_ACCURACY, gps_accuracy), + (ATTR_BATTERY, battery), + ) + if value is not None + } if attributes: data[ATTR_ATTRIBUTES] = attributes hass.services.call(DOMAIN, SERVICE_SEE, data) @@ -126,8 +153,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_platform_discovered(p_type, info): """Load a platform.""" - platform = await setup.async_create_platform_type( - hass, config, p_type, {}) + platform = await setup.async_create_platform_type(hass, config, p_type, {}) if platform is None or platform.type != PLATFORM_TYPE_LEGACY: return @@ -138,18 +164,20 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): # Clean up stale devices async_track_utc_time_change( - hass, tracker.async_update_stale, second=range(0, 60, 5)) + hass, tracker.async_update_stale, second=range(0, 60, 5) + ) async def async_see_service(call): """Service to see a device.""" # Temp workaround for iOS, introduced in 0.65 data = dict(call.data) - data.pop('hostname', None) - data.pop('battery_status', None) + data.pop("hostname", None) + data.pop("battery_status", None) await tracker.async_see(**data) hass.services.async_register( - DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA) + DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA + ) # restore await tracker.async_setup_tracked_device() diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 6efcd7826a4..460f1198409 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -13,11 +13,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from .const import ( - ATTR_SOURCE_TYPE, - DOMAIN, - LOGGER, -) +from .const import ATTR_SOURCE_TYPE, DOMAIN, LOGGER async def async_setup_entry(hass, entry): @@ -25,9 +21,7 @@ async def async_setup_entry(hass, entry): component = hass.data.get(DOMAIN) # type: Optional[EntityComponent] if component is None: - component = hass.data[DOMAIN] = EntityComponent( - LOGGER, DOMAIN, hass - ) + component = hass.data[DOMAIN] = EntityComponent(LOGGER, DOMAIN, hass) return await component.async_setup_entry(entry) @@ -56,9 +50,7 @@ class BaseTrackerEntity(Entity): @property def state_attributes(self): """Return the device state attributes.""" - attr = { - ATTR_SOURCE_TYPE: self.source_type - } + attr = {ATTR_SOURCE_TYPE: self.source_type} if self.battery_level: attr[ATTR_BATTERY_LEVEL] = self.battery_level @@ -100,8 +92,8 @@ class TrackerEntity(BaseTrackerEntity): if self.latitude is not None: zone_state = zone.async_active_zone( - self.hass, self.latitude, self.longitude, - self.location_accuracy) + self.hass, self.latitude, self.longitude, self.location_accuracy + ) if zone_state is None: state = STATE_NOT_HOME elif zone_state.entity_id == zone.ENTITY_ID_HOME: diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index 18ec486e693..1778a87b36a 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -4,37 +4,37 @@ import logging LOGGER = logging.getLogger(__package__) -DOMAIN = 'device_tracker' -ENTITY_ID_FORMAT = DOMAIN + '.{}' +DOMAIN = "device_tracker" +ENTITY_ID_FORMAT = DOMAIN + ".{}" -PLATFORM_TYPE_LEGACY = 'legacy' -PLATFORM_TYPE_ENTITY = 'entity_platform' +PLATFORM_TYPE_LEGACY = "legacy" +PLATFORM_TYPE_ENTITY = "entity_platform" -SOURCE_TYPE_GPS = 'gps' -SOURCE_TYPE_ROUTER = 'router' -SOURCE_TYPE_BLUETOOTH = 'bluetooth' -SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le' +SOURCE_TYPE_GPS = "gps" +SOURCE_TYPE_ROUTER = "router" +SOURCE_TYPE_BLUETOOTH = "bluetooth" +SOURCE_TYPE_BLUETOOTH_LE = "bluetooth_le" -CONF_SCAN_INTERVAL = 'interval_seconds' +CONF_SCAN_INTERVAL = "interval_seconds" SCAN_INTERVAL = timedelta(seconds=12) -CONF_TRACK_NEW = 'track_new_devices' +CONF_TRACK_NEW = "track_new_devices" DEFAULT_TRACK_NEW = True -CONF_AWAY_HIDE = 'hide_if_away' +CONF_AWAY_HIDE = "hide_if_away" DEFAULT_AWAY_HIDE = False -CONF_CONSIDER_HOME = 'consider_home' +CONF_CONSIDER_HOME = "consider_home" DEFAULT_CONSIDER_HOME = timedelta(seconds=180) -CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults' +CONF_NEW_DEVICE_DEFAULTS = "new_device_defaults" -ATTR_ATTRIBUTES = 'attributes' -ATTR_BATTERY = 'battery' -ATTR_DEV_ID = 'dev_id' -ATTR_GPS = 'gps' -ATTR_HOST_NAME = 'host_name' -ATTR_LOCATION_NAME = 'location_name' -ATTR_MAC = 'mac' -ATTR_SOURCE_TYPE = 'source_type' -ATTR_CONSIDER_HOME = 'consider_home' +ATTR_ATTRIBUTES = "attributes" +ATTR_BATTERY = "battery" +ATTR_DEV_ID = "dev_id" +ATTR_GPS = "gps" +ATTR_HOST_NAME = "host_name" +ATTR_LOCATION_NAME = "location_name" +ATTR_MAC = "mac" +ATTR_SOURCE_TYPE = "source_type" +ATTR_CONSIDER_HOME = "consider_home" diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 1a2e7c854e5..67e35df00a1 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -8,8 +8,13 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components import zone from homeassistant.components.group import ( - ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE, - DOMAIN as DOMAIN_GROUP, SERVICE_SET) + ATTR_ADD_ENTITIES, + ATTR_ENTITIES, + ATTR_OBJECT_ID, + ATTR_VISIBLE, + DOMAIN as DOMAIN_GROUP, + SERVICE_SET, +) from homeassistant.components.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError @@ -22,9 +27,19 @@ import homeassistant.util.dt as dt_util from homeassistant.util.yaml import dump from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ICON, ATTR_LATITUDE, - ATTR_LONGITUDE, ATTR_NAME, CONF_ICON, CONF_MAC, CONF_NAME, - DEVICE_DEFAULT_NAME, STATE_NOT_HOME, STATE_HOME) + ATTR_ENTITY_ID, + ATTR_GPS_ACCURACY, + ATTR_ICON, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_NAME, + CONF_ICON, + CONF_MAC, + CONF_NAME, + DEVICE_DEFAULT_NAME, + STATE_NOT_HOME, + STATE_HOME, +) from .const import ( ATTR_BATTERY, @@ -44,9 +59,9 @@ from .const import ( SOURCE_TYPE_GPS, ) -YAML_DEVICES = 'known_devices.yaml' -GROUP_NAME_ALL_DEVICES = 'all devices' -EVENT_NEW_DEVICE = 'device_tracker_new_device' +YAML_DEVICES = "known_devices.yaml" +GROUP_NAME_ALL_DEVICES = "all devices" +EVENT_NEW_DEVICE = "device_tracker_new_device" async def get_tracker(hass, config): @@ -63,75 +78,116 @@ async def get_tracker(hass, config): track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) devices = await async_load_config(yaml_path, hass, consider_home) - tracker = DeviceTracker( - hass, consider_home, track_new, defaults, devices) + tracker = DeviceTracker(hass, consider_home, track_new, defaults, devices) return tracker class DeviceTracker: """Representation of a device tracker.""" - def __init__(self, hass: HomeAssistantType, consider_home: timedelta, - track_new: bool, defaults: dict, - devices: Sequence) -> None: + def __init__( + self, + hass: HomeAssistantType, + consider_home: timedelta, + track_new: bool, + defaults: dict, + devices: Sequence, + ) -> None: """Initialize a device tracker.""" self.hass = hass self.devices = {dev.dev_id: dev for dev in devices} self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} self.consider_home = consider_home - self.track_new = track_new if track_new is not None \ + self.track_new = ( + track_new + if track_new is not None else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) + ) self.defaults = defaults self.group = None self._is_updating = asyncio.Lock() for dev in devices: if self.devices[dev.dev_id] is not dev: - LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id) + LOGGER.warning("Duplicate device IDs detected %s", dev.dev_id) if dev.mac and self.mac_to_dev[dev.mac] is not dev: - LOGGER.warning('Duplicate device MAC addresses detected %s', - dev.mac) + LOGGER.warning("Duplicate device MAC addresses detected %s", dev.mac) - def see(self, mac: str = None, dev_id: str = None, host_name: str = None, - location_name: str = None, gps: GPSType = None, - gps_accuracy: int = None, battery: int = None, - attributes: dict = None, source_type: str = SOURCE_TYPE_GPS, - picture: str = None, icon: str = None, - consider_home: timedelta = None): + def see( + self, + mac: str = None, + dev_id: str = None, + host_name: str = None, + location_name: str = None, + gps: GPSType = None, + gps_accuracy: int = None, + battery: int = None, + attributes: dict = None, + source_type: str = SOURCE_TYPE_GPS, + picture: str = None, + icon: str = None, + consider_home: timedelta = None, + ): """Notify the device tracker that you see a device.""" self.hass.add_job( - self.async_see(mac, dev_id, host_name, location_name, gps, - gps_accuracy, battery, attributes, source_type, - picture, icon, consider_home) + self.async_see( + mac, + dev_id, + host_name, + location_name, + gps, + gps_accuracy, + battery, + attributes, + source_type, + picture, + icon, + consider_home, + ) ) async def async_see( - self, mac: str = None, dev_id: str = None, host_name: str = None, - location_name: str = None, gps: GPSType = None, - gps_accuracy: int = None, battery: int = None, - attributes: dict = None, source_type: str = SOURCE_TYPE_GPS, - picture: str = None, icon: str = None, - consider_home: timedelta = None): + self, + mac: str = None, + dev_id: str = None, + host_name: str = None, + location_name: str = None, + gps: GPSType = None, + gps_accuracy: int = None, + battery: int = None, + attributes: dict = None, + source_type: str = SOURCE_TYPE_GPS, + picture: str = None, + icon: str = None, + consider_home: timedelta = None, + ): """Notify the device tracker that you see a device. This method is a coroutine. """ registry = await async_get_registry(self.hass) if mac is None and dev_id is None: - raise HomeAssistantError('Neither mac or device id passed in') + raise HomeAssistantError("Neither mac or device id passed in") if mac is not None: mac = str(mac).upper() device = self.mac_to_dev.get(mac) if not device: - dev_id = util.slugify(host_name or '') or util.slugify(mac) + dev_id = util.slugify(host_name or "") or util.slugify(mac) else: dev_id = cv.slug(str(dev_id).lower()) device = self.devices.get(dev_id) if device: await device.async_seen( - host_name, location_name, gps, gps_accuracy, battery, - attributes, source_type, consider_home) + host_name, + location_name, + gps, + gps_accuracy, + battery, + attributes, + source_type, + consider_home, + ) if device.track: await device.async_update_ha_state() return @@ -140,24 +196,36 @@ class DeviceTracker: entity_id = ENTITY_ID_FORMAT.format(dev_id) if registry.async_is_registered(entity_id): LOGGER.error( - "The see service is not supported for this entity %s", - entity_id) + "The see service is not supported for this entity %s", entity_id + ) return # If no device can be found, create it dev_id = util.ensure_unique_string(dev_id, self.devices.keys()) device = Device( - self.hass, consider_home or self.consider_home, self.track_new, - dev_id, mac, (host_name or dev_id).replace('_', ' '), - picture=picture, icon=icon, - hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) + self.hass, + consider_home or self.consider_home, + self.track_new, + dev_id, + mac, + (host_name or dev_id).replace("_", " "), + picture=picture, + icon=icon, + hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE), + ) self.devices[dev_id] = device if mac is not None: self.mac_to_dev[mac] = device await device.async_seen( - host_name, location_name, gps, gps_accuracy, battery, attributes, - source_type) + host_name, + location_name, + gps, + gps_accuracy, + battery, + attributes, + source_type, + ) if device.track: await device.async_update_ha_state() @@ -166,22 +234,31 @@ class DeviceTracker: if self.group and self.track_new: self.hass.async_create_task( self.hass.async_call( - DOMAIN_GROUP, SERVICE_SET, { + DOMAIN_GROUP, + SERVICE_SET, + { ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), ATTR_VISIBLE: False, ATTR_NAME: GROUP_NAME_ALL_DEVICES, - ATTR_ADD_ENTITIES: [device.entity_id]})) + ATTR_ADD_ENTITIES: [device.entity_id], + }, + ) + ) - self.hass.bus.async_fire(EVENT_NEW_DEVICE, { - ATTR_ENTITY_ID: device.entity_id, - ATTR_HOST_NAME: device.host_name, - ATTR_MAC: device.mac, - }) + self.hass.bus.async_fire( + EVENT_NEW_DEVICE, + { + ATTR_ENTITY_ID: device.entity_id, + ATTR_HOST_NAME: device.host_name, + ATTR_MAC: device.mac, + }, + ) # update known_devices.yaml self.hass.async_create_task( self.async_update_config( - self.hass.config.path(YAML_DEVICES), dev_id, device) + self.hass.config.path(YAML_DEVICES), dev_id, device + ) ) async def async_update_config(self, path, dev_id, device): @@ -191,8 +268,8 @@ class DeviceTracker: """ async with self._is_updating: await self.hass.async_add_executor_job( - update_config, self.hass.config.path(YAML_DEVICES), - dev_id, device) + update_config, self.hass.config.path(YAML_DEVICES), dev_id, device + ) @callback def async_setup_group(self): @@ -200,16 +277,20 @@ class DeviceTracker: This method must be run in the event loop. """ - entity_ids = [dev.entity_id for dev in self.devices.values() - if dev.track] + entity_ids = [dev.entity_id for dev in self.devices.values() if dev.track] self.hass.async_create_task( self.hass.services.async_call( - DOMAIN_GROUP, SERVICE_SET, { + DOMAIN_GROUP, + SERVICE_SET, + { ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), ATTR_VISIBLE: False, ATTR_NAME: GROUP_NAME_ALL_DEVICES, - ATTR_ENTITIES: entity_ids})) + ATTR_ENTITIES: entity_ids, + }, + ) + ) @callback def async_update_stale(self, now: dt_util.dt.datetime): @@ -218,8 +299,7 @@ class DeviceTracker: This method must be run in the event loop. """ for device in self.devices.values(): - if (device.track and device.last_update_home) and \ - device.stale(now): + if (device.track and device.last_update_home) and device.stale(now): self.hass.async_create_task(device.async_update_ha_state(True)) async def async_setup_tracked_device(self): @@ -227,6 +307,7 @@ class DeviceTracker: This method is a coroutine. """ + async def async_init_single_device(dev): """Init a single device_tracker entity.""" await dev.async_added_to_hass() @@ -235,8 +316,9 @@ class DeviceTracker: tasks = [] for device in self.devices.values(): if device.track and not device.last_seen: - tasks.append(self.hass.async_create_task( - async_init_single_device(device))) + tasks.append( + self.hass.async_create_task(async_init_single_device(device)) + ) if tasks: await asyncio.wait(tasks) @@ -259,10 +341,19 @@ class Device(RestoreEntity): last_update_home = False _state = STATE_NOT_HOME - def __init__(self, hass: HomeAssistantType, consider_home: timedelta, - track: bool, dev_id: str, mac: str, name: str = None, - picture: str = None, gravatar: str = None, icon: str = None, - hide_if_away: bool = False) -> None: + def __init__( + self, + hass: HomeAssistantType, + consider_home: timedelta, + track: bool, + dev_id: str, + mac: str, + name: str = None, + picture: str = None, + gravatar: str = None, + icon: str = None, + hide_if_away: bool = False, + ) -> None: """Initialize a device.""" self.hass = hass self.entity_id = ENTITY_ID_FORMAT.format(dev_id) @@ -313,9 +404,7 @@ class Device(RestoreEntity): @property def state_attributes(self): """Return the device state attributes.""" - attr = { - ATTR_SOURCE_TYPE: self.source_type - } + attr = {ATTR_SOURCE_TYPE: self.source_type} if self.gps: attr[ATTR_LATITUDE] = self.gps[0] @@ -338,11 +427,16 @@ class Device(RestoreEntity): return self.away_hide and self.state != STATE_HOME async def async_seen( - self, host_name: str = None, location_name: str = None, - gps: GPSType = None, gps_accuracy=0, battery: int = None, - attributes: dict = None, - source_type: str = SOURCE_TYPE_GPS, - consider_home: timedelta = None): + self, + host_name: str = None, + location_name: str = None, + gps: GPSType = None, + gps_accuracy=0, + battery: int = None, + attributes: dict = None, + source_type: str = SOURCE_TYPE_GPS, + consider_home: timedelta = None, + ): """Mark the device as seen.""" self.source_type = source_type self.last_seen = dt_util.utcnow() @@ -364,8 +458,7 @@ class Device(RestoreEntity): except (ValueError, TypeError, IndexError): self.gps = None self.gps_accuracy = 0 - LOGGER.warning( - "Could not parse gps value for %s: %s", self.dev_id, gps) + LOGGER.warning("Could not parse gps value for %s: %s", self.dev_id, gps) # pylint: disable=not-an-iterable await self.async_update() @@ -375,8 +468,10 @@ class Device(RestoreEntity): Async friendly. """ - return self.last_seen is None or \ - (now or dt_util.utcnow()) - self.last_seen > self.consider_home + return ( + self.last_seen is None + or (now or dt_util.utcnow()) - self.last_seen > self.consider_home + ) def mark_stale(self): """Mark the device state as stale.""" @@ -395,7 +490,8 @@ class Device(RestoreEntity): self._state = self.location_name elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: zone_state = async_active_zone( - self.hass, self.gps[0], self.gps[1], self.gps_accuracy) + self.hass, self.gps[0], self.gps[1], self.gps_accuracy + ) if zone_state is None: self._state = STATE_NOT_HOME elif zone_state.entity_id == zone.ENTITY_ID_HOME: @@ -415,20 +511,22 @@ class Device(RestoreEntity): if not state: return self._state = state.state - self.last_update_home = (state.state == STATE_HOME) + self.last_update_home = state.state == STATE_HOME self.last_seen = dt_util.utcnow() for attr, var in ( - (ATTR_SOURCE_TYPE, 'source_type'), - (ATTR_GPS_ACCURACY, 'gps_accuracy'), - (ATTR_BATTERY, 'battery'), + (ATTR_SOURCE_TYPE, "source_type"), + (ATTR_GPS_ACCURACY, "gps_accuracy"), + (ATTR_BATTERY, "battery"), ): if attr in state.attributes: setattr(self, var, state.attributes[attr]) if ATTR_LONGITUDE in state.attributes: - self.gps = (state.attributes[ATTR_LATITUDE], - state.attributes[ATTR_LONGITUDE]) + self.gps = ( + state.attributes[ATTR_LATITUDE], + state.attributes[ATTR_LONGITUDE], + ) class DeviceScanner: @@ -470,28 +568,32 @@ class DeviceScanner: return self.hass.async_add_job(self.get_extra_attributes, device) -async def async_load_config(path: str, hass: HomeAssistantType, - consider_home: timedelta): +async def async_load_config( + path: str, hass: HomeAssistantType, consider_home: timedelta +): """Load devices from YAML configuration file. This method is a coroutine. """ - dev_schema = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon), - vol.Optional('track', default=False): cv.boolean, - vol.Optional(CONF_MAC, default=None): - vol.Any(None, vol.All(cv.string, vol.Upper)), - vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, - vol.Optional('gravatar', default=None): vol.Any(None, cv.string), - vol.Optional('picture', default=None): vol.Any(None, cv.string), - vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( - cv.time_period, cv.positive_timedelta), - }) + dev_schema = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon), + vol.Optional("track", default=False): cv.boolean, + vol.Optional(CONF_MAC, default=None): vol.Any( + None, vol.All(cv.string, vol.Upper) + ), + vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, + vol.Optional("gravatar", default=None): vol.Any(None, cv.string), + vol.Optional("picture", default=None): vol.Any(None, cv.string), + vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( + cv.time_period, cv.positive_timedelta + ), + } + ) result = [] try: - devices = await hass.async_add_job( - load_yaml_config_file, path) + devices = await hass.async_add_job(load_yaml_config_file, path) except HomeAssistantError as err: LOGGER.error("Unable to load %s: %s", path, str(err)) return [] @@ -500,10 +602,10 @@ async def async_load_config(path: str, hass: HomeAssistantType, for dev_id, device in devices.items(): # Deprecated option. We just ignore it to avoid breaking change - device.pop('vendor', None) + device.pop("vendor", None) try: device = dev_schema(device) - device['dev_id'] = cv.slugify(dev_id) + device["dev_id"] = cv.slugify(dev_id) except vol.Invalid as exp: async_log_exception(exp, dev_id, devices, hass) else: @@ -513,16 +615,18 @@ async def async_load_config(path: str, hass: HomeAssistantType, def update_config(path: str, dev_id: str, device: Device): """Add device to YAML configuration file.""" - with open(path, 'a') as out: - device = {device.dev_id: { - ATTR_NAME: device.name, - ATTR_MAC: device.mac, - ATTR_ICON: device.icon, - 'picture': device.config_picture, - 'track': device.track, - CONF_AWAY_HIDE: device.away_hide, - }} - out.write('\n') + with open(path, "a") as out: + device = { + device.dev_id: { + ATTR_NAME: device.name, + ATTR_MAC: device.mac, + ATTR_ICON: device.icon, + "picture": device.config_picture, + "track": device.track, + CONF_AWAY_HIDE: device.away_hide, + } + } + out.write("\n") out.write(dump(device)) @@ -532,5 +636,6 @@ def get_gravatar_for_email(email: str): Async friendly. """ import hashlib - url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar' - return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest()) + + url = "https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar" + return url.format(hashlib.md5(email.encode("utf-8").lower()).hexdigest()) diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py index a74f51c6638..e6edb5f63ac 100644 --- a/homeassistant/components/device_tracker/setup.py +++ b/homeassistant/components/device_tracker/setup.py @@ -12,10 +12,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util -from homeassistant.const import ( - ATTR_LATITUDE, - ATTR_LONGITUDE, -) +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from .const import ( @@ -33,10 +30,10 @@ class DeviceTrackerPlatform: """Class to hold platform information.""" LEGACY_SETUP = ( - 'async_get_scanner', - 'get_scanner', - 'async_setup_scanner', - 'setup_scanner', + "async_get_scanner", + "get_scanner", + "async_setup_scanner", + "setup_scanner", ) name = attr.ib(type=str) @@ -46,9 +43,7 @@ class DeviceTrackerPlatform: @property def type(self): """Return platform type.""" - for methods, platform_type in ( - (self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY), - ): + for methods, platform_type in ((self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),): for meth in methods: if hasattr(self.platform, meth): return platform_type @@ -61,26 +56,33 @@ class DeviceTrackerPlatform: try: scanner = None setup = None - if hasattr(self.platform, 'async_get_scanner'): + if hasattr(self.platform, "async_get_scanner"): scanner = await self.platform.async_get_scanner( - hass, {DOMAIN: self.config}) - elif hasattr(self.platform, 'get_scanner'): + hass, {DOMAIN: self.config} + ) + elif hasattr(self.platform, "get_scanner"): scanner = await hass.async_add_job( - self.platform.get_scanner, hass, {DOMAIN: self.config}) - elif hasattr(self.platform, 'async_setup_scanner'): + self.platform.get_scanner, hass, {DOMAIN: self.config} + ) + elif hasattr(self.platform, "async_setup_scanner"): setup = await self.platform.async_setup_scanner( - hass, self.config, tracker.async_see, discovery_info) - elif hasattr(self.platform, 'setup_scanner'): + hass, self.config, tracker.async_see, discovery_info + ) + elif hasattr(self.platform, "setup_scanner"): setup = await hass.async_add_job( - self.platform.setup_scanner, hass, self.config, - tracker.see, discovery_info) + self.platform.setup_scanner, + hass, + self.config, + tracker.see, + discovery_info, + ) else: - raise HomeAssistantError( - "Invalid legacy device_tracker platform.") + raise HomeAssistantError("Invalid legacy device_tracker platform.") if scanner: async_setup_scanner_platform( - hass, self.config, scanner, tracker.async_see, self.type) + hass, self.config, scanner, tracker.async_see, self.type + ) return if not setup: @@ -95,27 +97,32 @@ async def async_extract_config(hass, config): """Extract device tracker config and split between legacy and modern.""" legacy = [] - for platform in await asyncio.gather(*[ + for platform in await asyncio.gather( + *( async_create_platform_type(hass, config, p_type, p_config) for p_type, p_config in config_per_platform(config, DOMAIN) - ]): + ) + ): if platform is None: continue if platform.type == PLATFORM_TYPE_LEGACY: legacy.append(platform) else: - raise ValueError("Unable to determine type for {}: {}".format( - platform.name, platform.type)) + raise ValueError( + "Unable to determine type for {}: {}".format( + platform.name, platform.type + ) + ) return legacy -async def async_create_platform_type(hass, config, p_type, p_config) \ - -> Optional[DeviceTrackerPlatform]: +async def async_create_platform_type( + hass, config, p_type, p_config +) -> Optional[DeviceTrackerPlatform]: """Determine type of platform.""" - platform = await async_prepare_setup_platform( - hass, config, DOMAIN, p_type) + platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) if platform is None: return None @@ -124,9 +131,13 @@ async def async_create_platform_type(hass, config, p_type, p_config) \ @callback -def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, - scanner: Any, async_see_device: Callable, - platform: str): +def async_setup_scanner_platform( + hass: HomeAssistantType, + config: ConfigType, + scanner: Any, + async_see_device: Callable, + platform: str, +): """Set up the connect scanner-based platform to device tracker. This method must be run in the event loop. @@ -143,7 +154,10 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, if update_lock.locked(): LOGGER.warning( "Updating device list from %s took longer than the scheduled " - "scan interval %s", platform, interval) + "scan interval %s", + platform, + interval, + ) return async with update_lock: @@ -157,26 +171,27 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, seen.add(mac) try: - extra_attributes = \ - await scanner.async_get_extra_attributes(mac) + extra_attributes = await scanner.async_get_extra_attributes(mac) except NotImplementedError: extra_attributes = dict() kwargs = { - 'mac': mac, - 'host_name': host_name, - 'source_type': SOURCE_TYPE_ROUTER, - 'attributes': { - 'scanner': scanner.__class__.__name__, - **extra_attributes - } + "mac": mac, + "host_name": host_name, + "source_type": SOURCE_TYPE_ROUTER, + "attributes": { + "scanner": scanner.__class__.__name__, + **extra_attributes, + }, } zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME) if zone_home: - kwargs['gps'] = [zone_home.attributes[ATTR_LATITUDE], - zone_home.attributes[ATTR_LONGITUDE]] - kwargs['gps_accuracy'] = 0 + kwargs["gps"] = [ + zone_home.attributes[ATTR_LATITUDE], + zone_home.attributes[ATTR_LONGITUDE], + ] + kwargs["gps_accuracy"] = 0 hass.async_create_task(async_see_device(**kwargs)) diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index d544bfa74e8..6ea5e7a46a2 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -6,42 +6,46 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS) +from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit _LOGGER = logging.getLogger(__name__) -CONF_PIN = 'pin' -CONF_SENSOR = 'sensor' -CONF_HUMIDITY_OFFSET = 'humidity_offset' -CONF_TEMPERATURE_OFFSET = 'temperature_offset' +CONF_PIN = "pin" +CONF_SENSOR = "sensor" +CONF_HUMIDITY_OFFSET = "humidity_offset" +CONF_TEMPERATURE_OFFSET = "temperature_offset" -DEFAULT_NAME = 'DHT Sensor' +DEFAULT_NAME = "DHT Sensor" # DHT11 is able to deliver data once per second, DHT22 once every two MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) -SENSOR_TEMPERATURE = 'temperature' -SENSOR_HUMIDITY = 'humidity' +SENSOR_TEMPERATURE = "temperature" +SENSOR_HUMIDITY = "humidity" SENSOR_TYPES = { - SENSOR_TEMPERATURE: ['Temperature', None], - SENSOR_HUMIDITY: ['Humidity', '%'] + SENSOR_TEMPERATURE: ["Temperature", None], + SENSOR_HUMIDITY: ["Humidity", "%"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSOR): cv.string, - vol.Required(CONF_PIN): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TEMPERATURE_OFFSET, default=0): - vol.All(vol.Coerce(float), vol.Range(min=-100, max=100)), - vol.Optional(CONF_HUMIDITY_OFFSET, default=0): - vol.All(vol.Coerce(float), vol.Range(min=-100, max=100)) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_SENSOR): cv.string, + vol.Required(CONF_PIN): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TEMPERATURE_OFFSET, default=0): vol.All( + vol.Coerce(float), vol.Range(min=-100, max=100) + ), + vol.Optional(CONF_HUMIDITY_OFFSET, default=0): vol.All( + vol.Coerce(float), vol.Range(min=-100, max=100) + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -69,9 +73,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: for variable in config[CONF_MONITORED_CONDITIONS]: - dev.append(DHTSensor( - data, variable, SENSOR_TYPES[variable][1], name, - temperature_offset, humidity_offset)) + dev.append( + DHTSensor( + data, + variable, + SENSOR_TYPES[variable][1], + name, + temperature_offset, + humidity_offset, + ) + ) except KeyError: pass @@ -81,8 +92,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DHTSensor(Entity): """Implementation of the DHT sensor.""" - def __init__(self, dht_client, sensor_type, temp_unit, name, - temperature_offset, humidity_offset): + def __init__( + self, + dht_client, + sensor_type, + temp_unit, + name, + temperature_offset, + humidity_offset, + ): """Initialize the sensor.""" self.client_name = name self._name = SENSOR_TYPES[sensor_type][0] @@ -97,7 +115,7 @@ class DHTSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self.client_name, self._name) + return "{} {}".format(self.client_name, self._name) @property def state(self): @@ -118,16 +136,18 @@ class DHTSensor(Entity): if self.type == SENSOR_TEMPERATURE and SENSOR_TEMPERATURE in data: temperature = data[SENSOR_TEMPERATURE] - _LOGGER.debug("Temperature %.1f \u00b0C + offset %.1f", - temperature, temperature_offset) + _LOGGER.debug( + "Temperature %.1f \u00b0C + offset %.1f", + temperature, + temperature_offset, + ) if -20 <= temperature < 80: self._state = round(temperature + temperature_offset, 1) if self.temp_unit == TEMP_FAHRENHEIT: self._state = round(celsius_to_fahrenheit(temperature), 1) elif self.type == SENSOR_HUMIDITY and SENSOR_HUMIDITY in data: humidity = data[SENSOR_HUMIDITY] - _LOGGER.debug("Humidity %.1f%% + offset %.1f", - humidity, humidity_offset) + _LOGGER.debug("Humidity %.1f%% + offset %.1f", humidity, humidity_offset) if 0 <= humidity <= 100: self._state = round(humidity + humidity_offset, 1) @@ -145,8 +165,7 @@ class DHTClient: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data the DHT sensor.""" - humidity, temperature = self.adafruit_dht.read_retry( - self.sensor, self.pin) + humidity, temperature = self.adafruit_dht.read_retry(self.sensor, self.pin) if temperature: self.data[SENSOR_TEMPERATURE] = temperature if humidity: diff --git a/homeassistant/components/dialogflow/.translations/bg.json b/homeassistant/components/dialogflow/.translations/bg.json index 6f06d5c00c6..af1237ce211 100644 --- a/homeassistant/components/dialogflow/.translations/bg.json +++ b/homeassistant/components/dialogflow/.translations/bg.json @@ -1,7 +1,18 @@ { "config": { "abort": { + "not_internet_accessible": "Home Assistant \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u0435\u043d \u043e\u0442 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0437\u0430 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430 \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e\u0442 Dialogflow.", "one_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." - } + }, + "create_entry": { + "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 [\u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 Dialogflow]({dialogflow_url}). \n\n \u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: ` {webhook_url} ` \n - Method: POST\n - Content Type: application/json\n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." + }, + "step": { + "user": { + "description": "\u0421\u0438\u0433\u0443\u0440\u043d\u0438 \u043b\u0438 \u0441\u0442\u0435, \u0447\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Dialogflow?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435 \u043d\u0430 Dialogflow Webhook" + } + }, + "title": "Dialogflow" } } \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/ko.json b/homeassistant/components/dialogflow/.translations/ko.json index 33c465bf0e7..91f15f1fb77 100644 --- a/homeassistant/components/dialogflow/.translations/ko.json +++ b/homeassistant/components/dialogflow/.translations/ko.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow Webhook]({dialogflow_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow Webhook]({dialogflow_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/pt-BR.json b/homeassistant/components/dialogflow/.translations/pt-BR.json new file mode 100644 index 00000000000..6d709875771 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel na Internet para receber mensagens da Dialogflow.", + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar [Integra\u00e7\u00e3o do webhook da Dialogflow] ( {dialogflow_url} ). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application / json \n\n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) para mais detalhes." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o Dialogflow?", + "title": "Configurar o Dialogflow Webhook" + } + }, + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index 3bf11a46098..4f2876df296 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -15,9 +15,7 @@ _LOGGER = logging.getLogger(__name__) SOURCE = "Home Assistant Dialogflow" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: {} -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA) class DialogFlowError(HomeAssistantError): @@ -37,7 +35,7 @@ async def handle_webhook(hass, webhook_id, request): try: response = await async_handle_message(hass, message) - return b'' if response is None else web.json_response(response) + return b"" if response is None else web.json_response(response) except DialogFlowError as err: _LOGGER.warning(str(err)) @@ -47,8 +45,7 @@ async def handle_webhook(hass, webhook_id, request): _LOGGER.warning(str(err)) return web.json_response( dialogflow_error_response( - message, - "This intent is not yet configured within Home Assistant." + message, "This intent is not yet configured within Home Assistant." ) ) @@ -56,21 +53,22 @@ async def handle_webhook(hass, webhook_id, request): _LOGGER.warning(str(err)) return web.json_response( dialogflow_error_response( - message, - "Invalid slot information received for this intent." + message, "Invalid slot information received for this intent." ) ) except intent.IntentError as err: _LOGGER.warning(str(err)) return web.json_response( - dialogflow_error_response(message, "Error handling intent.")) + dialogflow_error_response(message, "Error handling intent.") + ) async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( - DOMAIN, 'DialogFlow', entry.data[CONF_WEBHOOK_ID], handle_webhook) + DOMAIN, "DialogFlow", entry.data[CONF_WEBHOOK_ID], handle_webhook + ) return True @@ -86,36 +84,38 @@ async_remove_entry = config_entry_flow.webhook_async_remove_entry def dialogflow_error_response(message, error): """Return a response saying the error message.""" - dialogflow_response = DialogflowResponse(message['result']['parameters']) + dialogflow_response = DialogflowResponse(message["result"]["parameters"]) dialogflow_response.add_speech(error) return dialogflow_response.as_dict() async def async_handle_message(hass, message): """Handle a DialogFlow message.""" - req = message.get('result') - action_incomplete = req['actionIncomplete'] + req = message.get("result") + action_incomplete = req["actionIncomplete"] if action_incomplete: return None - action = req.get('action', '') - parameters = req.get('parameters').copy() + action = req.get("action", "") + parameters = req.get("parameters").copy() parameters["dialogflow_query"] = message dialogflow_response = DialogflowResponse(parameters) if action == "": raise DialogFlowError( - "You have not defined an action in your Dialogflow intent.") + "You have not defined an action in your Dialogflow intent." + ) intent_response = await intent.async_handle( - hass, DOMAIN, action, - {key: {'value': value} for key, value - in parameters.items()}) + hass, + DOMAIN, + action, + {key: {"value": value} for key, value in parameters.items()}, + ) - if 'plain' in intent_response.speech: - dialogflow_response.add_speech( - intent_response.speech['plain']['speech']) + if "plain" in intent_response.speech: + dialogflow_response.add_speech(intent_response.speech["plain"]["speech"]) return dialogflow_response.as_dict() @@ -129,7 +129,7 @@ class DialogflowResponse: self.parameters = {} # Parameter names replace '.' and '-' for '_' for key, value in parameters.items(): - underscored_key = key.replace('.', '_').replace('-', '_') + underscored_key = key.replace(".", "_").replace("-", "_") self.parameters[underscored_key] = value def add_speech(self, text): @@ -143,8 +143,4 @@ class DialogflowResponse: def as_dict(self): """Return response in a Dialogflow valid dictionary.""" - return { - 'speech': self.speech, - 'displayText': self.speech, - 'source': SOURCE, - } + return {"speech": self.speech, "displayText": self.speech, "source": SOURCE} diff --git a/homeassistant/components/dialogflow/config_flow.py b/homeassistant/components/dialogflow/config_flow.py index aa6f9f6f515..8e5be873f72 100644 --- a/homeassistant/components/dialogflow/config_flow.py +++ b/homeassistant/components/dialogflow/config_flow.py @@ -5,9 +5,9 @@ from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, - 'Dialogflow Webhook', + "Dialogflow Webhook", { - 'dialogflow_url': 'https://dialogflow.com/docs/fulfillment#webhook', - 'docs_url': 'https://www.home-assistant.io/components/dialogflow/' - } + "dialogflow_url": "https://dialogflow.com/docs/fulfillment#webhook", + "docs_url": "https://www.home-assistant.io/components/dialogflow/", + }, ) diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py index 9e034b2428d..18dfb49365a 100644 --- a/homeassistant/components/digital_ocean/__init__.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -10,31 +10,30 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_CREATED_AT = 'created_at' -ATTR_DROPLET_ID = 'droplet_id' -ATTR_DROPLET_NAME = 'droplet_name' -ATTR_FEATURES = 'features' -ATTR_IPV4_ADDRESS = 'ipv4_address' -ATTR_IPV6_ADDRESS = 'ipv6_address' -ATTR_MEMORY = 'memory' -ATTR_REGION = 'region' -ATTR_VCPUS = 'vcpus' +ATTR_CREATED_AT = "created_at" +ATTR_DROPLET_ID = "droplet_id" +ATTR_DROPLET_NAME = "droplet_name" +ATTR_FEATURES = "features" +ATTR_IPV4_ADDRESS = "ipv4_address" +ATTR_IPV6_ADDRESS = "ipv6_address" +ATTR_MEMORY = "memory" +ATTR_REGION = "region" +ATTR_VCPUS = "vcpus" -ATTRIBUTION = 'Data provided by Digital Ocean' +ATTRIBUTION = "Data provided by Digital Ocean" -CONF_DROPLETS = 'droplets' +CONF_DROPLETS = "droplets" -DATA_DIGITAL_OCEAN = 'data_do' -DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor'] -DOMAIN = 'digital_ocean' +DATA_DIGITAL_OCEAN = "data_do" +DIGITAL_OCEAN_PLATFORMS = ["switch", "binary_sensor"] +DOMAIN = "digital_ocean" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})}, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index 83406247a07..50c87907774 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -3,23 +3,32 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv from . import ( - ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, ATTR_FEATURES, - ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, ATTR_REGION, ATTR_VCPUS, - ATTRIBUTION, CONF_DROPLETS, DATA_DIGITAL_OCEAN) + ATTR_CREATED_AT, + ATTR_DROPLET_ID, + ATTR_DROPLET_NAME, + ATTR_FEATURES, + ATTR_IPV4_ADDRESS, + ATTR_IPV6_ADDRESS, + ATTR_MEMORY, + ATTR_REGION, + ATTR_VCPUS, + ATTRIBUTION, + CONF_DROPLETS, + DATA_DIGITAL_OCEAN, +) _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Droplet' -DEFAULT_DEVICE_CLASS = 'moving' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]), -}) +DEFAULT_NAME = "Droplet" +DEFAULT_DEVICE_CLASS = "moving" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -59,7 +68,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice): @property def is_on(self): """Return true if the binary sensor is on.""" - return self.data.status == 'active' + return self.data.status == "active" @property def device_class(self): @@ -78,7 +87,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice): ATTR_IPV4_ADDRESS: self.data.ip_address, ATTR_IPV6_ADDRESS: self.data.ip_v6_address, ATTR_MEMORY: self.data.memory, - ATTR_REGION: self.data.region['name'], + ATTR_REGION: self.data.region["name"], ATTR_VCPUS: self.data.vcpus, } diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index 8016ccef0ea..95d2e15a510 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -8,17 +8,27 @@ from homeassistant.const import ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv from . import ( - ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, ATTR_FEATURES, - ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, ATTR_REGION, ATTR_VCPUS, - ATTRIBUTION, CONF_DROPLETS, DATA_DIGITAL_OCEAN) + ATTR_CREATED_AT, + ATTR_DROPLET_ID, + ATTR_DROPLET_NAME, + ATTR_FEATURES, + ATTR_IPV4_ADDRESS, + ATTR_IPV6_ADDRESS, + ATTR_MEMORY, + ATTR_REGION, + ATTR_VCPUS, + ATTRIBUTION, + CONF_DROPLETS, + DATA_DIGITAL_OCEAN, +) _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Droplet' +DEFAULT_NAME = "Droplet" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -58,7 +68,7 @@ class DigitalOceanSwitch(SwitchDevice): @property def is_on(self): """Return true if switch is on.""" - return self.data.status == 'active' + return self.data.status == "active" @property def device_state_attributes(self): @@ -72,18 +82,18 @@ class DigitalOceanSwitch(SwitchDevice): ATTR_IPV4_ADDRESS: self.data.ip_address, ATTR_IPV6_ADDRESS: self.data.ip_v6_address, ATTR_MEMORY: self.data.memory, - ATTR_REGION: self.data.region['name'], + ATTR_REGION: self.data.region["name"], ATTR_VCPUS: self.data.vcpus, } def turn_on(self, **kwargs): """Boot-up the droplet.""" - if self.data.status != 'active': + if self.data.status != "active": self.data.power_on() def turn_off(self, **kwargs): """Shutdown the droplet.""" - if self.data.status == 'active': + if self.data.status == "active": self.data.power_off() def update(self): diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py index 4d1a87c44f9..d80385d0f54 100644 --- a/homeassistant/components/digitalloggers/switch.py +++ b/homeassistant/components/digitalloggers/switch.py @@ -4,34 +4,43 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT) + CONF_HOST, + CONF_NAME, + CONF_USERNAME, + CONF_PASSWORD, + CONF_TIMEOUT, +) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -CONF_CYCLETIME = 'cycletime' +CONF_CYCLETIME = "cycletime" -DEFAULT_NAME = 'DINRelay' -DEFAULT_USERNAME = 'admin' -DEFAULT_PASSWORD = 'admin' +DEFAULT_NAME = "DINRelay" +DEFAULT_USERNAME = "admin" +DEFAULT_PASSWORD = "admin" DEFAULT_TIMEOUT = 20 DEFAULT_CYCLETIME = 2 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): - vol.All(vol.Coerce(int), vol.Range(min=1, max=600)), - vol.Optional(CONF_CYCLETIME, default=DEFAULT_CYCLETIME): - vol.All(vol.Coerce(int), vol.Range(min=1, max=600)), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=600) + ), + vol.Optional(CONF_CYCLETIME, default=DEFAULT_CYCLETIME): vol.All( + vol.Coerce(int), vol.Range(min=1, max=600) + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -46,8 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): cycl = config.get(CONF_CYCLETIME) power_switch = dlipower.PowerSwitch( - hostname=host, userid=user, password=pswd, - timeout=tout, cycletime=cycl + hostname=host, userid=user, password=pswd, timeout=tout, cycletime=cycl ) if not power_switch.verify(): @@ -58,8 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): parent_device = DINRelayDevice(power_switch) outlets.extend( - DINRelay(controller_name, parent_device, outlet) - for outlet in power_switch[0:] + DINRelay(controller_name, parent_device, outlet) for outlet in power_switch[0:] ) add_entities(outlets) @@ -76,15 +83,12 @@ class DINRelay(SwitchDevice): self._outlet_number = self._outlet.outlet_number self._name = self._outlet.description - self._state = self._outlet.state == 'ON' + self._state = self._outlet.state == "ON" @property def name(self): """Return the display name of this relay.""" - return '{}_{}'.format( - self._controller_name, - self._name - ) + return "{}_{}".format(self._controller_name, self._name) @property def is_on(self): @@ -108,11 +112,10 @@ class DINRelay(SwitchDevice): """Trigger update for all switches on the parent device.""" self._parent_device.update() - outlet_status = self._parent_device.get_outlet_status( - self._outlet_number) + outlet_status = self._parent_device.get_outlet_status(self._outlet_number) self._name = outlet_status[1] - self._state = outlet_status[2] == 'ON' + self._state = outlet_status[2] == "ON" class DINRelayDevice: diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index aaffd44d572..2be2544cec1 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -3,45 +3,73 @@ import logging import requests import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, MEDIA_TYPE_TVSHOW, - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) + MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_TVSHOW, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) from homeassistant.const import ( - CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, - STATE_PLAYING) + CONF_DEVICE, + CONF_HOST, + CONF_NAME, + CONF_PORT, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -ATTR_MEDIA_CURRENTLY_RECORDING = 'media_currently_recording' -ATTR_MEDIA_RATING = 'media_rating' -ATTR_MEDIA_RECORDED = 'media_recorded' -ATTR_MEDIA_START_TIME = 'media_start_time' +ATTR_MEDIA_CURRENTLY_RECORDING = "media_currently_recording" +ATTR_MEDIA_RATING = "media_rating" +ATTR_MEDIA_RECORDED = "media_recorded" +ATTR_MEDIA_START_TIME = "media_start_time" -DEFAULT_DEVICE = '0' +DEFAULT_DEVICE = "0" DEFAULT_NAME = "DirecTV Receiver" DEFAULT_PORT = 8080 -SUPPORT_DTV = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY +SUPPORT_DTV = ( + SUPPORT_PAUSE + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_NEXT_TRACK + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_PLAY +) -SUPPORT_DTV_CLIENT = SUPPORT_PAUSE | \ - SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY +SUPPORT_DTV_CLIENT = ( + SUPPORT_PAUSE + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_NEXT_TRACK + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_PLAY +) -DATA_DIRECTV = 'data_directv' +DATA_DIRECTV = "data_directv" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -50,21 +78,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hosts = [] if CONF_HOST in config: - _LOGGER.debug("Adding configured device %s with client address %s ", - config.get(CONF_NAME), config.get(CONF_DEVICE)) - hosts.append([ - config.get(CONF_NAME), config.get(CONF_HOST), - config.get(CONF_PORT), config.get(CONF_DEVICE) - ]) + _LOGGER.debug( + "Adding configured device %s with client address %s ", + config.get(CONF_NAME), + config.get(CONF_DEVICE), + ) + hosts.append( + [ + config.get(CONF_NAME), + config.get(CONF_HOST), + config.get(CONF_PORT), + config.get(CONF_DEVICE), + ] + ) elif discovery_info: - host = discovery_info.get('host') - name = 'DirecTV_{}'.format(discovery_info.get('serial', '')) + host = discovery_info.get("host") + name = "DirecTV_{}".format(discovery_info.get("serial", "")) # Attempt to discover additional RVU units _LOGGER.debug("Doing discovery of DirecTV devices on %s", host) from DirectPy import DIRECTV + dtv = DIRECTV(host, DEFAULT_PORT) try: resp = dtv.get_locations() @@ -73,12 +109,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Make sure that this device is not already configured # Comparing based on host (IP) and clientAddr. _LOGGER.debug("Request exception %s trying to get locations", ex) - resp = { - 'locations': [{ - 'locationName': name, - 'clientAddr': DEFAULT_DEVICE - }] - } + resp = {"locations": [{"locationName": name, "clientAddr": DEFAULT_DEVICE}]} _LOGGER.debug("Known devices: %s", known_devices) for loc in resp.get("locations") or []: @@ -88,18 +119,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Make sure that this device is not already configured # Comparing based on host (IP) and clientAddr. if (host, loc["clientAddr"]) in known_devices: - _LOGGER.debug("Discovered device %s on host %s with " - "client address %s is already " - "configured", - str.title(loc["locationName"]), - host, loc["clientAddr"]) + _LOGGER.debug( + "Discovered device %s on host %s with " + "client address %s is already " + "configured", + str.title(loc["locationName"]), + host, + loc["clientAddr"], + ) else: - _LOGGER.debug("Adding discovered device %s with" - " client address %s", - str.title(loc["locationName"]), - loc["clientAddr"]) - hosts.append([str.title(loc["locationName"]), host, - DEFAULT_PORT, loc["clientAddr"]]) + _LOGGER.debug( + "Adding discovered device %s with" " client address %s", + str.title(loc["locationName"]), + loc["clientAddr"], + ) + hosts.append( + [ + str.title(loc["locationName"]), + host, + DEFAULT_PORT, + loc["clientAddr"], + ] + ) dtvs = [] @@ -116,6 +157,7 @@ class DirecTvDevice(MediaPlayerDevice): def __init__(self, name, host, port, device): """Initialize the device.""" from DirectPy import DIRECTV + self.dtv = DIRECTV(host, port, device) self._name = name self._is_standby = True @@ -124,14 +166,13 @@ class DirecTvDevice(MediaPlayerDevice): self._paused = None self._last_position = None self._is_recorded = None - self._is_client = device != '0' + self._is_client = device != "0" self._assumed_state = None self._available = False self._first_error_timestamp = None if self._is_client: - _LOGGER.debug("Created DirecTV client %s for device %s", - self._name, device) + _LOGGER.debug("Created DirecTV client %s for device %s", self._name, device) else: _LOGGER.debug("Created DirecTV device for %s", self._name) @@ -150,22 +191,22 @@ class DirecTvDevice(MediaPlayerDevice): self._last_update = None else: self._current = self.dtv.get_tuned() - if self._current['status']['code'] == 200: + if self._current["status"]["code"] == 200: self._first_error_timestamp = None - self._is_recorded = self._current.get('uniqueId')\ - is not None - self._paused = self._last_position == \ - self._current['offset'] + self._is_recorded = self._current.get("uniqueId") is not None + self._paused = self._last_position == self._current["offset"] self._assumed_state = self._is_recorded - self._last_position = self._current['offset'] - self._last_update = dt_util.utcnow() if not self._paused \ - or self._last_update is None else self._last_update + self._last_position = self._current["offset"] + self._last_update = ( + dt_util.utcnow() + if not self._paused or self._last_update is None + else self._last_update + ) else: # If an error is received then only set to unavailable if # this started at least 1 minute ago. log_message = "{}: Invalid status {} received".format( - self.entity_id, - self._current['status']['code'] + self.entity_id, self._current["status"]["code"] ) if self._check_state_available(): _LOGGER.debug(log_message) @@ -173,13 +214,17 @@ class DirecTvDevice(MediaPlayerDevice): _LOGGER.error(log_message) except requests.RequestException as ex: - _LOGGER.error("%s: Request error trying to update current status: " - "%s", self.entity_id, ex) + _LOGGER.error( + "%s: Request error trying to update current status: " "%s", + self.entity_id, + ex, + ) self._check_state_available() except Exception as ex: - _LOGGER.error("%s: Exception trying to update current status: %s", - self.entity_id, ex) + _LOGGER.error( + "%s: Exception trying to update current status: %s", self.entity_id, ex + ) self._available = False if not self._first_error_timestamp: self._first_error_timestamp = dt_util.utcnow() @@ -201,8 +246,7 @@ class DirecTvDevice(MediaPlayerDevice): """Return device specific state attributes.""" attributes = {} if not self._is_standby: - attributes[ATTR_MEDIA_CURRENTLY_RECORDING] =\ - self.media_currently_recording + attributes[ATTR_MEDIA_CURRENTLY_RECORDING] = self.media_currently_recording attributes[ATTR_MEDIA_RATING] = self.media_rating attributes[ATTR_MEDIA_RECORDED] = self.media_recorded attributes[ATTR_MEDIA_START_TIME] = self.media_start_time @@ -245,7 +289,7 @@ class DirecTvDevice(MediaPlayerDevice): if self._is_standby: return None - return self._current['programId'] + return self._current["programId"] @property def media_content_type(self): @@ -253,7 +297,7 @@ class DirecTvDevice(MediaPlayerDevice): if self._is_standby: return None - if 'episodeTitle' in self._current: + if "episodeTitle" in self._current: return MEDIA_TYPE_TVSHOW return MEDIA_TYPE_MOVIE @@ -264,7 +308,7 @@ class DirecTvDevice(MediaPlayerDevice): if self._is_standby: return None - return self._current['duration'] + return self._current["duration"] @property def media_position(self): @@ -291,7 +335,7 @@ class DirecTvDevice(MediaPlayerDevice): if self._is_standby: return None - return self._current['title'] + return self._current["title"] @property def media_series_title(self): @@ -299,7 +343,7 @@ class DirecTvDevice(MediaPlayerDevice): if self._is_standby: return None - return self._current.get('episodeTitle') + return self._current.get("episodeTitle") @property def media_channel(self): @@ -307,8 +351,7 @@ class DirecTvDevice(MediaPlayerDevice): if self._is_standby: return None - return "{} ({})".format( - self._current['callsign'], self._current['major']) + return "{} ({})".format(self._current["callsign"], self._current["major"]) @property def source(self): @@ -316,7 +359,7 @@ class DirecTvDevice(MediaPlayerDevice): if self._is_standby: return None - return self._current['major'] + return self._current["major"] @property def supported_features(self): @@ -329,7 +372,7 @@ class DirecTvDevice(MediaPlayerDevice): if self._is_standby: return None - return self._current['isRecording'] + return self._current["isRecording"] @property def media_rating(self): @@ -337,7 +380,7 @@ class DirecTvDevice(MediaPlayerDevice): if self._is_standby: return None - return self._current['rating'] + return self._current["rating"] @property def media_recorded(self): @@ -353,8 +396,7 @@ class DirecTvDevice(MediaPlayerDevice): if self._is_standby: return None - return dt_util.as_local( - dt_util.utc_from_timestamp(self._current['startTime'])) + return dt_util.as_local(dt_util.utc_from_timestamp(self._current["startTime"])) def turn_on(self): """Turn on the receiver.""" @@ -362,7 +404,7 @@ class DirecTvDevice(MediaPlayerDevice): raise NotImplementedError() _LOGGER.debug("Turn on %s", self._name) - self.dtv.key_press('poweron') + self.dtv.key_press("poweron") def turn_off(self): """Turn off the receiver.""" @@ -370,38 +412,41 @@ class DirecTvDevice(MediaPlayerDevice): raise NotImplementedError() _LOGGER.debug("Turn off %s", self._name) - self.dtv.key_press('poweroff') + self.dtv.key_press("poweroff") def media_play(self): """Send play command.""" _LOGGER.debug("Play on %s", self._name) - self.dtv.key_press('play') + self.dtv.key_press("play") def media_pause(self): """Send pause command.""" _LOGGER.debug("Pause on %s", self._name) - self.dtv.key_press('pause') + self.dtv.key_press("pause") def media_stop(self): """Send stop command.""" _LOGGER.debug("Stop on %s", self._name) - self.dtv.key_press('stop') + self.dtv.key_press("stop") def media_previous_track(self): """Send rewind command.""" _LOGGER.debug("Rewind on %s", self._name) - self.dtv.key_press('rew') + self.dtv.key_press("rew") def media_next_track(self): """Send fast forward command.""" _LOGGER.debug("Fast forward on %s", self._name) - self.dtv.key_press('ffwd') + self.dtv.key_press("ffwd") def play_media(self, media_type, media_id, **kwargs): """Select input source.""" if media_type != MEDIA_TYPE_CHANNEL: - _LOGGER.error("Invalid media type %s. Only %s is supported", - media_type, MEDIA_TYPE_CHANNEL) + _LOGGER.error( + "Invalid media type %s. Only %s is supported", + media_type, + MEDIA_TYPE_CHANNEL, + ) return _LOGGER.debug("Changing channel on %s to %s", self._name, media_id) diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index f9f821668f9..64528f4ca5e 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -7,53 +7,60 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TOKEN) + ATTR_ATTRIBUTION, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + CONF_TOKEN, +) from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_IDENTITY = 'identity' +ATTR_IDENTITY = "identity" ATTRIBUTION = "Data provided by Discogs" -DEFAULT_NAME = 'Discogs' +DEFAULT_NAME = "Discogs" -ICON_RECORD = 'mdi:album' -ICON_PLAYER = 'mdi:record-player' -UNIT_RECORDS = 'records' +ICON_RECORD = "mdi:album" +ICON_PLAYER = "mdi:record-player" +UNIT_RECORDS = "records" SCAN_INTERVAL = timedelta(minutes=10) -SENSOR_COLLECTION_TYPE = 'collection' -SENSOR_WANTLIST_TYPE = 'wantlist' -SENSOR_RANDOM_RECORD_TYPE = 'random_record' +SENSOR_COLLECTION_TYPE = "collection" +SENSOR_WANTLIST_TYPE = "wantlist" +SENSOR_RANDOM_RECORD_TYPE = "random_record" SENSORS = { SENSOR_COLLECTION_TYPE: { - 'name': 'Collection', - 'icon': ICON_RECORD, - 'unit_of_measurement': UNIT_RECORDS + "name": "Collection", + "icon": ICON_RECORD, + "unit_of_measurement": UNIT_RECORDS, }, SENSOR_WANTLIST_TYPE: { - 'name': 'Wantlist', - 'icon': ICON_RECORD, - 'unit_of_measurement': UNIT_RECORDS + "name": "Wantlist", + "icon": ICON_RECORD, + "unit_of_measurement": UNIT_RECORDS, }, SENSOR_RANDOM_RECORD_TYPE: { - 'name': 'Random Record', - 'icon': ICON_PLAYER, - 'unit_of_measurement': None + "name": "Random Record", + "icon": ICON_PLAYER, + "unit_of_measurement": None, }, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): - vol.All(cv.ensure_list, [vol.In(SENSORS)]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( + cv.ensure_list, [vol.In(SENSORS)] + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -64,14 +71,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config[CONF_NAME] try: - discogs_client = discogs_client.Client( - SERVER_SOFTWARE, user_token=token) + discogs_client = discogs_client.Client(SERVER_SOFTWARE, user_token=token) discogs_data = { - 'user': discogs_client.identity().name, - 'folders': discogs_client.identity().collection_folders, - 'collection_count': discogs_client.identity().num_collection, - 'wantlist_count': discogs_client.identity().num_wantlist + "user": discogs_client.identity().name, + "folders": discogs_client.identity().collection_folders, + "collection_count": discogs_client.identity().num_collection, + "wantlist_count": discogs_client.identity().num_wantlist, } except discogs_client.exceptions.HTTPError: _LOGGER.error("API token is not valid") @@ -98,7 +104,7 @@ class DiscogsSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, SENSORS[self._type]['name']) + return "{} {}".format(self._name, SENSORS[self._type]["name"]) @property def state(self): @@ -108,12 +114,12 @@ class DiscogsSensor(Entity): @property def icon(self): """Return the icon to use in the frontend, if any.""" - return SENSORS[self._type]['icon'] + return SENSORS[self._type]["icon"] @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" - return SENSORS[self._type]['unit_of_measurement'] + return SENSORS[self._type]["unit_of_measurement"] @property def device_state_attributes(self): @@ -124,38 +130,39 @@ class DiscogsSensor(Entity): if self._type != SENSOR_RANDOM_RECORD_TYPE: return { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_IDENTITY: self._discogs_data['user'], + ATTR_IDENTITY: self._discogs_data["user"], } return { - 'cat_no': self._attrs['labels'][0]['catno'], - 'cover_image': self._attrs['cover_image'], - 'format': "{} ({})".format( - self._attrs['formats'][0]['name'], - self._attrs['formats'][0]['descriptions'][0]), - 'label': self._attrs['labels'][0]['name'], - 'released': self._attrs['year'], + "cat_no": self._attrs["labels"][0]["catno"], + "cover_image": self._attrs["cover_image"], + "format": "{} ({})".format( + self._attrs["formats"][0]["name"], + self._attrs["formats"][0]["descriptions"][0], + ), + "label": self._attrs["labels"][0]["name"], + "released": self._attrs["year"], ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_IDENTITY: self._discogs_data['user'], + ATTR_IDENTITY: self._discogs_data["user"], } def get_random_record(self): """Get a random record suggestion from the user's collection.""" # Index 0 in the folders is the 'All' folder - collection = self._discogs_data['folders'][0] + collection = self._discogs_data["folders"][0] random_index = random.randrange(collection.count) random_record = collection.releases[random_index].release self._attrs = random_record.data return "{} - {}".format( - random_record.data['artists'][0]['name'], - random_record.data['title']) + random_record.data["artists"][0]["name"], random_record.data["title"] + ) def update(self): """Set state to the amount of records in user's collection.""" if self._type == SENSOR_COLLECTION_TYPE: - self._state = self._discogs_data['collection_count'] + self._state = self._discogs_data["collection_count"] elif self._type == SENSOR_WANTLIST_TYPE: - self._state = self._discogs_data['wantlist_count'] + self._state = self._discogs_data["wantlist_count"] else: self._state = self.get_random_record() diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 75a434a3739..17ff0a192d0 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -7,17 +7,18 @@ import voluptuous as vol from homeassistant.const import CONF_TOKEN import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (ATTR_DATA, ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + PLATFORM_SCHEMA, + BaseNotificationService, +) _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TOKEN): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_TOKEN): cv.string}) -ATTR_IMAGES = 'images' +ATTR_IMAGES = "images" def get_service(hass, config, discovery_info=None): @@ -60,8 +61,8 @@ class DiscordNotificationService(BaseNotificationService): for image in data.get(ATTR_IMAGES): image_exists = await self.hass.async_add_executor_job( - self.file_exists, - image) + self.file_exists, image + ) if image_exists: images.append(image) @@ -78,9 +79,7 @@ class DiscordNotificationService(BaseNotificationService): channel = discord_bot.get_channel(channelid) if channel is None: - _LOGGER.warning( - "Channel not found for id: %s", - channelid) + _LOGGER.warning("Channel not found for id: %s", channelid) continue # Must create new instances of File for each channel. @@ -91,8 +90,7 @@ class DiscordNotificationService(BaseNotificationService): files.append(discord.File(image)) await channel.send(message, files=files) - except (discord.errors.HTTPException, - discord.errors.NotFound) as error: + except (discord.errors.HTTPException, discord.errors.NotFound) as error: _LOGGER.warning("Communication error: %s", error) await discord_bot.logout() await discord_bot.close() diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 229e64ad682..5f1fd335d45 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -20,106 +20,110 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -DOMAIN = 'discovery' +DOMAIN = "discovery" SCAN_INTERVAL = timedelta(seconds=300) -SERVICE_APPLE_TV = 'apple_tv' -SERVICE_DAIKIN = 'daikin' -SERVICE_DLNA_DMR = 'dlna_dmr' -SERVICE_ENIGMA2 = 'enigma2' -SERVICE_FREEBOX = 'freebox' -SERVICE_HASS_IOS_APP = 'hass_ios' -SERVICE_HASSIO = 'hassio' -SERVICE_HEOS = 'heos' -SERVICE_IGD = 'igd' -SERVICE_KONNECTED = 'konnected' -SERVICE_MOBILE_APP = 'hass_mobile_app' -SERVICE_NETGEAR = 'netgear_router' -SERVICE_OCTOPRINT = 'octoprint' -SERVICE_ROKU = 'roku' -SERVICE_SABNZBD = 'sabnzbd' -SERVICE_SAMSUNG_PRINTER = 'samsung_printer' -SERVICE_TELLDUSLIVE = 'tellstick' -SERVICE_YEELIGHT = 'yeelight' -SERVICE_WEMO = 'belkin_wemo' -SERVICE_WINK = 'wink' -SERVICE_XIAOMI_GW = 'xiaomi_gw' +SERVICE_APPLE_TV = "apple_tv" +SERVICE_DAIKIN = "daikin" +SERVICE_DLNA_DMR = "dlna_dmr" +SERVICE_ENIGMA2 = "enigma2" +SERVICE_FREEBOX = "freebox" +SERVICE_HASS_IOS_APP = "hass_ios" +SERVICE_HASSIO = "hassio" +SERVICE_HEOS = "heos" +SERVICE_IGD = "igd" +SERVICE_KONNECTED = "konnected" +SERVICE_MOBILE_APP = "hass_mobile_app" +SERVICE_NETGEAR = "netgear_router" +SERVICE_OCTOPRINT = "octoprint" +SERVICE_ROKU = "roku" +SERVICE_SABNZBD = "sabnzbd" +SERVICE_SAMSUNG_PRINTER = "samsung_printer" +SERVICE_TELLDUSLIVE = "tellstick" +SERVICE_YEELIGHT = "yeelight" +SERVICE_WEMO = "belkin_wemo" +SERVICE_WINK = "wink" +SERVICE_XIAOMI_GW = "xiaomi_gw" CONFIG_ENTRY_HANDLERS = { - SERVICE_DAIKIN: 'daikin', - SERVICE_TELLDUSLIVE: 'tellduslive', - SERVICE_IGD: 'upnp', + SERVICE_DAIKIN: "daikin", + SERVICE_TELLDUSLIVE: "tellduslive", + SERVICE_IGD: "upnp", } SERVICE_HANDLERS = { - SERVICE_MOBILE_APP: ('mobile_app', None), - SERVICE_HASS_IOS_APP: ('ios', None), - SERVICE_NETGEAR: ('device_tracker', None), - SERVICE_HASSIO: ('hassio', None), - SERVICE_APPLE_TV: ('apple_tv', None), - SERVICE_ENIGMA2: ('media_player', 'enigma2'), - SERVICE_ROKU: ('roku', None), - SERVICE_WINK: ('wink', None), - SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), - SERVICE_SABNZBD: ('sabnzbd', None), - SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), - SERVICE_KONNECTED: ('konnected', None), - SERVICE_OCTOPRINT: ('octoprint', None), - SERVICE_FREEBOX: ('freebox', None), - SERVICE_YEELIGHT: ('yeelight', None), - 'panasonic_viera': ('media_player', 'panasonic_viera'), - 'plex_mediaserver': ('media_player', 'plex'), - 'yamaha': ('media_player', 'yamaha'), - 'logitech_mediaserver': ('media_player', 'squeezebox'), - 'directv': ('media_player', 'directv'), - 'denonavr': ('media_player', 'denonavr'), - 'samsung_tv': ('media_player', 'samsungtv'), - 'frontier_silicon': ('media_player', 'frontier_silicon'), - 'openhome': ('media_player', 'openhome'), - 'harmony': ('remote', 'harmony'), - 'bose_soundtouch': ('media_player', 'soundtouch'), - 'bluesound': ('media_player', 'bluesound'), - 'songpal': ('media_player', 'songpal'), - 'kodi': ('media_player', 'kodi'), - 'volumio': ('media_player', 'volumio'), - 'lg_smart_device': ('media_player', 'lg_soundbar'), - 'nanoleaf_aurora': ('light', 'nanoleaf'), + SERVICE_MOBILE_APP: ("mobile_app", None), + SERVICE_HASS_IOS_APP: ("ios", None), + SERVICE_NETGEAR: ("device_tracker", None), + SERVICE_HASSIO: ("hassio", None), + SERVICE_APPLE_TV: ("apple_tv", None), + SERVICE_ENIGMA2: ("media_player", "enigma2"), + SERVICE_ROKU: ("roku", None), + SERVICE_WINK: ("wink", None), + SERVICE_XIAOMI_GW: ("xiaomi_aqara", None), + SERVICE_SABNZBD: ("sabnzbd", None), + SERVICE_SAMSUNG_PRINTER: ("sensor", "syncthru"), + SERVICE_KONNECTED: ("konnected", None), + SERVICE_OCTOPRINT: ("octoprint", None), + SERVICE_FREEBOX: ("freebox", None), + SERVICE_YEELIGHT: ("yeelight", None), + "panasonic_viera": ("media_player", "panasonic_viera"), + "plex_mediaserver": ("media_player", "plex"), + "yamaha": ("media_player", "yamaha"), + "logitech_mediaserver": ("media_player", "squeezebox"), + "directv": ("media_player", "directv"), + "denonavr": ("media_player", "denonavr"), + "samsung_tv": ("media_player", "samsungtv"), + "frontier_silicon": ("media_player", "frontier_silicon"), + "openhome": ("media_player", "openhome"), + "harmony": ("remote", "harmony"), + "bose_soundtouch": ("media_player", "soundtouch"), + "bluesound": ("media_player", "bluesound"), + "songpal": ("media_player", "songpal"), + "kodi": ("media_player", "kodi"), + "volumio": ("media_player", "volumio"), + "lg_smart_device": ("media_player", "lg_soundbar"), + "nanoleaf_aurora": ("light", "nanoleaf"), } -OPTIONAL_SERVICE_HANDLERS = { - SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'), -} +OPTIONAL_SERVICE_HANDLERS = {SERVICE_DLNA_DMR: ("media_player", "dlna_dmr")} MIGRATED_SERVICE_HANDLERS = [ - 'axis', - 'deconz', - 'esphome', - 'google_cast', + "axis", + "deconz", + "esphome", + "google_cast", SERVICE_HEOS, - 'homekit', - 'ikea_tradfri', - 'philips_hue', - 'sonos', + "homekit", + "ikea_tradfri", + "philips_hue", + "sonos", SERVICE_WEMO, ] -DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + \ - MIGRATED_SERVICE_HANDLERS -DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + \ - MIGRATED_SERVICE_HANDLERS +DEFAULT_ENABLED = ( + list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + MIGRATED_SERVICE_HANDLERS +) +DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + MIGRATED_SERVICE_HANDLERS -CONF_IGNORE = 'ignore' -CONF_ENABLE = 'enable' +CONF_IGNORE = "ignore" +CONF_ENABLE = "enable" -CONFIG_SCHEMA = vol.Schema({ - vol.Optional(DOMAIN): vol.Schema({ - vol.Optional(CONF_IGNORE, default=[]): - vol.All(cv.ensure_list, [vol.In(DEFAULT_ENABLED)]), - vol.Optional(CONF_ENABLE, default=[]): - vol.All(cv.ensure_list, [ - vol.In(DEFAULT_DISABLED + DEFAULT_ENABLED)]), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + vol.Optional(DOMAIN): vol.Schema( + { + vol.Optional(CONF_IGNORE, default=[]): vol.All( + cv.ensure_list, [vol.In(DEFAULT_ENABLED)] + ), + vol.Optional(CONF_ENABLE, default=[]): vol.All( + cv.ensure_list, [vol.In(DEFAULT_DISABLED + DEFAULT_ENABLED)] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -131,7 +135,7 @@ async def async_setup(hass, config): already_discovered = set() # Disable zeroconf logging, it spams - logging.getLogger('zeroconf').setLevel(logging.CRITICAL) + logging.getLogger("zeroconf").setLevel(logging.CRITICAL) if DOMAIN in config: # Platforms ignore by config @@ -170,8 +174,8 @@ async def async_setup(hass, config): if service in CONFIG_ENTRY_HANDLERS: await hass.config_entries.flow.async_init( CONFIG_ENTRY_HANDLERS[service], - context={'source': config_entries.SOURCE_DISCOVERY}, - data=info + context={"source": config_entries.SOURCE_DISCOVERY}, + data=info, ) return @@ -192,8 +196,7 @@ async def async_setup(hass, config): if platform is None: await async_discover(hass, service, info, component, config) else: - await async_load_platform( - hass, component, platform, info, config) + await async_load_platform(hass, component, platform, info, config) async def scan_devices(now): """Scan for devices.""" @@ -206,7 +209,8 @@ async def async_setup(hass, config): logger.error("Network is unreachable") async_track_point_in_utc_time( - hass, scan_devices, dt_util.utcnow() + SCAN_INTERVAL) + hass, scan_devices, dt_util.utcnow() + SCAN_INTERVAL + ) @callback def schedule_first(event): diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 0bc657a615d..749e536e2e8 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -3,23 +3,28 @@ import logging import io from homeassistant.core import split_entity_id + # pylint: disable=unused-import from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) + ImageProcessingFaceEntity, + CONF_SOURCE, + CONF_ENTITY_ID, + CONF_NAME, +) _LOGGER = logging.getLogger(__name__) -ATTR_LOCATION = 'location' +ATTR_LOCATION = "location" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dlib Face detection platform.""" entities = [] for camera in config[CONF_SOURCE]: - entities.append(DlibFaceDetectEntity( - camera[CONF_ENTITY_ID], camera.get(CONF_NAME) - )) + entities.append( + DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) + ) add_entities(entities) @@ -36,8 +41,7 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity): if name: self._name = name else: - self._name = "Dlib Face {0}".format( - split_entity_id(camera_entity)[1]) + self._name = "Dlib Face {0}".format(split_entity_id(camera_entity)[1]) @property def camera_entity(self): @@ -54,13 +58,12 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity): import face_recognition # pylint: disable=import-error fak_file = io.BytesIO(image) - fak_file.name = 'snapshot.jpg' + fak_file.name = "snapshot.jpg" fak_file.seek(0) image = face_recognition.load_image_file(fak_file) face_locations = face_recognition.face_locations(image) - face_locations = [{ATTR_LOCATION: location} - for location in face_locations] + face_locations = [{ATTR_LOCATION: location} for location in face_locations] self.process_faces(face_locations, len(face_locations)) diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index 1dd7b6e4626..d4851be28c8 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -6,29 +6,40 @@ import voluptuous as vol from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, - CONF_NAME, CONF_CONFIDENCE) + ImageProcessingFaceEntity, + PLATFORM_SCHEMA, + CONF_SOURCE, + CONF_ENTITY_ID, + CONF_NAME, + CONF_CONFIDENCE, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_NAME = 'name' -CONF_FACES = 'faces' +ATTR_NAME = "name" +CONF_FACES = "faces" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_FACES): {cv.string: cv.isfile}, - vol.Optional(CONF_CONFIDENCE, default=0.6): vol.Coerce(float), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_FACES): {cv.string: cv.isfile}, + vol.Optional(CONF_CONFIDENCE, default=0.6): vol.Coerce(float), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dlib Face detection platform.""" entities = [] for camera in config[CONF_SOURCE]: - entities.append(DlibFaceIdentifyEntity( - camera[CONF_ENTITY_ID], config[CONF_FACES], camera.get(CONF_NAME), - config[CONF_CONFIDENCE] - )) + entities.append( + DlibFaceIdentifyEntity( + camera[CONF_ENTITY_ID], + config[CONF_FACES], + camera.get(CONF_NAME), + config[CONF_CONFIDENCE], + ) + ) add_entities(entities) @@ -40,6 +51,7 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): """Initialize Dlib face identify entry.""" # pylint: disable=import-error import face_recognition + super().__init__() self._camera = camera_entity @@ -47,15 +59,13 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): if name: self._name = name else: - self._name = "Dlib Face {0}".format( - split_entity_id(camera_entity)[1]) + self._name = "Dlib Face {0}".format(split_entity_id(camera_entity)[1]) self._faces = {} for face_name, face_file in faces.items(): try: image = face_recognition.load_image_file(face_file) - self._faces[face_name] = \ - face_recognition.face_encodings(image)[0] + self._faces[face_name] = face_recognition.face_encodings(image)[0] except IndexError as err: _LOGGER.error("Failed to parse %s. Error: %s", face_file, err) @@ -77,7 +87,7 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): import face_recognition fak_file = io.BytesIO(image) - fak_file.name = 'snapshot.jpg' + fak_file.name = "snapshot.jpg" fak_file.seek(0) image = face_recognition.load_image_file(fak_file) @@ -87,12 +97,9 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): for unknown_face in unknowns: for name, face in self._faces.items(): result = face_recognition.compare_faces( - [face], - unknown_face, - tolerance=self._tolerance) + [face], unknown_face, tolerance=self._tolerance + ) if result[0]: - found.append({ - ATTR_NAME: name - }) + found.append({ATTR_NAME: name}) self.process_faces(found, len(unknowns)) diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 7164bb2310a..25091e14dbd 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -7,30 +7,37 @@ import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - TEMP_CELSIUS) + ATTR_TEMPERATURE, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) -ATTR_TOTAL_CONSUMPTION = 'total_consumption' +ATTR_TOTAL_CONSUMPTION = "total_consumption" -CONF_USE_LEGACY_PROTOCOL = 'use_legacy_protocol' +CONF_USE_LEGACY_PROTOCOL = "use_legacy_protocol" DEFAULT_NAME = "D-Link Smart Plug W215" -DEFAULT_PASSWORD = '' -DEFAULT_USERNAME = 'admin' +DEFAULT_PASSWORD = "" +DEFAULT_USERNAME = "admin" SCAN_INTERVAL = timedelta(minutes=2) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_USE_LEGACY_PROTOCOL, default=False): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_USE_LEGACY_PROTOCOL, default=False): cv.boolean, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -67,8 +74,7 @@ class SmartPlugSwitch(SwitchDevice): def device_state_attributes(self): """Return the state attributes of the device.""" try: - ui_temp = self.units.temperature( - int(self.data.temperature), TEMP_CELSIUS) + ui_temp = self.units.temperature(int(self.data.temperature), TEMP_CELSIUS) temperature = ui_temp except (ValueError, TypeError): temperature = None @@ -96,15 +102,15 @@ class SmartPlugSwitch(SwitchDevice): @property def is_on(self): """Return true if switch is on.""" - return self.data.state == 'ON' + return self.data.state == "ON" def turn_on(self, **kwargs): """Turn the switch on.""" - self.data.smartplug.state = 'ON' + self.data.smartplug.state = "ON" def turn_off(self, **kwargs): """Turn the switch off.""" - self.data.smartplug.state = 'OFF' + self.data.smartplug.state = "OFF" def update(self): """Get the latest data from the smart plug and updates the states.""" @@ -133,20 +139,20 @@ class SmartPlugData: def update(self): """Get the latest data from the smart plug.""" if self._last_tried is not None: - last_try_s = (dt_util.now() - self._last_tried).total_seconds()/60 - retry_seconds = min(self._n_tried*2, 10) - last_try_s + last_try_s = (dt_util.now() - self._last_tried).total_seconds() / 60 + retry_seconds = min(self._n_tried * 2, 10) - last_try_s if self._n_tried > 0 and retry_seconds > 0: _LOGGER.warning("Waiting %s s to retry", retry_seconds) return - _state = 'unknown' + _state = "unknown" try: self._last_tried = dt_util.now() _state = self.smartplug.state except urllib.error.HTTPError: _LOGGER.error("D-Link connection problem") - if _state == 'unknown': + if _state == "unknown": self._n_tried += 1 self.available = False _LOGGER.warning("Failed to connect to D-Link switch") diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index dd348d1fbbc..c7c488950cc 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -9,18 +9,36 @@ from typing import Optional import aiohttp import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, MEDIA_TYPE_IMAGE, - MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) + MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_IMAGE, + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_TVSHOW, + MEDIA_TYPE_VIDEO, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) from homeassistant.const import ( - CONF_NAME, CONF_URL, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, - STATE_ON, STATE_PAUSED, STATE_PLAYING) + CONF_NAME, + CONF_URL, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType @@ -29,50 +47,54 @@ from homeassistant.util import get_local_ip _LOGGER = logging.getLogger(__name__) -DLNA_DMR_DATA = 'dlna_dmr' +DLNA_DMR_DATA = "dlna_dmr" -DEFAULT_NAME = 'DLNA Digital Media Renderer' +DEFAULT_NAME = "DLNA Digital Media Renderer" DEFAULT_LISTEN_PORT = 8301 -CONF_LISTEN_IP = 'listen_ip' -CONF_LISTEN_PORT = 'listen_port' -CONF_CALLBACK_URL_OVERRIDE = 'callback_url_override' +CONF_LISTEN_IP = "listen_ip" +CONF_LISTEN_PORT = "listen_port" +CONF_CALLBACK_URL_OVERRIDE = "callback_url_override" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_URL): cv.string, - vol.Optional(CONF_LISTEN_IP): cv.string, - vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_CALLBACK_URL_OVERRIDE): cv.url, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_LISTEN_IP): cv.string, + vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CALLBACK_URL_OVERRIDE): cv.url, + } +) HOME_ASSISTANT_UPNP_CLASS_MAPPING = { - MEDIA_TYPE_MUSIC: 'object.item.audioItem', - MEDIA_TYPE_TVSHOW: 'object.item.videoItem', - MEDIA_TYPE_MOVIE: 'object.item.videoItem', - MEDIA_TYPE_VIDEO: 'object.item.videoItem', - MEDIA_TYPE_EPISODE: 'object.item.videoItem', - MEDIA_TYPE_CHANNEL: 'object.item.videoItem', - MEDIA_TYPE_IMAGE: 'object.item.imageItem', - MEDIA_TYPE_PLAYLIST: 'object.item.playlist', + MEDIA_TYPE_MUSIC: "object.item.audioItem", + MEDIA_TYPE_TVSHOW: "object.item.videoItem", + MEDIA_TYPE_MOVIE: "object.item.videoItem", + MEDIA_TYPE_VIDEO: "object.item.videoItem", + MEDIA_TYPE_EPISODE: "object.item.videoItem", + MEDIA_TYPE_CHANNEL: "object.item.videoItem", + MEDIA_TYPE_IMAGE: "object.item.imageItem", + MEDIA_TYPE_PLAYLIST: "object.item.playlist", } -UPNP_CLASS_DEFAULT = 'object.item' +UPNP_CLASS_DEFAULT = "object.item" HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING = { - MEDIA_TYPE_MUSIC: 'audio/*', - MEDIA_TYPE_TVSHOW: 'video/*', - MEDIA_TYPE_MOVIE: 'video/*', - MEDIA_TYPE_VIDEO: 'video/*', - MEDIA_TYPE_EPISODE: 'video/*', - MEDIA_TYPE_CHANNEL: 'video/*', - MEDIA_TYPE_IMAGE: 'image/*', - MEDIA_TYPE_PLAYLIST: 'playlist/*', + MEDIA_TYPE_MUSIC: "audio/*", + MEDIA_TYPE_TVSHOW: "video/*", + MEDIA_TYPE_MOVIE: "video/*", + MEDIA_TYPE_VIDEO: "video/*", + MEDIA_TYPE_EPISODE: "video/*", + MEDIA_TYPE_CHANNEL: "video/*", + MEDIA_TYPE_IMAGE: "image/*", + MEDIA_TYPE_PLAYLIST: "playlist/*", } def catch_request_errors(): """Catch asyncio.TimeoutError, aiohttp.ClientError errors.""" + def call_wrapper(func): """Call wrapper for decorator.""" + @functools.wraps(func) def wrapper(self, *args, **kwargs): """Catch asyncio.TimeoutError, aiohttp.ClientError errors.""" @@ -87,75 +109,79 @@ def catch_request_errors(): async def async_start_event_handler( - hass: HomeAssistantType, - server_host: str, - server_port: int, - requester, - callback_url_override: Optional[str] = None): + hass: HomeAssistantType, + server_host: str, + server_port: int, + requester, + callback_url_override: Optional[str] = None, +): """Register notify view.""" hass_data = hass.data[DLNA_DMR_DATA] - if 'event_handler' in hass_data: - return hass_data['event_handler'] + if "event_handler" in hass_data: + return hass_data["event_handler"] # start event handler from async_upnp_client.aiohttp import AiohttpNotifyServer + server = AiohttpNotifyServer( requester, listen_port=server_port, listen_host=server_host, - callback_url=callback_url_override) + callback_url=callback_url_override, + ) await server.start_server() - _LOGGER.info( - 'UPNP/DLNA event handler listening, url: %s', server.callback_url) - hass_data['notify_server'] = server - hass_data['event_handler'] = server.event_handler + _LOGGER.info("UPNP/DLNA event handler listening, url: %s", server.callback_url) + hass_data["notify_server"] = server + hass_data["event_handler"] = server.event_handler # register for graceful shutdown async def async_stop_server(event): """Stop server.""" - _LOGGER.debug('Stopping UPNP/DLNA event handler') + _LOGGER.debug("Stopping UPNP/DLNA event handler") await server.stop_server() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_server) - return hass_data['event_handler'] + return hass_data["event_handler"] async def async_setup_platform( - hass: HomeAssistantType, - config, - async_add_entities, - discovery_info=None): + hass: HomeAssistantType, config, async_add_entities, discovery_info=None +): """Set up DLNA DMR platform.""" if config.get(CONF_URL) is not None: url = config[CONF_URL] name = config.get(CONF_NAME) elif discovery_info is not None: - url = discovery_info['ssdp_description'] - name = discovery_info.get('name') + url = discovery_info["ssdp_description"] + name = discovery_info.get("name") if DLNA_DMR_DATA not in hass.data: hass.data[DLNA_DMR_DATA] = {} - if 'lock' not in hass.data[DLNA_DMR_DATA]: - hass.data[DLNA_DMR_DATA]['lock'] = asyncio.Lock() + if "lock" not in hass.data[DLNA_DMR_DATA]: + hass.data[DLNA_DMR_DATA]["lock"] = asyncio.Lock() # build upnp/aiohttp requester from async_upnp_client.aiohttp import AiohttpSessionRequester + session = async_get_clientsession(hass) requester = AiohttpSessionRequester(session, True) # ensure event handler has been started - with await hass.data[DLNA_DMR_DATA]['lock']: + with await hass.data[DLNA_DMR_DATA]["lock"]: server_host = config.get(CONF_LISTEN_IP) if server_host is None: server_host = get_local_ip() server_port = config.get(CONF_LISTEN_PORT, DEFAULT_LISTEN_PORT) callback_url_override = config.get(CONF_CALLBACK_URL_OVERRIDE) event_handler = await async_start_event_handler( - hass, server_host, server_port, requester, callback_url_override) + hass, server_host, server_port, requester, callback_url_override + ) # create upnp device from async_upnp_client import UpnpFactory + factory = UpnpFactory(requester, disable_state_variable_validation=True) try: upnp_device = await factory.async_create_device(url) @@ -164,6 +190,7 @@ async def async_setup_platform( # wrap with DmrDevice from async_upnp_client.profiles.dlna import DmrDevice + dlna_device = DmrDevice(upnp_device, event_handler) # create our own device @@ -189,8 +216,7 @@ class DlnaDmrDevice(MediaPlayerDevice): # Register unsubscribe on stop bus = self.hass.bus - bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_on_hass_stop) + bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_on_hass_stop) @property def available(self): @@ -199,7 +225,7 @@ class DlnaDmrDevice(MediaPlayerDevice): async def _async_on_hass_stop(self, event): """Event handler on HASS stop.""" - with await self.hass.data[DLNA_DMR_DATA]['lock']: + with await self.hass.data[DLNA_DMR_DATA]["lock"]: await self._device.async_unsubscribe_services() async def async_update(self): @@ -216,10 +242,10 @@ class DlnaDmrDevice(MediaPlayerDevice): # do we need to (re-)subscribe? now = datetime.now() - should_renew = self._subscription_renew_time and \ - now >= self._subscription_renew_time - if should_renew or \ - not was_available and self._available: + should_renew = ( + self._subscription_renew_time and now >= self._subscription_renew_time + ) + if should_renew or not was_available and self._available: try: timeout = await self._device.async_subscribe_services() self._subscription_renew_time = datetime.now() + timeout / 2 @@ -282,7 +308,7 @@ class DlnaDmrDevice(MediaPlayerDevice): async def async_media_pause(self): """Send pause command.""" if not self._device.can_pause: - _LOGGER.debug('Cannot do Pause') + _LOGGER.debug("Cannot do Pause") return await self._device.async_pause() @@ -291,7 +317,7 @@ class DlnaDmrDevice(MediaPlayerDevice): async def async_media_play(self): """Send play command.""" if not self._device.can_play: - _LOGGER.debug('Cannot do Play') + _LOGGER.debug("Cannot do Play") return await self._device.async_play() @@ -300,7 +326,7 @@ class DlnaDmrDevice(MediaPlayerDevice): async def async_media_stop(self): """Send stop command.""" if not self._device.can_stop: - _LOGGER.debug('Cannot do Stop') + _LOGGER.debug("Cannot do Stop") return await self._device.async_stop() @@ -309,7 +335,7 @@ class DlnaDmrDevice(MediaPlayerDevice): async def async_media_seek(self, position): """Send seek command.""" if not self._device.can_seek_rel_time: - _LOGGER.debug('Cannot do Seek/rel_time') + _LOGGER.debug("Cannot do Seek/rel_time") return time = timedelta(seconds=position) @@ -319,10 +345,10 @@ class DlnaDmrDevice(MediaPlayerDevice): async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" title = "Home Assistant" - mime_type = HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING.get(media_type, - media_type) - upnp_class = HOME_ASSISTANT_UPNP_CLASS_MAPPING.get(media_type, - UPNP_CLASS_DEFAULT) + mime_type = HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING.get(media_type, media_type) + upnp_class = HOME_ASSISTANT_UPNP_CLASS_MAPPING.get( + media_type, UPNP_CLASS_DEFAULT + ) # Stop current playing media if self._device.can_stop: @@ -330,11 +356,13 @@ class DlnaDmrDevice(MediaPlayerDevice): # Queue media await self._device.async_set_transport_uri( - media_id, title, mime_type, upnp_class) + media_id, title, mime_type, upnp_class + ) await self._device.async_wait_for_can_play() # If already playing, no need to call Play from async_upnp_client.profiles.dlna import DeviceState + if self._device.state == DeviceState.PLAYING: return @@ -345,7 +373,7 @@ class DlnaDmrDevice(MediaPlayerDevice): async def async_media_previous_track(self): """Send previous track command.""" if not self._device.can_previous: - _LOGGER.debug('Cannot do Previous') + _LOGGER.debug("Cannot do Previous") return await self._device.async_previous() @@ -354,7 +382,7 @@ class DlnaDmrDevice(MediaPlayerDevice): async def async_media_next_track(self): """Send next track command.""" if not self._device.can_next: - _LOGGER.debug('Cannot do Next') + _LOGGER.debug("Cannot do Next") return await self._device.async_next() @@ -376,6 +404,7 @@ class DlnaDmrDevice(MediaPlayerDevice): return STATE_OFF from async_upnp_client.profiles.dlna import DeviceState + if self._device.state is None: return STATE_ON if self._device.state == DeviceState.PLAYING: diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 337a68a77ce..0053d5a95ea 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -11,30 +11,31 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_HOSTNAME = 'hostname' -CONF_IPV6 = 'ipv6' -CONF_RESOLVER = 'resolver' -CONF_RESOLVER_IPV6 = 'resolver_ipv6' +CONF_HOSTNAME = "hostname" +CONF_IPV6 = "ipv6" +CONF_RESOLVER = "resolver" +CONF_RESOLVER_IPV6 = "resolver_ipv6" -DEFAULT_HOSTNAME = 'myip.opendns.com' +DEFAULT_HOSTNAME = "myip.opendns.com" DEFAULT_IPV6 = False -DEFAULT_NAME = 'myip' -DEFAULT_RESOLVER = '208.67.222.222' -DEFAULT_RESOLVER_IPV6 = '2620:0:ccc::2' +DEFAULT_NAME = "myip" +DEFAULT_RESOLVER = "208.67.222.222" +DEFAULT_RESOLVER_IPV6 = "2620:0:ccc::2" SCAN_INTERVAL = timedelta(seconds=120) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string, - vol.Optional(CONF_RESOLVER, default=DEFAULT_RESOLVER): cv.string, - vol.Optional(CONF_RESOLVER_IPV6, default=DEFAULT_RESOLVER_IPV6): cv.string, - vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string, + vol.Optional(CONF_RESOLVER, default=DEFAULT_RESOLVER): cv.string, + vol.Optional(CONF_RESOLVER_IPV6, default=DEFAULT_RESOLVER_IPV6): cv.string, + vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean, + } +) -async def async_setup_platform( - hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the DNS IP sensor.""" hostname = config.get(CONF_HOSTNAME) name = config.get(CONF_NAME) @@ -49,8 +50,7 @@ async def async_setup_platform( else: resolver = config.get(CONF_RESOLVER) - async_add_devices([WanIpSensor( - hass, name, hostname, resolver, ipv6)], True) + async_add_devices([WanIpSensor(hass, name, hostname, resolver, ipv6)], True) class WanIpSensor(Entity): @@ -65,7 +65,7 @@ class WanIpSensor(Entity): self.hostname = hostname self.resolver = aiodns.DNSResolver() self.resolver.nameservers = [resolver] - self.querytype = 'AAAA' if ipv6 else 'A' + self.querytype = "AAAA" if ipv6 else "A" self._state = None @property @@ -83,8 +83,7 @@ class WanIpSensor(Entity): from aiodns.error import DNSError try: - response = await self.resolver.query( - self.hostname, self.querytype) + response = await self.resolver.query(self.hostname, self.querytype) except DNSError as err: _LOGGER.warning("Exception while resolving host: %s", err) response = None diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index 3c5cb3ed6ec..59869ed0a97 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -14,42 +14,50 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) # The domain of your component. Should be equal to the name of your component. -DOMAIN = 'dominos' -ENTITY_ID_FORMAT = DOMAIN + '.{}' +DOMAIN = "dominos" +ENTITY_ID_FORMAT = DOMAIN + ".{}" -ATTR_COUNTRY = 'country_code' -ATTR_FIRST_NAME = 'first_name' -ATTR_LAST_NAME = 'last_name' -ATTR_EMAIL = 'email' -ATTR_PHONE = 'phone' -ATTR_ADDRESS = 'address' -ATTR_ORDERS = 'orders' -ATTR_SHOW_MENU = 'show_menu' -ATTR_ORDER_ENTITY = 'order_entity_id' -ATTR_ORDER_NAME = 'name' -ATTR_ORDER_CODES = 'codes' +ATTR_COUNTRY = "country_code" +ATTR_FIRST_NAME = "first_name" +ATTR_LAST_NAME = "last_name" +ATTR_EMAIL = "email" +ATTR_PHONE = "phone" +ATTR_ADDRESS = "address" +ATTR_ORDERS = "orders" +ATTR_SHOW_MENU = "show_menu" +ATTR_ORDER_ENTITY = "order_entity_id" +ATTR_ORDER_NAME = "name" +ATTR_ORDER_CODES = "codes" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) MIN_TIME_BETWEEN_STORE_UPDATES = timedelta(minutes=3330) -_ORDERS_SCHEMA = vol.Schema({ - vol.Required(ATTR_ORDER_NAME): cv.string, - vol.Required(ATTR_ORDER_CODES): vol.All(cv.ensure_list, [cv.string]), -}) +_ORDERS_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ORDER_NAME): cv.string, + vol.Required(ATTR_ORDER_CODES): vol.All(cv.ensure_list, [cv.string]), + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(ATTR_COUNTRY): cv.string, - vol.Required(ATTR_FIRST_NAME): cv.string, - vol.Required(ATTR_LAST_NAME): cv.string, - vol.Required(ATTR_EMAIL): cv.string, - vol.Required(ATTR_PHONE): cv.string, - vol.Required(ATTR_ADDRESS): cv.string, - vol.Optional(ATTR_SHOW_MENU): cv.boolean, - vol.Optional(ATTR_ORDERS, default=[]): vol.All( - cv.ensure_list, [_ORDERS_SCHEMA]), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(ATTR_COUNTRY): cv.string, + vol.Required(ATTR_FIRST_NAME): cv.string, + vol.Required(ATTR_LAST_NAME): cv.string, + vol.Required(ATTR_EMAIL): cv.string, + vol.Required(ATTR_PHONE): cv.string, + vol.Required(ATTR_ADDRESS): cv.string, + vol.Optional(ATTR_SHOW_MENU): cv.boolean, + vol.Optional(ATTR_ORDERS, default=[]): vol.All( + cv.ensure_list, [_ORDERS_SCHEMA] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -61,7 +69,7 @@ def setup(hass, config): entities = [] conf = config[DOMAIN] - hass.services.register(DOMAIN, 'order', dominos.handle_order) + hass.services.register(DOMAIN, "order", dominos.handle_order) if conf.get(ATTR_SHOW_MENU): hass.http.register_view(DominosProductListView(dominos)) @@ -77,7 +85,7 @@ def setup(hass, config): return True -class Dominos(): +class Dominos: """Main Dominos service.""" def __init__(self, hass, config): @@ -85,16 +93,18 @@ class Dominos(): conf = config[DOMAIN] from pizzapi import Address, Customer from pizzapi.address import StoreException + self.hass = hass self.customer = Customer( conf.get(ATTR_FIRST_NAME), conf.get(ATTR_LAST_NAME), conf.get(ATTR_EMAIL), conf.get(ATTR_PHONE), - conf.get(ATTR_ADDRESS)) + conf.get(ATTR_ADDRESS), + ) self.address = Address( - *self.customer.address.split(','), - country=conf.get(ATTR_COUNTRY)) + *self.customer.address.split(","), country=conf.get(ATTR_COUNTRY) + ) self.country = conf.get(ATTR_COUNTRY) try: self.closest_store = self.address.closest_store() @@ -105,8 +115,11 @@ class Dominos(): """Handle ordering pizza.""" entity_ids = call.data.get(ATTR_ORDER_ENTITY, None) - target_orders = [order for order in self.hass.data[DOMAIN]['entities'] - if order.entity_id in entity_ids] + target_orders = [ + order + for order in self.hass.data[DOMAIN]["entities"] + if order.entity_id in entity_ids + ] for order in target_orders: order.place() @@ -115,6 +128,7 @@ class Dominos(): def update_closest_store(self): """Update the shared closest store (if open).""" from pizzapi.address import StoreException + try: self.closest_store = self.address.closest_store() return True @@ -126,19 +140,19 @@ class Dominos(): """Return the products from the closest stores menu.""" self.update_closest_store() if self.closest_store is None: - _LOGGER.warning('Cannot get menu. Store may be closed') + _LOGGER.warning("Cannot get menu. Store may be closed") return [] menu = self.closest_store.get_menu() product_entries = [] for product in menu.products: item = {} - if isinstance(product.menu_data['Variants'], list): - variants = ', '.join(product.menu_data['Variants']) + if isinstance(product.menu_data["Variants"], list): + variants = ", ".join(product.menu_data["Variants"]) else: - variants = product.menu_data['Variants'] - item['name'] = product.name - item['variants'] = variants + variants = product.menu_data["Variants"] + item["name"] = product.name + item["variants"] = variants product_entries.append(item) return product_entries @@ -147,7 +161,7 @@ class Dominos(): class DominosProductListView(http.HomeAssistantView): """View to retrieve product list content.""" - url = '/api/dominos' + url = "/api/dominos" name = "api:dominos" def __init__(self, dominos): @@ -165,8 +179,8 @@ class DominosOrder(Entity): def __init__(self, order_info, dominos): """Set up the entity.""" - self._name = order_info['name'] - self._product_codes = order_info['codes'] + self._name = order_info["name"] + self._product_codes = order_info["codes"] self._orderable = False self.dominos = dominos @@ -189,13 +203,14 @@ class DominosOrder(Entity): def state(self): """Return the state either closed, orderable or unorderable.""" if self.dominos.closest_store is None: - return 'closed' - return 'orderable' if self._orderable else 'unorderable' + return "closed" + return "orderable" if self._orderable else "unorderable" @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the order state and refreshes the store.""" from pizzapi.address import StoreException + try: self.dominos.update_closest_store() except StoreException: @@ -221,7 +236,8 @@ class DominosOrder(Entity): self.dominos.closest_store, self.dominos.customer, self.dominos.address, - self.dominos.country) + self.dominos.country, + ) for code in self._product_codes: order.add_item(code) @@ -231,10 +247,12 @@ class DominosOrder(Entity): def place(self): """Place the order.""" from pizzapi.address import StoreException + try: order = self.order() order.place() except StoreException: self._orderable = False _LOGGER.warning( - 'Attempted to order Dominos - Order invalid or store closed') + "Attempted to order Dominos - Order invalid or store closed" + ) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 62d3584603a..3afa9c58e66 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -6,38 +6,47 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_TOKEN, - CONF_USERNAME) + CONF_DEVICES, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, +) import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util, slugify _LOGGER = logging.getLogger(__name__) -DOMAIN = 'doorbird' +DOMAIN = "doorbird" -API_URL = '/api/{}'.format(DOMAIN) +API_URL = "/api/{}".format(DOMAIN) -CONF_CUSTOM_URL = 'hass_url_override' -CONF_EVENTS = 'events' +CONF_CUSTOM_URL = "hass_url_override" +CONF_EVENTS = "events" -RESET_DEVICE_FAVORITES = 'doorbird_reset_favorites' +RESET_DEVICE_FAVORITES = "doorbird_reset_favorites" -DEVICE_SCHEMA = vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_EVENTS, default=[]): vol.All( - cv.ensure_list, [cv.string]), - vol.Optional(CONF_CUSTOM_URL): cv.string, - vol.Optional(CONF_NAME): cv.string -}) +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_EVENTS, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_CUSTOM_URL): cv.string, + vol.Optional(CONF_NAME): cv.string, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA]) - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA])} + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -56,25 +65,32 @@ def setup(hass, config): custom_url = doorstation_config.get(CONF_CUSTOM_URL) events = doorstation_config.get(CONF_EVENTS) token = doorstation_config.get(CONF_TOKEN) - name = (doorstation_config.get(CONF_NAME) - or 'DoorBird {}'.format(index + 1)) + name = doorstation_config.get(CONF_NAME) or "DoorBird {}".format(index + 1) device = DoorBird(device_ip, username, password) status = device.ready() if status[0]: - doorstation = ConfiguredDoorBird(device, name, events, custom_url, - token) + doorstation = ConfiguredDoorBird(device, name, events, custom_url, token) doorstations.append(doorstation) - _LOGGER.info('Connected to DoorBird "%s" as %s@%s', - doorstation.name, username, device_ip) + _LOGGER.info( + 'Connected to DoorBird "%s" as %s@%s', + doorstation.name, + username, + device_ip, + ) elif status[1] == 401: - _LOGGER.error("Authorization rejected by DoorBird for %s@%s", - username, device_ip) + _LOGGER.error( + "Authorization rejected by DoorBird for %s@%s", username, device_ip + ) return False else: - _LOGGER.error("Could not connect to DoorBird as %s@%s: Error %s", - username, device_ip, str(status[1])) + _LOGGER.error( + "Could not connect to DoorBird as %s@%s: Error %s", + username, + device_ip, + str(status[1]), + ) return False # Subscribe to doorbell or motion events @@ -83,12 +99,13 @@ def setup(hass, config): doorstation.register_events(hass) except HTTPError: hass.components.persistent_notification.create( - 'Doorbird configuration failed. Please verify that API ' - 'Operator permission is enabled for the Doorbird user. ' - 'A restart will be required once permissions have been ' - 'verified.', - title='Doorbird Configuration Failure', - notification_id='doorbird_schedule_error') + "Doorbird configuration failed. Please verify that API " + "Operator permission is enabled for the Doorbird user. " + "A restart will be required once permissions have been " + "verified.", + title="Doorbird Configuration Failure", + notification_id="doorbird_schedule_error", + ) return False @@ -96,7 +113,7 @@ def setup(hass, config): def _reset_device_favorites_handler(event): """Handle clearing favorites on device.""" - token = event.data.get('token') + token = event.data.get("token") if token is None: return @@ -104,7 +121,7 @@ def setup(hass, config): doorstation = get_doorstation_by_token(hass, token) if doorstation is None: - _LOGGER.error('Device not found for provided token.') + _LOGGER.error("Device not found for provided token.") # Clear webhooks favorites = doorstation.device.favorites() @@ -125,7 +142,7 @@ def get_doorstation_by_token(hass, token): return doorstation -class ConfiguredDoorBird(): +class ConfiguredDoorBird: """Attach additional information to pass along with configured device.""" def __init__(self, device, name, events, custom_url, token): @@ -170,8 +187,7 @@ class ConfiguredDoorBird(): self._register_event(hass_url, event) - _LOGGER.info('Successfully registered URL for %s on %s', - event, self.name) + _LOGGER.info("Successfully registered URL for %s on %s", event, self.name) @property def slug(self): @@ -179,33 +195,37 @@ class ConfiguredDoorBird(): return slugify(self._name) def _get_event_name(self, event): - return '{}_{}'.format(self.slug, event) + return "{}_{}".format(self.slug, event) def _register_event(self, hass_url, event): """Add a schedule entry in the device for a sensor.""" - url = '{}{}/{}?token={}'.format(hass_url, API_URL, event, self._token) + url = "{}{}/{}?token={}".format(hass_url, API_URL, event, self._token) # Register HA URL as webhook if not already, then get the ID if not self.webhook_is_registered(url): - self.device.change_favorite('http', 'Home Assistant ({})' - .format(event), url) + self.device.change_favorite( + "http", "Home Assistant ({})".format(event), url + ) fav_id = self.get_webhook_id(url) if not fav_id: - _LOGGER.warning('Could not find favorite for URL "%s". ' - 'Skipping sensor "%s"', url, event) + _LOGGER.warning( + 'Could not find favorite for URL "%s". ' 'Skipping sensor "%s"', + url, + event, + ) return def webhook_is_registered(self, url, favs=None) -> bool: """Return whether the given URL is registered as a device favorite.""" favs = favs if favs else self.device.favorites() - if 'http' not in favs: + if "http" not in favs: return False - for fav in favs['http'].values(): - if fav['value'] == url: + for fav in favs["http"].values(): + if fav["value"] == url: return True return False @@ -218,11 +238,11 @@ class ConfiguredDoorBird(): """ favs = favs if favs else self.device.favorites() - if 'http' not in favs: + if "http" not in favs: return None - for fav_id in favs['http']: - if favs['http'][fav_id]['value'] == url: + for fav_id in favs["http"]: + if favs["http"][fav_id]["value"] == url: return fav_id return None @@ -230,11 +250,11 @@ class ConfiguredDoorBird(): def get_event_data(self): """Get data to pass along with HA event.""" return { - 'timestamp': dt_util.utcnow().isoformat(), - 'live_video_url': self._device.live_video_url, - 'live_image_url': self._device.live_image_url, - 'rtsp_live_video_url': self._device.rtsp_live_video_url, - 'html5_viewer_url': self._device.html5_viewer_url + "timestamp": dt_util.utcnow().isoformat(), + "live_video_url": self._device.live_video_url, + "live_image_url": self._device.live_image_url, + "rtsp_live_video_url": self._device.rtsp_live_video_url, + "html5_viewer_url": self._device.html5_viewer_url, } @@ -243,34 +263,34 @@ class DoorBirdRequestView(HomeAssistantView): requires_auth = False url = API_URL - name = API_URL[1:].replace('/', ':') - extra_urls = [API_URL + '/{event}'] + name = API_URL[1:].replace("/", ":") + extra_urls = [API_URL + "/{event}"] # pylint: disable=no-self-use async def get(self, request, event): """Respond to requests from the device.""" from aiohttp import web - hass = request.app['hass'] - token = request.query.get('token') + hass = request.app["hass"] + + token = request.query.get("token") device = get_doorstation_by_token(hass, token) if device is None: - return web.Response(status=401, text='Invalid token provided.') + return web.Response(status=401, text="Invalid token provided.") if device: event_data = device.get_event_data() else: event_data = {} - if event == 'clear': - hass.bus.async_fire(RESET_DEVICE_FAVORITES, - {'token': token}) + if event == "clear": + hass.bus.async_fire(RESET_DEVICE_FAVORITES, {"token": token}) - message = 'HTTP Favorites cleared for {}'.format(device.slug) + message = "HTTP Favorites cleared for {}".format(device.slug) return web.Response(status=200, text=message) - hass.bus.async_fire('{}_{}'.format(DOMAIN, event), event_data) + hass.bus.async_fire("{}_{}".format(DOMAIN, event), event_data) - return web.Response(status=200, text='OK') + return web.Response(status=200, text="OK") diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index b4bd40c442c..eaae3f19236 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -21,26 +21,30 @@ _LOGGER = logging.getLogger(__name__) _TIMEOUT = 10 # seconds -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the DoorBird camera platform.""" for doorstation in hass.data[DOORBIRD_DOMAIN]: device = doorstation.device - async_add_entities([ - DoorBirdCamera( - device.live_image_url, - _CAMERA_LIVE.format(doorstation.name), - _LIVE_INTERVAL, - device.rtsp_live_video_url), - DoorBirdCamera( - device.history_image_url(1, 'doorbell'), - _CAMERA_LAST_VISITOR.format(doorstation.name), - _LAST_VISITOR_INTERVAL), - DoorBirdCamera( - device.history_image_url(1, 'motionsensor'), - _CAMERA_LAST_MOTION.format(doorstation.name), - _LAST_MOTION_INTERVAL), - ]) + async_add_entities( + [ + DoorBirdCamera( + device.live_image_url, + _CAMERA_LIVE.format(doorstation.name), + _LIVE_INTERVAL, + device.rtsp_live_video_url, + ), + DoorBirdCamera( + device.history_image_url(1, "doorbell"), + _CAMERA_LAST_VISITOR.format(doorstation.name), + _LAST_VISITOR_INTERVAL, + ), + DoorBirdCamera( + device.history_image_url(1, "motionsensor"), + _CAMERA_LAST_MOTION.format(doorstation.name), + _LAST_MOTION_INTERVAL, + ), + ] + ) class DoorBirdCamera(Camera): diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py index f3b1f5f059e..a907099cba4 100644 --- a/homeassistant/components/doorbird/switch.py +++ b/homeassistant/components/doorbird/switch.py @@ -8,7 +8,7 @@ from . import DOMAIN as DOORBIRD_DOMAIN _LOGGER = logging.getLogger(__name__) -IR_RELAY = '__ir_light__' +IR_RELAY = "__ir_light__" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -16,7 +16,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): switches = [] for doorstation in hass.data[DOORBIRD_DOMAIN]: - relays = doorstation.device.info()['RELAYS'] + relays = doorstation.device.info()["RELAYS"] relays.append(IR_RELAY) for relay in relays: @@ -71,8 +71,7 @@ class DoorBirdSwitch(SwitchDevice): def turn_off(self, **kwargs): """Turn off the relays is not needed. They are time-based.""" - raise NotImplementedError( - "DoorBird relays cannot be manually turned off.") + raise NotImplementedError("DoorBird relays cannot be manually turned off.") def update(self): """Wait for the correct amount of assumed time to pass.""" diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index 2a240c2a79e..29f0cc59392 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -6,20 +6,26 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, - DEVICE_DEFAULT_NAME) + CONF_USERNAME, + CONF_PASSWORD, + CONF_HOST, + CONF_PORT, + DEVICE_DEFAULT_NAME, +) from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -DOMAIN = 'dovado' +DOMAIN = "dovado" -CONFIG_SCHEMA = vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port, -}) +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + } +) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) @@ -30,8 +36,11 @@ def setup(hass, config): hass.data[DOMAIN] = DovadoData( dovado.Dovado( - config[CONF_USERNAME], config[CONF_PASSWORD], - config.get(CONF_HOST), config.get(CONF_PORT)) + config[CONF_USERNAME], + config[CONF_PASSWORD], + config.get(CONF_HOST), + config.get(CONF_PORT), + ) ) return True @@ -56,8 +65,7 @@ class DovadoData: self.state = self._client.state or {} if not self.state: return False - self.state.update( - connected=self.state.get("modem status") == "CONNECTED") + self.state.update(connected=self.state.get("modem status") == "CONNECTED") _LOGGER.debug("Received: %s", self.state) return True except OSError as error: diff --git a/homeassistant/components/dovado/notify.py b/homeassistant/components/dovado/notify.py index f9d9e5574a1..02ce994b1df 100644 --- a/homeassistant/components/dovado/notify.py +++ b/homeassistant/components/dovado/notify.py @@ -1,8 +1,7 @@ """Support for SMS notifications from the Dovado router.""" import logging -from homeassistant.components.notify import ( - ATTR_TARGET, BaseNotificationService) +from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from . import DOMAIN as DOVADO_DOMAIN diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index 7a825118fc6..d3374c8d02a 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -16,26 +16,23 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) -SENSOR_UPLOAD = 'upload' -SENSOR_DOWNLOAD = 'download' -SENSOR_SIGNAL = 'signal' -SENSOR_NETWORK = 'network' -SENSOR_SMS_UNREAD = 'sms' +SENSOR_UPLOAD = "upload" +SENSOR_DOWNLOAD = "download" +SENSOR_SIGNAL = "signal" +SENSOR_NETWORK = "network" +SENSOR_SMS_UNREAD = "sms" SENSORS = { - SENSOR_NETWORK: ('signal strength', 'Network', None, - 'mdi:access-point-network'), - SENSOR_SIGNAL: ('signal strength', 'Signal Strength', '%', 'mdi:signal'), - SENSOR_SMS_UNREAD: ('sms unread', 'SMS unread', '', - 'mdi:message-text-outline'), - SENSOR_UPLOAD: ('traffic modem tx', 'Sent', 'GB', 'mdi:cloud-upload'), - SENSOR_DOWNLOAD: ('traffic modem rx', 'Received', 'GB', - 'mdi:cloud-download'), + SENSOR_NETWORK: ("signal strength", "Network", None, "mdi:access-point-network"), + SENSOR_SIGNAL: ("signal strength", "Signal Strength", "%", "mdi:signal"), + SENSOR_SMS_UNREAD: ("sms unread", "SMS unread", "", "mdi:message-text-outline"), + SENSOR_UPLOAD: ("traffic modem tx", "Sent", "GB", "mdi:cloud-upload"), + SENSOR_DOWNLOAD: ("traffic modem rx", "Received", "GB", "mdi:cloud-download"), } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSORS)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSORS)])} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -103,5 +100,4 @@ class DovadoSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - return {k: v for k, v in self._data.state.items() - if k not in ['date', 'time']} + return {k: v for k, v in self._data.state.items() if k not in ["date", "time"]} diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 7e169acc5a3..0fe589f2765 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -12,31 +12,32 @@ from homeassistant.util import sanitize_filename _LOGGER = logging.getLogger(__name__) -ATTR_FILENAME = 'filename' -ATTR_SUBDIR = 'subdir' -ATTR_URL = 'url' -ATTR_OVERWRITE = 'overwrite' +ATTR_FILENAME = "filename" +ATTR_SUBDIR = "subdir" +ATTR_URL = "url" +ATTR_OVERWRITE = "overwrite" -CONF_DOWNLOAD_DIR = 'download_dir' +CONF_DOWNLOAD_DIR = "download_dir" -DOMAIN = 'downloader' -DOWNLOAD_FAILED_EVENT = 'download_failed' -DOWNLOAD_COMPLETED_EVENT = 'download_completed' +DOMAIN = "downloader" +DOWNLOAD_FAILED_EVENT = "download_failed" +DOWNLOAD_COMPLETED_EVENT = "download_completed" -SERVICE_DOWNLOAD_FILE = 'download_file' +SERVICE_DOWNLOAD_FILE = "download_file" -SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({ - vol.Required(ATTR_URL): cv.url, - vol.Optional(ATTR_SUBDIR): cv.string, - vol.Optional(ATTR_FILENAME): cv.string, - vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean, -}) +SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_URL): cv.url, + vol.Optional(ATTR_SUBDIR): cv.string, + vol.Optional(ATTR_FILENAME): cv.string, + vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DOWNLOAD_DIR): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_DOWNLOAD_DIR): cv.string})}, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -49,13 +50,14 @@ def setup(hass, config): if not os.path.isdir(download_path): _LOGGER.error( - "Download path %s does not exist. File Downloader not active", - download_path) + "Download path %s does not exist. File Downloader not active", download_path + ) return False def download_file(service): """Start thread to download file specified in the URL.""" + def do_download(): """Download the file.""" try: @@ -76,20 +78,18 @@ def setup(hass, config): if req.status_code != 200: _LOGGER.warning( - "downloading '%s' failed, status_code=%d", - url, - req.status_code) + "downloading '%s' failed, status_code=%d", url, req.status_code + ) hass.bus.fire( - "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), { - 'url': url, - 'filename': filename - }) + "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), + {"url": url, "filename": filename}, + ) else: - if filename is None and \ - 'content-disposition' in req.headers: - match = re.findall(r"filename=(\S+)", - req.headers['content-disposition']) + if filename is None and "content-disposition" in req.headers: + match = re.findall( + r"filename=(\S+)", req.headers["content-disposition"] + ) if match: filename = match[0].strip("'\" ") @@ -98,7 +98,7 @@ def setup(hass, config): filename = os.path.basename(url).strip() if not filename: - filename = 'ha_download' + filename = "ha_download" # Remove stuff to ruin paths filename = sanitize_filename(filename) @@ -130,24 +130,22 @@ def setup(hass, config): _LOGGER.debug("%s -> %s", url, final_path) - with open(final_path, 'wb') as fil: + with open(final_path, "wb") as fil: for chunk in req.iter_content(1024): fil.write(chunk) _LOGGER.debug("Downloading of %s done", url) hass.bus.fire( - "{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), { - 'url': url, - 'filename': filename - }) + "{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), + {"url": url, "filename": filename}, + ) except requests.exceptions.ConnectionError: _LOGGER.exception("ConnectionError occurred for %s", url) hass.bus.fire( - "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), { - 'url': url, - 'filename': filename - }) + "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), + {"url": url, "filename": filename}, + ) # Remove file if we started downloading but failed if final_path and os.path.isfile(final_path): @@ -155,7 +153,11 @@ def setup(hass, config): threading.Thread(target=do_download).start() - hass.services.register(DOMAIN, SERVICE_DOWNLOAD_FILE, download_file, - schema=SERVICE_DOWNLOAD_FILE_SCHEMA) + hass.services.register( + DOMAIN, + SERVICE_DOWNLOAD_FILE, + download_file, + schema=SERVICE_DOWNLOAD_FILE_SCHEMA, + ) return True diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 15b2b7fd0de..82a81118dbd 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -7,166 +7,98 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_DSMR_VERSION = 'dsmr_version' -CONF_RECONNECT_INTERVAL = 'reconnect_interval' -CONF_PRECISION = 'precision' +CONF_DSMR_VERSION = "dsmr_version" +CONF_RECONNECT_INTERVAL = "reconnect_interval" +CONF_PRECISION = "precision" -DEFAULT_DSMR_VERSION = '2.2' -DEFAULT_PORT = '/dev/ttyUSB0' +DEFAULT_DSMR_VERSION = "2.2" +DEFAULT_PORT = "/dev/ttyUSB0" DEFAULT_PRECISION = 3 -DOMAIN = 'dsmr' +DOMAIN = "dsmr" -ICON_GAS = 'mdi:fire' -ICON_POWER = 'mdi:flash' -ICON_POWER_FAILURE = 'mdi:flash-off' -ICON_SWELL_SAG = 'mdi:pulse' +ICON_GAS = "mdi:fire" +ICON_POWER = "mdi:flash" +ICON_POWER_FAILURE = "mdi:flash-off" +ICON_SWELL_SAG = "mdi:pulse" # Smart meter sends telegram every 10 seconds MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) RECONNECT_INTERVAL = 5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( - cv.string, vol.In(['5', '4', '2.2'])), - vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int, - vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( + cv.string, vol.In(["5", "4", "2.2"]) + ), + vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int, + vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the DSMR sensor.""" # Suppress logging - logging.getLogger('dsmr_parser').setLevel(logging.ERROR) + logging.getLogger("dsmr_parser").setLevel(logging.ERROR) from dsmr_parser import obis_references as obis_ref - from dsmr_parser.clients.protocol import ( - create_dsmr_reader, create_tcp_dsmr_reader) + from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader import serial dsmr_version = config[CONF_DSMR_VERSION] # Define list of name,obis mappings to generate entities obis_mapping = [ - [ - 'Power Consumption', - obis_ref.CURRENT_ELECTRICITY_USAGE - ], - [ - 'Power Production', - obis_ref.CURRENT_ELECTRICITY_DELIVERY - ], - [ - 'Power Tariff', - obis_ref.ELECTRICITY_ACTIVE_TARIFF - ], - [ - 'Power Consumption (low)', - obis_ref.ELECTRICITY_USED_TARIFF_1 - ], - [ - 'Power Consumption (normal)', - obis_ref.ELECTRICITY_USED_TARIFF_2 - ], - [ - 'Power Production (low)', - obis_ref.ELECTRICITY_DELIVERED_TARIFF_1 - ], - [ - 'Power Production (normal)', - obis_ref.ELECTRICITY_DELIVERED_TARIFF_2 - ], - [ - 'Power Consumption Phase L1', - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE - ], - [ - 'Power Consumption Phase L2', - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE - ], - [ - 'Power Consumption Phase L3', - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE - ], - [ - 'Power Production Phase L1', - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE - ], - [ - 'Power Production Phase L2', - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE - ], - [ - 'Power Production Phase L3', - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE - ], - [ - 'Long Power Failure Count', - obis_ref.LONG_POWER_FAILURE_COUNT - ], - [ - 'Voltage Sags Phase L1', - obis_ref.VOLTAGE_SAG_L1_COUNT - ], - [ - 'Voltage Sags Phase L2', - obis_ref.VOLTAGE_SAG_L2_COUNT - ], - [ - 'Voltage Sags Phase L3', - obis_ref.VOLTAGE_SAG_L3_COUNT - ], - [ - 'Voltage Swells Phase L1', - obis_ref.VOLTAGE_SWELL_L1_COUNT - ], - [ - 'Voltage Swells Phase L2', - obis_ref.VOLTAGE_SWELL_L2_COUNT - ], - [ - 'Voltage Swells Phase L3', - obis_ref.VOLTAGE_SWELL_L3_COUNT - ], - [ - 'Voltage Phase L1', - obis_ref.INSTANTANEOUS_VOLTAGE_L1 - ], - [ - 'Voltage Phase L2', - obis_ref.INSTANTANEOUS_VOLTAGE_L2 - ], - [ - 'Voltage Phase L3', - obis_ref.INSTANTANEOUS_VOLTAGE_L3 - ], + ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE], + ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY], + ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF], + ["Power Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL], + ["Power Consumption (low)", obis_ref.ELECTRICITY_USED_TARIFF_1], + ["Power Consumption (normal)", obis_ref.ELECTRICITY_USED_TARIFF_2], + ["Power Production (low)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], + ["Power Production (normal)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], + ["Power Consumption Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], + ["Power Consumption Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], + ["Power Consumption Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], + ["Power Production Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], + ["Power Production Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], + ["Power Production Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], + ["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT], + ["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT], + ["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_COUNT], + ["Voltage Sags Phase L3", obis_ref.VOLTAGE_SAG_L3_COUNT], + ["Voltage Swells Phase L1", obis_ref.VOLTAGE_SWELL_L1_COUNT], + ["Voltage Swells Phase L2", obis_ref.VOLTAGE_SWELL_L2_COUNT], + ["Voltage Swells Phase L3", obis_ref.VOLTAGE_SWELL_L3_COUNT], + ["Voltage Phase L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1], + ["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2], + ["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3], ] # Generate device entities devices = [DSMREntity(name, obis, config) for name, obis in obis_mapping] # Protocol version specific obis - if dsmr_version in ('4', '5'): + if dsmr_version in ("4", "5"): gas_obis = obis_ref.HOURLY_GAS_METER_READING else: gas_obis = obis_ref.GAS_METER_READING # Add gas meter reading and derivative for usage devices += [ - DSMREntity('Gas Consumption', gas_obis, config), - DerivativeDSMREntity('Hourly Gas Consumption', gas_obis, config), + DSMREntity("Gas Consumption", gas_obis, config), + DerivativeDSMREntity("Hourly Gas Consumption", gas_obis, config), ] async_add_entities(devices) @@ -182,23 +114,33 @@ async def async_setup_platform(hass, config, async_add_entities, # serial and calls update_entities_telegram to update entities on arrival if CONF_HOST in config: reader_factory = partial( - create_tcp_dsmr_reader, config[CONF_HOST], config[CONF_PORT], - config[CONF_DSMR_VERSION], update_entities_telegram, - loop=hass.loop) + create_tcp_dsmr_reader, + config[CONF_HOST], + config[CONF_PORT], + config[CONF_DSMR_VERSION], + update_entities_telegram, + loop=hass.loop, + ) else: reader_factory = partial( - create_dsmr_reader, config[CONF_PORT], config[CONF_DSMR_VERSION], - update_entities_telegram, loop=hass.loop) + create_dsmr_reader, + config[CONF_PORT], + config[CONF_DSMR_VERSION], + update_entities_telegram, + loop=hass.loop, + ) async def connect_and_reconnect(): """Connect to DSMR and keep reconnecting until Home Assistant stops.""" while hass.state != CoreState.stopping: # Start DSMR asyncio.Protocol reader try: - transport, protocol = await hass.loop.create_task( - reader_factory()) - except (serial.serialutil.SerialException, ConnectionRefusedError, - TimeoutError): + transport, protocol = await hass.loop.create_task(reader_factory()) + except ( + serial.serialutil.SerialException, + ConnectionRefusedError, + TimeoutError, + ): # Log any error while establishing connection and drop to retry # connection wait _LOGGER.exception("Error connecting to DSMR") @@ -207,7 +149,8 @@ async def async_setup_platform(hass, config, async_add_entities, if transport: # Register listener to close transport on HA shutdown stop_listener = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, transport.close) + EVENT_HOMEASSISTANT_STOP, transport.close + ) # Wait for reader to close await protocol.wait_closed() @@ -257,13 +200,13 @@ class DSMREntity(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - if 'Sags' in self._name or 'Swells' in self.name: + if "Sags" in self._name or "Swells" in self.name: return ICON_SWELL_SAG - if 'Failure' in self._name: + if "Failure" in self._name: return ICON_POWER_FAILURE - if 'Power' in self._name: + if "Power" in self._name: return ICON_POWER - if 'Gas' in self._name: + if "Gas" in self._name: return ICON_GAS @property @@ -271,7 +214,7 @@ class DSMREntity(Entity): """Return the state of sensor, if available, translate if needed.""" from dsmr_parser import obis_references as obis - value = self.get_dsmr_object_attr('value') + value = self.get_dsmr_object_attr("value") if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value) @@ -289,17 +232,17 @@ class DSMREntity(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return self.get_dsmr_object_attr('unit') + return self.get_dsmr_object_attr("unit") @staticmethod def translate_tariff(value): """Convert 2/1 to normal/low.""" # DSMR V2.2: Note: Rate code 1 is used for low rate and rate code 2 is # used for normal rate. - if value == '0002': - return 'normal' - if value == '0001': - return 'low' + if value == "0002": + return "normal" + if value == "0001": + return "low" return None @@ -331,9 +274,9 @@ class DerivativeDSMREntity(DSMREntity): """ # check if the timestamp for the object differs from the previous one - timestamp = self.get_dsmr_object_attr('datetime') + timestamp = self.get_dsmr_object_attr("datetime") if timestamp and timestamp != self._previous_timestamp: - current_reading = self.get_dsmr_object_attr('value') + current_reading = self.get_dsmr_object_attr("value") if self._previous_reading is None: # Can't calculate rate without previous datapoint @@ -352,6 +295,6 @@ class DerivativeDSMREntity(DSMREntity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, per hour, if any.""" - unit = self.get_dsmr_object_attr('unit') + unit = self.get_dsmr_object_attr("unit") if unit: - return unit + '/h' + return unit + "/h" diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index 8610a1e7f70..b904d004c61 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -10,20 +10,23 @@ from homeassistant.const import CONF_NAME _LOGGER = logging.getLogger(__name__) -CONF_IP_ADDRESS = 'ip' -CONF_VERSION = 'version' +CONF_IP_ADDRESS = "ip" +CONF_VERSION = "version" -DEFAULT_NAME = 'Current Energy Usage' +DEFAULT_NAME = "Current Energy Usage" DEFAULT_VERSION = 1 -ICON = 'mdi:flash' +ICON = "mdi:flash" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): - vol.All(vol.Coerce(int), vol.Any(1, 2)) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.All( + vol.Coerce(int), vol.Any(1, 2) + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -81,14 +84,16 @@ class DteEnergyBridgeSensor(Entity): response = requests.get(self._url, timeout=5) except (requests.exceptions.RequestException, ValueError): _LOGGER.warning( - 'Could not update status for DTE Energy Bridge (%s)', - self._name) + "Could not update status for DTE Energy Bridge (%s)", self._name + ) return if response.status_code != 200: _LOGGER.warning( - 'Invalid status_code from DTE Energy Bridge: %s (%s)', - response.status_code, self._name) + "Invalid status_code from DTE Energy Bridge: %s (%s)", + response.status_code, + self._name, + ) return response_split = response.text.split() @@ -96,7 +101,9 @@ class DteEnergyBridgeSensor(Entity): if len(response_split) != 2: _LOGGER.warning( 'Invalid response from DTE Energy Bridge: "%s" (%s)', - response.text, self._name) + response.text, + self._name, + ) return val = float(response_split[0]) @@ -107,7 +114,7 @@ class DteEnergyBridgeSensor(Entity): # Limiting to version 1 because version 2 apparently always returns # values in the format 000000.000 kW, but the scaling is Watts # NOT kWatts - if self._version == 1 and '.' in response_split[0]: + if self._version == 1 and "." in response_split[0]: self._state = val else: self._state = val / 1000 diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index 7a70d7af3a7..203cfb1e27c 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -20,30 +20,32 @@ from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -_RESOURCE = 'https://data.dublinked.ie/cgi-bin/rtpi/realtimebusinformation' +_RESOURCE = "https://data.dublinked.ie/cgi-bin/rtpi/realtimebusinformation" -ATTR_STOP_ID = 'Stop ID' -ATTR_ROUTE = 'Route' -ATTR_DUE_IN = 'Due in' -ATTR_DUE_AT = 'Due at' -ATTR_NEXT_UP = 'Later Bus' +ATTR_STOP_ID = "Stop ID" +ATTR_ROUTE = "Route" +ATTR_DUE_IN = "Due in" +ATTR_DUE_AT = "Due at" +ATTR_NEXT_UP = "Later Bus" ATTRIBUTION = "Data provided by data.dublinked.ie" -CONF_STOP_ID = 'stopid' -CONF_ROUTE = 'route' +CONF_STOP_ID = "stopid" +CONF_ROUTE = "route" -DEFAULT_NAME = 'Next Bus' -ICON = 'mdi:bus' +DEFAULT_NAME = "Next Bus" +ICON = "mdi:bus" SCAN_INTERVAL = timedelta(minutes=1) -TIME_STR_FORMAT = '%H:%M' +TIME_STR_FORMAT = "%H:%M" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_STOP_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ROUTE, default=""): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STOP_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ROUTE, default=""): cv.string, + } +) def due_in_minutes(timestamp): @@ -51,8 +53,9 @@ def due_in_minutes(timestamp): The timestamp should be in the format day/month/year hour/minute/second """ - diff = datetime.strptime( - timestamp, "%d/%m/%Y %H:%M:%S") - dt_util.now().replace(tzinfo=None) + diff = datetime.strptime(timestamp, "%d/%m/%Y %H:%M:%S") - dt_util.now().replace( + tzinfo=None + ) return str(int(diff.total_seconds() / 60)) @@ -103,13 +106,13 @@ class DublinPublicTransportSensor(Entity): ATTR_STOP_ID: self._stop, ATTR_ROUTE: self._times[0][ATTR_ROUTE], ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_NEXT_UP: next_up + ATTR_NEXT_UP: next_up, } @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" - return 'min' + return "min" @property def icon(self): @@ -133,48 +136,48 @@ class PublicTransportData: """Initialize the data object.""" self.stop = stop self.route = route - self.info = [{ATTR_DUE_AT: 'n/a', - ATTR_ROUTE: self.route, - ATTR_DUE_IN: 'n/a'}] + self.info = [{ATTR_DUE_AT: "n/a", ATTR_ROUTE: self.route, ATTR_DUE_IN: "n/a"}] def update(self): """Get the latest data from opendata.ch.""" params = {} - params['stopid'] = self.stop + params["stopid"] = self.stop if self.route: - params['routeid'] = self.route + params["routeid"] = self.route - params['maxresults'] = 2 - params['format'] = 'json' + params["maxresults"] = 2 + params["format"] = "json" response = requests.get(_RESOURCE, params, timeout=10) if response.status_code != 200: - self.info = [{ATTR_DUE_AT: 'n/a', - ATTR_ROUTE: self.route, - ATTR_DUE_IN: 'n/a'}] + self.info = [ + {ATTR_DUE_AT: "n/a", ATTR_ROUTE: self.route, ATTR_DUE_IN: "n/a"} + ] return result = response.json() - if str(result['errorcode']) != '0': - self.info = [{ATTR_DUE_AT: 'n/a', - ATTR_ROUTE: self.route, - ATTR_DUE_IN: 'n/a'}] + if str(result["errorcode"]) != "0": + self.info = [ + {ATTR_DUE_AT: "n/a", ATTR_ROUTE: self.route, ATTR_DUE_IN: "n/a"} + ] return self.info = [] - for item in result['results']: - due_at = item.get('departuredatetime') - route = item.get('route') + for item in result["results"]: + due_at = item.get("departuredatetime") + route = item.get("route") if due_at is not None and route is not None: - bus_data = {ATTR_DUE_AT: due_at, - ATTR_ROUTE: route, - ATTR_DUE_IN: due_in_minutes(due_at)} + bus_data = { + ATTR_DUE_AT: due_at, + ATTR_ROUTE: route, + ATTR_DUE_IN: due_in_minutes(due_at), + } self.info.append(bus_data) if not self.info: - self.info = [{ATTR_DUE_AT: 'n/a', - ATTR_ROUTE: self.route, - ATTR_DUE_IN: 'n/a'}] + self.info = [ + {ATTR_DUE_AT: "n/a", ATTR_ROUTE: self.route, ATTR_DUE_IN: "n/a"} + ] diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index 9899a0af98e..7d677580177 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -11,26 +11,29 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) -ATTR_TXT = 'txt' +ATTR_TXT = "txt" -DOMAIN = 'duckdns' +DOMAIN = "duckdns" INTERVAL = timedelta(minutes=5) -SERVICE_SET_TXT = 'set_txt' +SERVICE_SET_TXT = "set_txt" -UPDATE_URL = 'https://www.duckdns.org/update' +UPDATE_URL = "https://www.duckdns.org/update" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DOMAIN): cv.string, - vol.Required(CONF_ACCESS_TOKEN): cv.string, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_DOMAIN): cv.string, + vol.Required(CONF_ACCESS_TOKEN): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -SERVICE_TXT_SCHEMA = vol.Schema({ - vol.Required(ATTR_TXT): vol.Any(None, cv.string) -}) +SERVICE_TXT_SCHEMA = vol.Schema({vol.Required(ATTR_TXT): vol.Any(None, cv.string)}) async def async_setup(hass, config): @@ -50,13 +53,12 @@ async def async_setup(hass, config): async def update_domain_service(call): """Update the DuckDNS entry.""" - await _update_duckdns( - session, domain, token, txt=call.data[ATTR_TXT]) + await _update_duckdns(session, domain, token, txt=call.data[ATTR_TXT]) async_track_time_interval(hass, update_domain_interval, INTERVAL) hass.services.async_register( - DOMAIN, SERVICE_SET_TXT, update_domain_service, - schema=SERVICE_TXT_SCHEMA) + DOMAIN, SERVICE_SET_TXT, update_domain_service, schema=SERVICE_TXT_SCHEMA + ) return result @@ -64,29 +66,25 @@ async def async_setup(hass, config): _SENTINEL = object() -async def _update_duckdns(session, domain, token, *, txt=_SENTINEL, - clear=False): +async def _update_duckdns(session, domain, token, *, txt=_SENTINEL, clear=False): """Update DuckDNS.""" - params = { - 'domains': domain, - 'token': token, - } + params = {"domains": domain, "token": token} if txt is not _SENTINEL: if txt is None: # Pass in empty txt value to indicate it's clearing txt record - params['txt'] = '' + params["txt"] = "" clear = True else: - params['txt'] = txt + params["txt"] = txt if clear: - params['clear'] = 'true' + params["clear"] = "true" resp = await session.get(UPDATE_URL, params=params) body = await resp.text() - if body != 'OK': + if body != "OK": _LOGGER.warning("Updating DuckDNS domain failed: %s", domain) return False diff --git a/homeassistant/components/duke_energy/sensor.py b/homeassistant/components/duke_energy/sensor.py index e364e35048b..b8a9bec5db8 100644 --- a/homeassistant/components/duke_energy/sensor.py +++ b/homeassistant/components/duke_energy/sensor.py @@ -10,10 +10,9 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} +) LAST_BILL_USAGE = "last_bills_usage" LAST_BILL_AVERAGE_USAGE = "last_bills_average_usage" @@ -25,9 +24,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): from pydukeenergy.api import DukeEnergy, DukeEnergyException try: - duke = DukeEnergy(config[CONF_USERNAME], - config[CONF_PASSWORD], - update_interval=120) + duke = DukeEnergy( + config[CONF_USERNAME], config[CONF_PASSWORD], update_interval=120 + ) except DukeEnergyException: _LOGGER.error("Failed to set up Duke Energy") return @@ -68,7 +67,7 @@ class DukeEnergyMeter(Entity): attributes = { LAST_BILL_USAGE: self.duke_meter.get_total(), LAST_BILL_AVERAGE_USAGE: self.duke_meter.get_average(), - LAST_BILL_DAYS_BILLED: self.duke_meter.get_days_billed() + LAST_BILL_DAYS_BILLED: self.duke_meter.get_days_billed(), } return attributes diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index a5698c74654..95e8cac3dbd 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -1,30 +1,47 @@ """DuneHD implementation of the media player.""" import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON) + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) + CONF_HOST, + CONF_NAME, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +) import homeassistant.helpers.config_validation as cv -DEFAULT_NAME = 'DuneHD' +DEFAULT_NAME = "DuneHD" -CONF_SOURCES = 'sources' +CONF_SOURCES = "sources" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_SOURCES): vol.Schema({cv.string: cv.string}), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_SOURCES): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) -DUNEHD_PLAYER_SUPPORT = \ - SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_PLAY +DUNEHD_PLAYER_SUPPORT = ( + SUPPORT_PAUSE + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_PLAY +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -59,13 +76,13 @@ class DuneHDPlayerEntity(MediaPlayerDevice): def state(self): """Return player state.""" state = STATE_OFF - if 'playback_position' in self._state: + if "playback_position" in self._state: state = STATE_PLAYING - if self._state['player_state'] in ('playing', 'buffering'): + if self._state["player_state"] in ("playing", "buffering"): state = STATE_PLAYING - if int(self._state.get('playback_speed', 1234)) == 0: + if int(self._state.get("playback_speed", 1234)) == 0: state = STATE_PAUSED - if self._state['player_state'] == 'navigator': + if self._state["player_state"] == "navigator": state = STATE_ON return state @@ -77,12 +94,12 @@ class DuneHDPlayerEntity(MediaPlayerDevice): @property def volume_level(self): """Return the volume level of the media player (0..1).""" - return int(self._state.get('playback_volume', 0)) / 100 + return int(self._state.get("playback_volume", 0)) / 100 @property def is_volume_muted(self): """Return a boolean if volume is currently muted.""" - return int(self._state.get('playback_mute', 0)) == 1 + return int(self._state.get("playback_mute", 0)) == 1 @property def source_list(self): @@ -133,16 +150,16 @@ class DuneHDPlayerEntity(MediaPlayerDevice): self.__update_title() if self._media_title: return self._media_title - return self._state.get('playback_url', 'Not playing') + return self._state.get("playback_url", "Not playing") def __update_title(self): - if self._state['player_state'] == 'bluray_playback': - self._media_title = 'Blu-Ray' - elif 'playback_url' in self._state: + if self._state["player_state"] == "bluray_playback": + self._media_title = "Blu-Ray" + elif "playback_url" in self._state: sources = self._sources sval = sources.values() skey = sources.keys() - pburl = self._state['playback_url'] + pburl = self._state["playback_url"] if pburl in sval: self._media_title = list(skey)[list(sval).index(pburl)] else: diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 9e61c9e3196..a019a5c7b3a 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -21,8 +21,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_NAME, CONF_MONITORED_CONDITIONS) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.util import Throttle import homeassistant.util.dt as dt_util from homeassistant.components.rest.sensor import RestData @@ -31,26 +30,34 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Data provided by DWD" -DEFAULT_NAME = 'DWD-Weather-Warnings' +DEFAULT_NAME = "DWD-Weather-Warnings" -CONF_REGION_NAME = 'region_name' +CONF_REGION_NAME = "region_name" SCAN_INTERVAL = timedelta(minutes=15) MONITORED_CONDITIONS = { - 'current_warning_level': ['Current Warning Level', - None, 'mdi:close-octagon-outline'], - 'advance_warning_level': ['Advance Warning Level', - None, 'mdi:close-octagon-outline'], + "current_warning_level": [ + "Current Warning Level", + None, + "mdi:close-octagon-outline", + ], + "advance_warning_level": [ + "Advance Warning Level", + None, + "mdi:close-octagon-outline", + ], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_REGION_NAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, - default=list(MONITORED_CONDITIONS)): - vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_REGION_NAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional( + CONF_MONITORED_CONDITIONS, default=list(MONITORED_CONDITIONS) + ): vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -60,8 +67,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): api = DwdWeatherWarningsAPI(region_name) - sensors = [DwdWeatherWarningsSensor(api, name, condition) - for condition in config[CONF_MONITORED_CONDITIONS]] + sensors = [ + DwdWeatherWarningsSensor(api, name, condition) + for condition in config[CONF_MONITORED_CONDITIONS] + ] add_entities(sensors, True) @@ -106,50 +115,50 @@ class DwdWeatherWarningsSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the DWD-Weather-Warnings.""" - data = { - ATTR_ATTRIBUTION: ATTRIBUTION, - 'region_name': self._api.region_name - } + data = {ATTR_ATTRIBUTION: ATTRIBUTION, "region_name": self._api.region_name} if self._api.region_id is not None: - data['region_id'] = self._api.region_id + data["region_id"] = self._api.region_id if self._api.region_state is not None: - data['region_state'] = self._api.region_state + data["region_state"] = self._api.region_state - if self._api.data['time'] is not None: - data['last_update'] = dt_util.as_local( - dt_util.utc_from_timestamp(self._api.data['time'] / 1000)) + if self._api.data["time"] is not None: + data["last_update"] = dt_util.as_local( + dt_util.utc_from_timestamp(self._api.data["time"] / 1000) + ) - if self._var_id == 'current_warning_level': - prefix = 'current' - elif self._var_id == 'advance_warning_level': - prefix = 'advance' + if self._var_id == "current_warning_level": + prefix = "current" + elif self._var_id == "advance_warning_level": + prefix = "advance" else: - raise Exception('Unknown warning type') + raise Exception("Unknown warning type") - data['warning_count'] = self._api.data[prefix + '_warning_count'] + data["warning_count"] = self._api.data[prefix + "_warning_count"] i = 0 - for event in self._api.data[prefix + '_warnings']: + for event in self._api.data[prefix + "_warnings"]: i = i + 1 - data['warning_{}_name'.format(i)] = event['event'] - data['warning_{}_level'.format(i)] = event['level'] - data['warning_{}_type'.format(i)] = event['type'] - if event['headline']: - data['warning_{}_headline'.format(i)] = event['headline'] - if event['description']: - data['warning_{}_description'.format(i)] = event['description'] - if event['instruction']: - data['warning_{}_instruction'.format(i)] = event['instruction'] + data["warning_{}_name".format(i)] = event["event"] + data["warning_{}_level".format(i)] = event["level"] + data["warning_{}_type".format(i)] = event["type"] + if event["headline"]: + data["warning_{}_headline".format(i)] = event["headline"] + if event["description"]: + data["warning_{}_description".format(i)] = event["description"] + if event["instruction"]: + data["warning_{}_instruction".format(i)] = event["instruction"] - if event['start'] is not None: - data['warning_{}_start'.format(i)] = dt_util.as_local( - dt_util.utc_from_timestamp(event['start'] / 1000)) + if event["start"] is not None: + data["warning_{}_start".format(i)] = dt_util.as_local( + dt_util.utc_from_timestamp(event["start"] / 1000) + ) - if event['end'] is not None: - data['warning_{}_end'.format(i)] = dt_util.as_local( - dt_util.utc_from_timestamp(event['end'] / 1000)) + if event["end"] is not None: + data["warning_{}_end".format(i)] = dt_util.as_local( + dt_util.utc_from_timestamp(event["end"] / 1000) + ) return data @@ -169,13 +178,13 @@ class DwdWeatherWarningsAPI: def __init__(self, region_name): """Initialize the data object.""" resource = "{}{}{}?{}".format( - 'https://', - 'www.dwd.de', - '/DWD/warnungen/warnapp_landkreise/json/warnings.json', - 'jsonp=loadWarnings' + "https://", + "www.dwd.de", + "/DWD/warnungen/warnapp_landkreise/json/warnings.json", + "jsonp=loadWarnings", ) - self._rest = RestData('GET', resource, None, None, None, True) + self._rest = RestData("GET", resource, None, None, None, True) self.region_name = region_name self.region_id = None self.region_state = None @@ -189,20 +198,21 @@ class DwdWeatherWarningsAPI: try: self._rest.update() - json_string = self._rest.data[24:len(self._rest.data) - 2] + json_string = self._rest.data[24 : len(self._rest.data) - 2] json_obj = json.loads(json_string) - data = {'time': json_obj['time']} + data = {"time": json_obj["time"]} for mykey, myvalue in { - 'current': 'warnings', - 'advance': 'vorabInformation' + "current": "warnings", + "advance": "vorabInformation", }.items(): - _LOGGER.debug("Found %d %s global DWD warnings", - len(json_obj[myvalue]), mykey) + _LOGGER.debug( + "Found %d %s global DWD warnings", len(json_obj[myvalue]), mykey + ) - data['{}_warning_level'.format(mykey)] = 0 + data["{}_warning_level".format(mykey)] = 0 my_warnings = [] if self.region_id is not None: @@ -214,26 +224,25 @@ class DwdWeatherWarningsAPI: # loop through all items to find warnings, region_id # and region_state for region_name for key in json_obj[myvalue]: - my_region = json_obj[myvalue][key][0]['regionName'] + my_region = json_obj[myvalue][key][0]["regionName"] if my_region != self.region_name: continue my_warnings = json_obj[myvalue][key] - my_state = json_obj[myvalue][key][0]['stateShort'] + my_state = json_obj[myvalue][key][0]["stateShort"] self.region_id = key self.region_state = my_state break # Get max warning level - maxlevel = data['{}_warning_level'.format(mykey)] + maxlevel = data["{}_warning_level".format(mykey)] for event in my_warnings: - if event['level'] >= maxlevel: - data['{}_warning_level'.format(mykey)] = event['level'] + if event["level"] >= maxlevel: + data["{}_warning_level".format(mykey)] = event["level"] - data['{}_warning_count'.format(mykey)] = len(my_warnings) - data['{}_warnings'.format(mykey)] = my_warnings + data["{}_warning_count".format(mykey)] = len(my_warnings) + data["{}_warnings".format(mykey)] = my_warnings - _LOGGER.debug("Found %d %s local DWD warnings", - len(my_warnings), mykey) + _LOGGER.debug("Found %d %s local DWD warnings", len(my_warnings), mykey) self.data = data self.available = True diff --git a/homeassistant/components/dweet/__init__.py b/homeassistant/components/dweet/__init__.py index 148eeeec9a4..bf1298479c3 100644 --- a/homeassistant/components/dweet/__init__.py +++ b/homeassistant/components/dweet/__init__.py @@ -5,24 +5,34 @@ from datetime import timedelta import voluptuous as vol from homeassistant.const import ( - CONF_NAME, CONF_WHITELIST, EVENT_STATE_CHANGED, STATE_UNKNOWN) + CONF_NAME, + CONF_WHITELIST, + EVENT_STATE_CHANGED, + STATE_UNKNOWN, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers import state as state_helper from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -DOMAIN = 'dweet' +DOMAIN = "dweet" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_WHITELIST, default=[]): - vol.All(cv.ensure_list, [cv.entity_id]), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_WHITELIST, default=[]): vol.All( + cv.ensure_list, [cv.entity_id] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -34,9 +44,12 @@ def setup(hass, config): def dweet_event_listener(event): """Listen for new messages on the bus and sends them to Dweet.io.""" - state = event.data.get('new_state') - if state is None or state.state in (STATE_UNKNOWN, '') \ - or state.entity_id not in whitelist: + state = event.data.get("new_state") + if ( + state is None + or state.state in (STATE_UNKNOWN, "") + or state.entity_id not in whitelist + ): return try: @@ -44,7 +57,7 @@ def setup(hass, config): except ValueError: _state = state.state - json_body[state.attributes.get('friendly_name')] = _state + json_body[state.attributes.get("friendly_name")] = _state send_data(name, json_body) @@ -57,6 +70,7 @@ def setup(hass, config): def send_data(name, msg): """Send the collected data to Dweet.io.""" import dweepy + try: dweepy.dweet_for(name, msg) except dweepy.DweepyError: diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index 55f3c5341a3..937de9b030a 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -8,21 +8,27 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_DEVICE) + CONF_NAME, + CONF_VALUE_TEMPLATE, + CONF_UNIT_OF_MEASUREMENT, + CONF_DEVICE, +) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Dweet.io Sensor' +DEFAULT_NAME = "Dweet.io Sensor" SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICE): cv.string, - vol.Required(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_DEVICE): cv.string, + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -37,12 +43,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): value_template.hass = hass try: - content = json.dumps(dweepy.get_latest_dweet_for(device)[0]['content']) + content = json.dumps(dweepy.get_latest_dweet_for(device)[0]["content"]) except dweepy.DweepyError: _LOGGER.error("Device/thing %s could not be found", device) return - if value_template.render_with_possible_json_value(content) == '': + if value_template.render_with_possible_json_value(content) == "": _LOGGER.error("%s was not found", value_template) return @@ -85,9 +91,10 @@ class DweetSensor(Entity): if self.dweet.data is None: self._state = None else: - values = json.dumps(self.dweet.data[0]['content']) + values = json.dumps(self.dweet.data[0]["content"]) self._state = self._value_template.render_with_possible_json_value( - values, None) + values, None + ) class DweetData: diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py index fdba263d4ca..7f247be6bcc 100644 --- a/homeassistant/components/dyson/__init__.py +++ b/homeassistant/components/dyson/__init__.py @@ -4,33 +4,36 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_DEVICES, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME) +from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers import discovery _LOGGER = logging.getLogger(__name__) -CONF_LANGUAGE = 'language' -CONF_RETRY = 'retry' +CONF_LANGUAGE = "language" +CONF_RETRY = "retry" DEFAULT_TIMEOUT = 5 DEFAULT_RETRY = 10 -DYSON_DEVICES = 'dyson_devices' -DYSON_PLATFORMS = ['sensor', 'fan', 'vacuum', 'climate', 'air_quality'] +DYSON_DEVICES = "dyson_devices" +DYSON_PLATFORMS = ["sensor", "fan", "vacuum", "climate", "air_quality"] -DOMAIN = 'dyson' +DOMAIN = "dyson" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_LANGUAGE): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, - vol.Optional(CONF_DEVICES, default=[]): - vol.All(cv.ensure_list, [dict]), - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_LANGUAGE): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, + vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list, [dict]), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -41,9 +44,12 @@ def setup(hass, config): hass.data[DYSON_DEVICES] = [] from libpurecool.dyson import DysonAccount - dyson_account = DysonAccount(config[DOMAIN].get(CONF_USERNAME), - config[DOMAIN].get(CONF_PASSWORD), - config[DOMAIN].get(CONF_LANGUAGE)) + + dyson_account = DysonAccount( + config[DOMAIN].get(CONF_USERNAME), + config[DOMAIN].get(CONF_PASSWORD), + config[DOMAIN].get(CONF_LANGUAGE), + ) logged = dyson_account.login() @@ -59,8 +65,9 @@ def setup(hass, config): if CONF_DEVICES in config[DOMAIN] and config[DOMAIN].get(CONF_DEVICES): configured_devices = config[DOMAIN].get(CONF_DEVICES) for device in configured_devices: - dyson_device = next((d for d in dyson_devices if - d.serial == device["device_id"]), None) + dyson_device = next( + (d for d in dyson_devices if d.serial == device["device_id"]), None + ) if dyson_device: try: connected = dyson_device.connect(device["device_ip"]) @@ -68,20 +75,26 @@ def setup(hass, config): _LOGGER.info("Connected to device %s", dyson_device) hass.data[DYSON_DEVICES].append(dyson_device) else: - _LOGGER.warning("Unable to connect to device %s", - dyson_device) + _LOGGER.warning("Unable to connect to device %s", dyson_device) except OSError as ose: - _LOGGER.error("Unable to connect to device %s: %s", - str(dyson_device.network_device), str(ose)) + _LOGGER.error( + "Unable to connect to device %s: %s", + str(dyson_device.network_device), + str(ose), + ) else: _LOGGER.warning( - "Unable to find device %s in Dyson account", - device["device_id"]) + "Unable to find device %s in Dyson account", device["device_id"] + ) else: # Not yet reliable for device in dyson_devices: - _LOGGER.info("Trying to connect to device %s with timeout=%i " - "and retry=%i", device, timeout, retry) + _LOGGER.info( + "Trying to connect to device %s with timeout=%i " "and retry=%i", + device, + timeout, + retry, + ) connected = device.auto_connect(timeout, retry) if connected: _LOGGER.info("Connected to device %s", device) diff --git a/homeassistant/components/dyson/air_quality.py b/homeassistant/components/dyson/air_quality.py index 238b8b6934d..0276e47ed61 100644 --- a/homeassistant/components/dyson/air_quality.py +++ b/homeassistant/components/dyson/air_quality.py @@ -4,13 +4,13 @@ import logging from homeassistant.components.air_quality import AirQualityEntity, DOMAIN from . import DYSON_DEVICES -ATTRIBUTION = 'Dyson purifier air quality sensor' +ATTRIBUTION = "Dyson purifier air quality sensor" _LOGGER = logging.getLogger(__name__) -DYSON_AIQ_DEVICES = 'dyson_aiq_devices' +DYSON_AIQ_DEVICES = "dyson_aiq_devices" -ATTR_VOC = 'volatile_organic_compounds' +ATTR_VOC = "volatile_organic_compounds" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -25,8 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Get Dyson Devices from parent component device_ids = [device.unique_id for device in hass.data[DYSON_AIQ_DEVICES]] for device in hass.data[DYSON_DEVICES]: - if isinstance(device, DysonPureCool) and \ - device.serial not in device_ids: + if isinstance(device, DysonPureCool) and device.serial not in device_ids: hass.data[DYSON_AIQ_DEVICES].append(DysonAirSensor(device)) add_entities(hass.data[DYSON_AIQ_DEVICES]) @@ -43,18 +42,20 @@ class DysonAirSensor(AirQualityEntity): async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.async_add_executor_job( - self._device.add_message_listener, self.on_message) + self._device.add_message_listener, self.on_message + ) def on_message(self, message): """Handle new messages which are received from the fan.""" - from libpurecool.dyson_pure_state_v2 import \ - DysonEnvironmentalSensorV2State + from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State - _LOGGER.debug('%s: Message received for %s device: %s', - DOMAIN, self.name, message) - if (self._old_value is None or - self._old_value != self._device.environmental_state) and \ - isinstance(message, DysonEnvironmentalSensorV2State): + _LOGGER.debug( + "%s: Message received for %s device: %s", DOMAIN, self.name, message + ) + if ( + self._old_value is None + or self._old_value != self._device.environmental_state + ) and isinstance(message, DysonEnvironmentalSensorV2State): self._old_value = self._device.environmental_state self.schedule_update_ha_state() @@ -76,10 +77,12 @@ class DysonAirSensor(AirQualityEntity): @property def air_quality_index(self): """Return the Air Quality Index (AQI).""" - return max(self.particulate_matter_2_5, - self.particulate_matter_10, - self.nitrogen_dioxide, - self.volatile_organic_compounds) + return max( + self.particulate_matter_2_5, + self.particulate_matter_10, + self.nitrogen_dioxide, + self.volatile_organic_compounds, + ) @property def particulate_matter_2_5(self): @@ -106,8 +109,7 @@ class DysonAirSensor(AirQualityEntity): def volatile_organic_compounds(self): """Return the VOC (Volatile Organic Compounds) level.""" if self._device.environmental_state: - return int(self._device. - environmental_state.volatile_organic_compounds) + return int(self._device.environmental_state.volatile_organic_compounds) return None @property diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index f86579a316a..90c19e9de88 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -7,9 +7,16 @@ from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, HVAC_MODE_COOL, - HVAC_MODE_HEAT, SUPPORT_FAN_MODE, FAN_FOCUS, - FAN_DIFFUSE, SUPPORT_TARGET_TEMPERATURE) + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + SUPPORT_FAN_MODE, + FAN_FOCUS, + FAN_DIFFUSE, + SUPPORT_TARGET_TEMPERATURE, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from . import DYSON_DEVICES @@ -28,9 +35,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Get Dyson Devices from parent component. add_devices( - [DysonPureHotCoolLinkDevice(device) - for device in hass.data[DYSON_DEVICES] - if isinstance(device, DysonPureHotCoolLink)] + [ + DysonPureHotCoolLinkDevice(device) + for device in hass.data[DYSON_DEVICES] + if isinstance(device, DysonPureHotCoolLink) + ] ) @@ -44,16 +53,14 @@ class DysonPureHotCoolLinkDevice(ClimateDevice): async def async_added_to_hass(self): """Call when entity is added to hass.""" - self.hass.async_add_job( - self._device.add_message_listener, self.on_message) + self.hass.async_add_job(self._device.add_message_listener, self.on_message) def on_message(self, message): """Call when new messages received from the climate.""" if not isinstance(message, DysonPureHotCoolState): return - _LOGGER.debug( - "Message received for climate device %s : %s", self.name, message) + _LOGGER.debug("Message received for climate device %s : %s", self.name, message) self.schedule_update_ha_state() @property @@ -82,8 +89,7 @@ class DysonPureHotCoolLinkDevice(ClimateDevice): if self._device.environmental_state: temperature_kelvin = self._device.environmental_state.temperature if temperature_kelvin != 0: - self._current_temp = float("{0:.1f}".format( - temperature_kelvin - 273)) + self._current_temp = float("{0:.1f}".format(temperature_kelvin - 273)) return self._current_temp @property @@ -154,8 +160,8 @@ class DysonPureHotCoolLinkDevice(ClimateDevice): target_temp = min(self.max_temp, target_temp) target_temp = max(self.min_temp, target_temp) self._device.set_configuration( - heat_target=HeatTarget.celsius(target_temp), - heat_mode=HeatMode.HEAT_ON) + heat_target=HeatTarget.celsius(target_temp), heat_mode=HeatMode.HEAT_ON + ) def set_fan_mode(self, fan_mode): """Set new fan mode.""" diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 65ff093d6d5..0165044b839 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -9,64 +9,81 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( - SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity, - SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, + FanEntity, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, +) from homeassistant.const import ATTR_ENTITY_ID from . import DYSON_DEVICES _LOGGER = logging.getLogger(__name__) -ATTR_NIGHT_MODE = 'night_mode' -ATTR_AUTO_MODE = 'auto_mode' -ATTR_ANGLE_LOW = 'angle_low' -ATTR_ANGLE_HIGH = 'angle_high' -ATTR_FLOW_DIRECTION_FRONT = 'flow_direction_front' -ATTR_TIMER = 'timer' -ATTR_HEPA_FILTER = 'hepa_filter' -ATTR_CARBON_FILTER = 'carbon_filter' -ATTR_DYSON_SPEED = 'dyson_speed' -ATTR_DYSON_SPEED_LIST = 'dyson_speed_list' +ATTR_NIGHT_MODE = "night_mode" +ATTR_AUTO_MODE = "auto_mode" +ATTR_ANGLE_LOW = "angle_low" +ATTR_ANGLE_HIGH = "angle_high" +ATTR_FLOW_DIRECTION_FRONT = "flow_direction_front" +ATTR_TIMER = "timer" +ATTR_HEPA_FILTER = "hepa_filter" +ATTR_CARBON_FILTER = "carbon_filter" +ATTR_DYSON_SPEED = "dyson_speed" +ATTR_DYSON_SPEED_LIST = "dyson_speed_list" -DYSON_DOMAIN = 'dyson' -DYSON_FAN_DEVICES = 'dyson_fan_devices' +DYSON_DOMAIN = "dyson" +DYSON_FAN_DEVICES = "dyson_fan_devices" -SERVICE_SET_NIGHT_MODE = 'set_night_mode' -SERVICE_SET_AUTO_MODE = 'set_auto_mode' -SERVICE_SET_ANGLE = 'set_angle' -SERVICE_SET_FLOW_DIRECTION_FRONT = 'set_flow_direction_front' -SERVICE_SET_TIMER = 'set_timer' -SERVICE_SET_DYSON_SPEED = 'set_speed' +SERVICE_SET_NIGHT_MODE = "set_night_mode" +SERVICE_SET_AUTO_MODE = "set_auto_mode" +SERVICE_SET_ANGLE = "set_angle" +SERVICE_SET_FLOW_DIRECTION_FRONT = "set_flow_direction_front" +SERVICE_SET_TIMER = "set_timer" +SERVICE_SET_DYSON_SPEED = "set_speed" -DYSON_SET_NIGHT_MODE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_NIGHT_MODE): cv.boolean, -}) +DYSON_SET_NIGHT_MODE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_NIGHT_MODE): cv.boolean, + } +) -SET_AUTO_MODE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_AUTO_MODE): cv.boolean, -}) +SET_AUTO_MODE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_AUTO_MODE): cv.boolean, + } +) -SET_ANGLE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_ANGLE_LOW): cv.positive_int, - vol.Required(ATTR_ANGLE_HIGH): cv.positive_int -}) +SET_ANGLE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_ANGLE_LOW): cv.positive_int, + vol.Required(ATTR_ANGLE_HIGH): cv.positive_int, + } +) -SET_FLOW_DIRECTION_FRONT_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_FLOW_DIRECTION_FRONT): cv.boolean -}) +SET_FLOW_DIRECTION_FRONT_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_FLOW_DIRECTION_FRONT): cv.boolean, + } +) -SET_TIMER_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_TIMER): cv.positive_int -}) +SET_TIMER_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_TIMER): cv.positive_int, + } +) -SET_DYSON_SPEED_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_DYSON_SPEED): cv.positive_int -}) +SET_DYSON_SPEED_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_DYSON_SPEED): cv.positive_int, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -99,11 +116,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def service_handle(service): """Handle the Dyson services.""" entity_id = service.data[ATTR_ENTITY_ID] - fan_device = next((fan for fan in hass.data[DYSON_FAN_DEVICES] if - fan.entity_id == entity_id), None) + fan_device = next( + (fan for fan in hass.data[DYSON_FAN_DEVICES] if fan.entity_id == entity_id), + None, + ) if fan_device is None: - _LOGGER.warning("Unable to find Dyson fan device %s", - str(entity_id)) + _LOGGER.warning("Unable to find Dyson fan device %s", str(entity_id)) return if service.service == SERVICE_SET_NIGHT_MODE: @@ -113,12 +131,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): fan_device.set_auto_mode(service.data[ATTR_AUTO_MODE]) if service.service == SERVICE_SET_ANGLE: - fan_device.set_angle(service.data[ATTR_ANGLE_LOW], - service.data[ATTR_ANGLE_HIGH]) + fan_device.set_angle( + service.data[ATTR_ANGLE_LOW], service.data[ATTR_ANGLE_HIGH] + ) if service.service == SERVICE_SET_FLOW_DIRECTION_FRONT: - fan_device.set_flow_direction_front( - service.data[ATTR_FLOW_DIRECTION_FRONT]) + fan_device.set_flow_direction_front(service.data[ATTR_FLOW_DIRECTION_FRONT]) if service.service == SERVICE_SET_TIMER: fan_device.set_timer(service.data[ATTR_TIMER]) @@ -128,28 +146,40 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Register dyson service(s) hass.services.register( - DYSON_DOMAIN, SERVICE_SET_NIGHT_MODE, service_handle, - schema=DYSON_SET_NIGHT_MODE_SCHEMA) + DYSON_DOMAIN, + SERVICE_SET_NIGHT_MODE, + service_handle, + schema=DYSON_SET_NIGHT_MODE_SCHEMA, + ) if has_purecool_devices: hass.services.register( - DYSON_DOMAIN, SERVICE_SET_AUTO_MODE, service_handle, - schema=SET_AUTO_MODE_SCHEMA) + DYSON_DOMAIN, + SERVICE_SET_AUTO_MODE, + service_handle, + schema=SET_AUTO_MODE_SCHEMA, + ) hass.services.register( - DYSON_DOMAIN, SERVICE_SET_ANGLE, service_handle, - schema=SET_ANGLE_SCHEMA) + DYSON_DOMAIN, SERVICE_SET_ANGLE, service_handle, schema=SET_ANGLE_SCHEMA + ) hass.services.register( - DYSON_DOMAIN, SERVICE_SET_FLOW_DIRECTION_FRONT, service_handle, - schema=SET_FLOW_DIRECTION_FRONT_SCHEMA) + DYSON_DOMAIN, + SERVICE_SET_FLOW_DIRECTION_FRONT, + service_handle, + schema=SET_FLOW_DIRECTION_FRONT_SCHEMA, + ) hass.services.register( - DYSON_DOMAIN, SERVICE_SET_TIMER, service_handle, - schema=SET_TIMER_SCHEMA) + DYSON_DOMAIN, SERVICE_SET_TIMER, service_handle, schema=SET_TIMER_SCHEMA + ) hass.services.register( - DYSON_DOMAIN, SERVICE_SET_DYSON_SPEED, service_handle, - schema=SET_DYSON_SPEED_SCHEMA) + DYSON_DOMAIN, + SERVICE_SET_DYSON_SPEED, + service_handle, + schema=SET_DYSON_SPEED_SCHEMA, + ) class DysonPureCoolLinkDevice(FanEntity): @@ -163,16 +193,14 @@ class DysonPureCoolLinkDevice(FanEntity): async def async_added_to_hass(self): """Call when entity is added to hass.""" - self.hass.async_add_job( - self._device.add_message_listener, self.on_message) + self.hass.async_add_job(self._device.add_message_listener, self.on_message) def on_message(self, message): """Call when new messages received from the fan.""" from libpurecool.dyson_pure_state import DysonPureCoolState if isinstance(message, DysonPureCoolState): - _LOGGER.debug("Message received for fan device %s: %s", self.name, - message) + _LOGGER.debug("Message received for fan device %s: %s", self.name, message) self.schedule_update_ha_state() @property @@ -194,9 +222,8 @@ class DysonPureCoolLinkDevice(FanEntity): if speed == FanSpeed.FAN_SPEED_AUTO.value: self._device.set_configuration(fan_mode=FanMode.AUTO) else: - fan_speed = FanSpeed('{0:04d}'.format(int(speed))) - self._device.set_configuration( - fan_mode=FanMode.FAN, fan_speed=fan_speed) + fan_speed = FanSpeed("{0:04d}".format(int(speed))) + self._device.set_configuration(fan_mode=FanMode.FAN, fan_speed=fan_speed) def turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the fan.""" @@ -207,9 +234,10 @@ class DysonPureCoolLinkDevice(FanEntity): if speed == FanSpeed.FAN_SPEED_AUTO.value: self._device.set_configuration(fan_mode=FanMode.AUTO) else: - fan_speed = FanSpeed('{0:04d}'.format(int(speed))) + fan_speed = FanSpeed("{0:04d}".format(int(speed))) self._device.set_configuration( - fan_mode=FanMode.FAN, fan_speed=fan_speed) + fan_mode=FanMode.FAN, fan_speed=fan_speed + ) else: # Speed not set, just turn on self._device.set_configuration(fan_mode=FanMode.FAN) @@ -225,15 +253,12 @@ class DysonPureCoolLinkDevice(FanEntity): """Turn on/off oscillating.""" from libpurecool.const import Oscillation - _LOGGER.debug("Turn oscillation %s for device %s", oscillating, - self.name) + _LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name) if oscillating: - self._device.set_configuration( - oscillation=Oscillation.OSCILLATION_ON) + self._device.set_configuration(oscillation=Oscillation.OSCILLATION_ON) else: - self._device.set_configuration( - oscillation=Oscillation.OSCILLATION_OFF) + self._device.set_configuration(oscillation=Oscillation.OSCILLATION_OFF) @property def oscillating(self): @@ -322,10 +347,7 @@ class DysonPureCoolLinkDevice(FanEntity): @property def device_state_attributes(self) -> dict: """Return optional state attributes.""" - return { - ATTR_NIGHT_MODE: self.night_mode, - ATTR_AUTO_MODE: self.auto_mode - } + return {ATTR_NIGHT_MODE: self.night_mode, ATTR_AUTO_MODE: self.auto_mode} class DysonPureCoolDevice(FanEntity): @@ -338,15 +360,15 @@ class DysonPureCoolDevice(FanEntity): async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.async_add_executor_job( - self._device.add_message_listener, self.on_message) + self._device.add_message_listener, self.on_message + ) def on_message(self, message): """Call when new messages received from the fan.""" from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State if isinstance(message, DysonPureCoolV2State): - _LOGGER.debug("Message received for fan device %s: %s", self.name, - message) + _LOGGER.debug("Message received for fan device %s: %s", self.name, message) self.schedule_update_ha_state() @property @@ -371,6 +393,7 @@ class DysonPureCoolDevice(FanEntity): def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" from libpurecool.const import FanSpeed + if speed == SPEED_LOW: self._device.set_fan_speed(FanSpeed.FAN_SPEED_4) elif speed == SPEED_MEDIUM: @@ -389,13 +412,12 @@ class DysonPureCoolDevice(FanEntity): _LOGGER.debug("Set exact speed for fan %s", self.name) - fan_speed = FanSpeed('{0:04d}'.format(int(speed))) + fan_speed = FanSpeed("{0:04d}".format(int(speed))) self._device.set_fan_speed(fan_speed) def oscillate(self, oscillating: bool) -> None: """Turn on/off oscillating.""" - _LOGGER.debug("Turn oscillation %s for device %s", oscillating, - self.name) + _LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name) if oscillating: self._device.enable_oscillation() @@ -404,8 +426,7 @@ class DysonPureCoolDevice(FanEntity): def set_night_mode(self, night_mode: bool) -> None: """Turn on/off night mode.""" - _LOGGER.debug("Turn night mode %s for device %s", night_mode, - self.name) + _LOGGER.debug("Turn night mode %s for device %s", night_mode, self.name) if night_mode: self._device.enable_night_mode() @@ -414,8 +435,7 @@ class DysonPureCoolDevice(FanEntity): def set_auto_mode(self, auto_mode: bool) -> None: """Turn auto mode on/off.""" - _LOGGER.debug("Turn auto mode %s for device %s", auto_mode, - self.name) + _LOGGER.debug("Turn auto mode %s for device %s", auto_mode, self.name) if auto_mode: self._device.enable_auto_mode() else: @@ -423,16 +443,21 @@ class DysonPureCoolDevice(FanEntity): def set_angle(self, angle_low: int, angle_high: int) -> None: """Set device angle.""" - _LOGGER.debug("set low %s and high angle %s for device %s", - angle_low, angle_high, self.name) + _LOGGER.debug( + "set low %s and high angle %s for device %s", + angle_low, + angle_high, + self.name, + ) self._device.enable_oscillation(angle_low, angle_high) - def set_flow_direction_front(self, - flow_direction_front: bool) -> None: + def set_flow_direction_front(self, flow_direction_front: bool) -> None: """Set frontal airflow direction.""" - _LOGGER.debug("Set frontal flow direction to %s for device %s", - flow_direction_front, - self.name) + _LOGGER.debug( + "Set frontal flow direction to %s for device %s", + flow_direction_front, + self.name, + ) if flow_direction_front: self._device.enable_frontal_direction() @@ -441,8 +466,7 @@ class DysonPureCoolDevice(FanEntity): def set_timer(self, timer) -> None: """Set timer.""" - _LOGGER.debug("Set timer to %s for device %s", timer, - self.name) + _LOGGER.debug("Set timer to %s for device %s", timer, self.name) if timer == 0: self._device.disable_sleep_timer() @@ -465,17 +489,19 @@ class DysonPureCoolDevice(FanEntity): """Return the current speed.""" from libpurecool.const import FanSpeed - speed_map = {FanSpeed.FAN_SPEED_1.value: SPEED_LOW, - FanSpeed.FAN_SPEED_2.value: SPEED_LOW, - FanSpeed.FAN_SPEED_3.value: SPEED_LOW, - FanSpeed.FAN_SPEED_4.value: SPEED_LOW, - FanSpeed.FAN_SPEED_AUTO.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_5.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_8.value: SPEED_HIGH, - FanSpeed.FAN_SPEED_9.value: SPEED_HIGH, - FanSpeed.FAN_SPEED_10.value: SPEED_HIGH} + speed_map = { + FanSpeed.FAN_SPEED_1.value: SPEED_LOW, + FanSpeed.FAN_SPEED_2.value: SPEED_LOW, + FanSpeed.FAN_SPEED_3.value: SPEED_LOW, + FanSpeed.FAN_SPEED_4.value: SPEED_LOW, + FanSpeed.FAN_SPEED_AUTO.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_5.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_8.value: SPEED_HIGH, + FanSpeed.FAN_SPEED_9.value: SPEED_HIGH, + FanSpeed.FAN_SPEED_10.value: SPEED_HIGH, + } return speed_map[self._device.state.speed] @@ -512,7 +538,7 @@ class DysonPureCoolDevice(FanEntity): @property def flow_direction_front(self): """Return frontal flow direction.""" - return self._device.state.front_direction == 'ON' + return self._device.state.front_direction == "ON" @property def timer(self): @@ -538,6 +564,7 @@ class DysonPureCoolDevice(FanEntity): def dyson_speed_list(self) -> list: """Get the list of available dyson speeds.""" from libpurecool.const import FanSpeed + return [ int(FanSpeed.FAN_SPEED_1.value), int(FanSpeed.FAN_SPEED_2.value), @@ -559,8 +586,7 @@ class DysonPureCoolDevice(FanEntity): @property def supported_features(self) -> int: """Flag supported features.""" - return SUPPORT_OSCILLATE | \ - SUPPORT_SET_SPEED + return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED @property def device_state_attributes(self) -> dict: @@ -575,5 +601,5 @@ class DysonPureCoolDevice(FanEntity): ATTR_HEPA_FILTER: self.hepa_filter, ATTR_CARBON_FILTER: self.carbon_filter, ATTR_DYSON_SPEED: self.dyson_speed, - ATTR_DYSON_SPEED_LIST: self.dyson_speed_list + ATTR_DYSON_SPEED_LIST: self.dyson_speed_list, } diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index 9cd1c915c57..f89823b143f 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -6,21 +6,21 @@ from homeassistant.helpers.entity import Entity from . import DYSON_DEVICES SENSOR_UNITS = { - 'air_quality': None, - 'dust': None, - 'filter_life': 'hours', - 'humidity': '%', + "air_quality": None, + "dust": None, + "filter_life": "hours", + "humidity": "%", } SENSOR_ICONS = { - 'air_quality': 'mdi:fan', - 'dust': 'mdi:cloud', - 'filter_life': 'mdi:filter-outline', - 'humidity': 'mdi:water-percent', - 'temperature': 'mdi:thermometer', + "air_quality": "mdi:fan", + "dust": "mdi:cloud", + "filter_life": "mdi:filter-outline", + "humidity": "mdi:water-percent", + "temperature": "mdi:thermometer", } -DYSON_SENSOR_DEVICES = 'dyson_sensor_devices' +DYSON_SENSOR_DEVICES = "dyson_sensor_devices" _LOGGER = logging.getLogger(__name__) @@ -38,13 +38,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = hass.data[DYSON_SENSOR_DEVICES] # Get Dyson Devices from parent component - device_ids = [device.unique_id for device in - hass.data[DYSON_SENSOR_DEVICES]] + device_ids = [device.unique_id for device in hass.data[DYSON_SENSOR_DEVICES]] for device in hass.data[DYSON_DEVICES]: if isinstance(device, DysonPureCool): - if '{}-{}'.format(device.serial, 'temperature') not in device_ids: + if "{}-{}".format(device.serial, "temperature") not in device_ids: devices.append(DysonTemperatureSensor(device, unit)) - if '{}-{}'.format(device.serial, 'humidity') not in device_ids: + if "{}-{}".format(device.serial, "humidity") not in device_ids: devices.append(DysonHumiditySensor(device)) elif isinstance(device, DysonPureCoolLink): devices.append(DysonFilterLifeSensor(device)) @@ -68,14 +67,14 @@ class DysonSensor(Entity): async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.async_add_executor_job( - self._device.add_message_listener, self.on_message) + self._device.add_message_listener, self.on_message + ) def on_message(self, message): """Handle new messages which are received from the fan.""" # Prevent refreshing if not needed if self._old_value is None or self._old_value != self.state: - _LOGGER.debug("Message received for %s device: %s", self.name, - message) + _LOGGER.debug("Message received for %s device: %s", self.name, message) self._old_value = self.state self.schedule_update_ha_state() @@ -102,7 +101,7 @@ class DysonSensor(Entity): @property def unique_id(self): """Return the sensor's unique id.""" - return '{}-{}'.format(self._device.serial, self._sensor_type) + return "{}-{}".format(self._device.serial, self._sensor_type) class DysonFilterLifeSensor(DysonSensor): @@ -110,7 +109,7 @@ class DysonFilterLifeSensor(DysonSensor): def __init__(self, device): """Create a new Dyson Filter Life sensor.""" - super().__init__(device, 'filter_life') + super().__init__(device, "filter_life") self._name = "{} Filter Life".format(self._device.name) @property @@ -126,7 +125,7 @@ class DysonDustSensor(DysonSensor): def __init__(self, device): """Create a new Dyson Dust sensor.""" - super().__init__(device, 'dust') + super().__init__(device, "dust") self._name = "{} Dust".format(self._device.name) @property @@ -142,7 +141,7 @@ class DysonHumiditySensor(DysonSensor): def __init__(self, device): """Create a new Dyson Humidity sensor.""" - super().__init__(device, 'humidity') + super().__init__(device, "humidity") self._name = "{} Humidity".format(self._device.name) @property @@ -160,7 +159,7 @@ class DysonTemperatureSensor(DysonSensor): def __init__(self, device, unit): """Create a new Dyson Temperature sensor.""" - super().__init__(device, 'temperature') + super().__init__(device, "temperature") self._name = "{} Temperature".format(self._device.name) self._unit = unit @@ -187,7 +186,7 @@ class DysonAirQualitySensor(DysonSensor): def __init__(self, device): """Create a new Dyson Air Quality sensor.""" - super().__init__(device, 'air_quality') + super().__init__(device, "air_quality") self._name = "{} AQI".format(self._device.name) @property diff --git a/homeassistant/components/dyson/vacuum.py b/homeassistant/components/dyson/vacuum.py index 0bb2368f690..cef5f0c9961 100644 --- a/homeassistant/components/dyson/vacuum.py +++ b/homeassistant/components/dyson/vacuum.py @@ -2,24 +2,38 @@ import logging from homeassistant.components.vacuum import ( - SUPPORT_BATTERY, SUPPORT_FAN_SPEED, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, - SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - VacuumDevice) + SUPPORT_BATTERY, + SUPPORT_FAN_SPEED, + SUPPORT_PAUSE, + SUPPORT_RETURN_HOME, + SUPPORT_STATUS, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + VacuumDevice, +) from homeassistant.helpers.icon import icon_for_battery_level from . import DYSON_DEVICES _LOGGER = logging.getLogger(__name__) -ATTR_CLEAN_ID = 'clean_id' -ATTR_FULL_CLEAN_TYPE = 'full_clean_type' -ATTR_POSITION = 'position' +ATTR_CLEAN_ID = "clean_id" +ATTR_FULL_CLEAN_TYPE = "full_clean_type" +ATTR_POSITION = "position" DYSON_360_EYE_DEVICES = "dyson_360_eye_devices" -SUPPORT_DYSON = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \ - SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | SUPPORT_STATUS | \ - SUPPORT_BATTERY | SUPPORT_STOP +SUPPORT_DYSON = ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PAUSE + | SUPPORT_RETURN_HOME + | SUPPORT_FAN_SPEED + | SUPPORT_STATUS + | SUPPORT_BATTERY + | SUPPORT_STOP +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -31,8 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.data[DYSON_360_EYE_DEVICES] = [] # Get Dyson Devices from parent component - for device in [d for d in hass.data[DYSON_DEVICES] if - isinstance(d, Dyson360Eye)]: + for device in [d for d in hass.data[DYSON_DEVICES] if isinstance(d, Dyson360Eye)]: dyson_entity = Dyson360EyeDevice(device) hass.data[DYSON_360_EYE_DEVICES].append(dyson_entity) @@ -50,8 +63,7 @@ class Dyson360EyeDevice(VacuumDevice): async def async_added_to_hass(self): """Call when entity is added to hass.""" - self.hass.async_add_job( - self._device.add_message_listener, self.on_message) + self.hass.async_add_job(self._device.add_message_listener, self.on_message) def on_message(self, message): """Handle a new messages that was received from the vacuum.""" @@ -75,6 +87,7 @@ class Dyson360EyeDevice(VacuumDevice): def status(self): """Return the status of the vacuum cleaner.""" from libpurecool.const import Dyson360EyeMode + dyson_labels = { Dyson360EyeMode.INACTIVE_CHARGING: "Stopped - Charging", Dyson360EyeMode.INACTIVE_CHARGED: "Stopped - Charged", @@ -83,13 +96,11 @@ class Dyson360EyeDevice(VacuumDevice): Dyson360EyeMode.FULL_CLEAN_ABORTED: "Returning home", Dyson360EyeMode.FULL_CLEAN_INITIATED: "Start cleaning", Dyson360EyeMode.FAULT_USER_RECOVERABLE: "Error - device blocked", - Dyson360EyeMode.FAULT_REPLACE_ON_DOCK: - "Error - Replace device on dock", + Dyson360EyeMode.FAULT_REPLACE_ON_DOCK: "Error - Replace device on dock", Dyson360EyeMode.FULL_CLEAN_FINISHED: "Finished", - Dyson360EyeMode.FULL_CLEAN_NEEDS_CHARGE: "Need charging" + Dyson360EyeMode.FULL_CLEAN_NEEDS_CHARGE: "Need charging", } - return dyson_labels.get( - self._device.state.state, self._device.state.state) + return dyson_labels.get(self._device.state.state, self._device.state.state) @property def battery_level(self): @@ -100,10 +111,8 @@ class Dyson360EyeDevice(VacuumDevice): def fan_speed(self): """Return the fan speed of the vacuum cleaner.""" from libpurecool.const import PowerMode - speed_labels = { - PowerMode.MAX: "Max", - PowerMode.QUIET: "Quiet" - } + + speed_labels = {PowerMode.MAX: "Max", PowerMode.QUIET: "Quiet"} return speed_labels[self._device.state.power_mode] @property @@ -114,9 +123,7 @@ class Dyson360EyeDevice(VacuumDevice): @property def device_state_attributes(self): """Return the specific state attributes of this vacuum cleaner.""" - return { - ATTR_POSITION: str(self._device.state.position) - } + return {ATTR_POSITION: str(self._device.state.position)} @property def is_on(self) -> bool: @@ -126,7 +133,7 @@ class Dyson360EyeDevice(VacuumDevice): return self._device.state.state in [ Dyson360EyeMode.FULL_CLEAN_INITIATED, Dyson360EyeMode.FULL_CLEAN_ABORTED, - Dyson360EyeMode.FULL_CLEAN_RUNNING + Dyson360EyeMode.FULL_CLEAN_RUNNING, ] @property @@ -144,10 +151,10 @@ class Dyson360EyeDevice(VacuumDevice): """Return the battery icon for the vacuum cleaner.""" from libpurecool.const import Dyson360EyeMode - charging = self._device.state.state in [ - Dyson360EyeMode.INACTIVE_CHARGING] + charging = self._device.state.state in [Dyson360EyeMode.INACTIVE_CHARGING] return icon_for_battery_level( - battery_level=self.battery_level, charging=charging) + battery_level=self.battery_level, charging=charging + ) def turn_on(self, **kwargs): """Turn the vacuum on.""" @@ -174,10 +181,7 @@ class Dyson360EyeDevice(VacuumDevice): from libpurecool.const import PowerMode _LOGGER.debug("Set fan speed %s on device %s", fan_speed, self.name) - power_modes = { - "Quiet": PowerMode.QUIET, - "Max": PowerMode.MAX - } + power_modes = {"Quiet": PowerMode.QUIET, "Max": PowerMode.MAX} self._device.set_power_mode(power_modes[fan_speed]) def start_pause(self, **kwargs): @@ -187,8 +191,10 @@ class Dyson360EyeDevice(VacuumDevice): if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: _LOGGER.debug("Resume device %s", self.name) self._device.resume() - elif self._device.state.state in [Dyson360EyeMode.INACTIVE_CHARGED, - Dyson360EyeMode.INACTIVE_CHARGING]: + elif self._device.state.state in [ + Dyson360EyeMode.INACTIVE_CHARGED, + Dyson360EyeMode.INACTIVE_CHARGING, + ]: _LOGGER.debug("Start device %s", self.name) self._device.start() else: diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index aaf3384d55f..1482ab34c68 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -14,8 +14,11 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, - CONF_NAME, CONF_MONITORED_VARIABLES) + CONF_USERNAME, + CONF_PASSWORD, + CONF_NAME, + CONF_MONITORED_VARIABLES, +) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.exceptions import PlatformNotReady @@ -23,47 +26,46 @@ from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) -GIGABITS = 'Gb' # type: str -PRICE = 'CAD' # type: str -DAYS = 'days' # type: str -PERCENT = '%' # type: str +GIGABITS = "Gb" # type: str +PRICE = "CAD" # type: str +DAYS = "days" # type: str +PERCENT = "%" # type: str -DEFAULT_NAME = 'EBox' +DEFAULT_NAME = "EBox" REQUESTS_TIMEOUT = 15 SCAN_INTERVAL = timedelta(minutes=15) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { - 'usage': ['Usage', PERCENT, 'mdi:percent'], - 'balance': ['Balance', PRICE, 'mdi:square-inc-cash'], - 'limit': ['Data limit', GIGABITS, 'mdi:download'], - 'days_left': ['Days left', DAYS, 'mdi:calendar-today'], - 'before_offpeak_download': - ['Download before offpeak', GIGABITS, 'mdi:download'], - 'before_offpeak_upload': - ['Upload before offpeak', GIGABITS, 'mdi:upload'], - 'before_offpeak_total': - ['Total before offpeak', GIGABITS, 'mdi:download'], - 'offpeak_download': ['Offpeak download', GIGABITS, 'mdi:download'], - 'offpeak_upload': ['Offpeak Upload', GIGABITS, 'mdi:upload'], - 'offpeak_total': ['Offpeak Total', GIGABITS, 'mdi:download'], - 'download': ['Download', GIGABITS, 'mdi:download'], - 'upload': ['Upload', GIGABITS, 'mdi:upload'], - 'total': ['Total', GIGABITS, 'mdi:download'], + "usage": ["Usage", PERCENT, "mdi:percent"], + "balance": ["Balance", PRICE, "mdi:square-inc-cash"], + "limit": ["Data limit", GIGABITS, "mdi:download"], + "days_left": ["Days left", DAYS, "mdi:calendar-today"], + "before_offpeak_download": ["Download before offpeak", GIGABITS, "mdi:download"], + "before_offpeak_upload": ["Upload before offpeak", GIGABITS, "mdi:upload"], + "before_offpeak_total": ["Total before offpeak", GIGABITS, "mdi:download"], + "offpeak_download": ["Offpeak download", GIGABITS, "mdi:download"], + "offpeak_upload": ["Offpeak Upload", GIGABITS, "mdi:upload"], + "offpeak_total": ["Offpeak Total", GIGABITS, "mdi:download"], + "download": ["Download", GIGABITS, "mdi:download"], + "upload": ["Upload", GIGABITS, "mdi:upload"], + "total": ["Total", GIGABITS, "mdi:download"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_VARIABLES): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MONITORED_VARIABLES): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the EBox sensor.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -74,6 +76,7 @@ async def async_setup_platform(hass, config, async_add_entities, name = config.get(CONF_NAME) from pyebox.client import PyEboxError + try: await ebox_data.async_update() except PyEboxError as exp: @@ -103,7 +106,7 @@ class EBoxSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self.client_name, self._name) + return "{} {}".format(self.client_name, self._name) @property def state(self): @@ -133,14 +136,15 @@ class EBoxData: def __init__(self, username, password, httpsession): """Initialize the data object.""" from pyebox import EboxClient - self.client = EboxClient(username, password, - REQUESTS_TIMEOUT, httpsession) + + self.client = EboxClient(username, password, REQUESTS_TIMEOUT, httpsession) self.data = {} @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from Ebox.""" from pyebox.client import PyEboxError + try: await self.client.fetch_data() except PyEboxError as exp: diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index c3e72bfd764..e11de446e40 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -6,20 +6,24 @@ import socket import voluptuous as vol from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_PORT, CONF_MONITORED_CONDITIONS) + CONF_NAME, + CONF_HOST, + CONF_PORT, + CONF_MONITORED_CONDITIONS, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.util import Throttle -from .const import (DOMAIN, SENSOR_TYPES) +from .const import DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'ebusd' +DEFAULT_NAME = "ebusd" DEFAULT_PORT = 8888 -CONF_CIRCUIT = 'circuit' +CONF_CIRCUIT = "circuit" CACHE_TTL = 900 -SERVICE_EBUSD_WRITE = 'ebusd_write' +SERVICE_EBUSD_WRITE = "ebusd_write" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) @@ -29,24 +33,27 @@ def verify_ebusd_config(config): circuit = config[CONF_CIRCUIT] for condition in config[CONF_MONITORED_CONDITIONS]: if condition not in SENSOR_TYPES[circuit]: - raise vol.Invalid( - "Condition '" + condition + "' not in '" + circuit + "'.") + raise vol.Invalid("Condition '" + condition + "' not in '" + circuit + "'.") return config -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema( - vol.All({ - vol.Required(CONF_CIRCUIT): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): - cv.ensure_list, - }, - verify_ebusd_config) - ) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + vol.All( + { + vol.Required(CONF_CIRCUIT): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): cv.ensure_list, + }, + verify_ebusd_config, + ) + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -55,24 +62,23 @@ def setup(hass, config): name = conf[CONF_NAME] circuit = conf[CONF_CIRCUIT] monitored_conditions = conf.get(CONF_MONITORED_CONDITIONS) - server_address = ( - conf.get(CONF_HOST), conf.get(CONF_PORT)) + server_address = (conf.get(CONF_HOST), conf.get(CONF_PORT)) try: _LOGGER.debug("Ebusd integration setup started") import ebusdpy + ebusdpy.init(server_address) hass.data[DOMAIN] = EbusdData(server_address, circuit) sensor_config = { CONF_MONITORED_CONDITIONS: monitored_conditions, - 'client_name': name, - 'sensor_types': SENSOR_TYPES[circuit] + "client_name": name, + "sensor_types": SENSOR_TYPES[circuit], } - load_platform(hass, 'sensor', DOMAIN, sensor_config, config) + load_platform(hass, "sensor", DOMAIN, sensor_config, config) - hass.services.register( - DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write) + hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write) _LOGGER.debug("Ebusd integration setup completed") return True @@ -97,9 +103,10 @@ class EbusdData: try: _LOGGER.debug("Opening socket to ebusd %s", name) command_result = ebusdpy.read( - self._address, self._circuit, name, stype, CACHE_TTL) + self._address, self._circuit, name, stype, CACHE_TTL + ) if command_result is not None: - if 'ERR:' in command_result: + if "ERR:" in command_result: _LOGGER.warning(command_result) else: self.value[name] = command_result @@ -110,15 +117,15 @@ class EbusdData: def write(self, call): """Call write methon on ebusd.""" import ebusdpy - name = call.data.get('name') - value = call.data.get('value') + + name = call.data.get("name") + value = call.data.get("value") try: _LOGGER.debug("Opening socket to ebusd %s", name) - command_result = ebusdpy.write( - self._address, self._circuit, name, value) + command_result = ebusdpy.write(self._address, self._circuit, name, value) if command_result is not None: - if 'done' not in command_result: - _LOGGER.warning('Write command failed: %s', name) + if "done" not in command_result: + _LOGGER.warning("Write command failed: %s", name) except RuntimeError as err: _LOGGER.error(err) diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index 3821bd8ce15..7587d0cd42d 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,102 +1,104 @@ """Constants for ebus component.""" from homeassistant.const import ENERGY_KILO_WATT_HOUR -DOMAIN = 'ebusd' +DOMAIN = "ebusd" # SensorTypes: # 0='decimal', 1='time-schedule', 2='switch', 3='string', 4='value;status' SENSOR_TYPES = { - '700': { - 'ActualFlowTemperatureDesired': - ['Hc1ActualFlowTempDesired', '°C', 'mdi:thermometer', 0], - 'MaxFlowTemperatureDesired': - ['Hc1MaxFlowTempDesired', '°C', 'mdi:thermometer', 0], - 'MinFlowTemperatureDesired': - ['Hc1MinFlowTempDesired', '°C', 'mdi:thermometer', 0], - 'PumpStatus': - ['Hc1PumpStatus', None, 'mdi:toggle-switch', 2], - 'HCSummerTemperatureLimit': - ['Hc1SummerTempLimit', '°C', 'mdi:weather-sunny', 0], - 'HolidayTemperature': - ['HolidayTemp', '°C', 'mdi:thermometer', 0], - 'HWTemperatureDesired': - ['HwcTempDesired', '°C', 'mdi:thermometer', 0], - 'HWTimerMonday': - ['hwcTimer.Monday', None, 'mdi:timer', 1], - 'HWTimerTuesday': - ['hwcTimer.Tuesday', None, 'mdi:timer', 1], - 'HWTimerWednesday': - ['hwcTimer.Wednesday', None, 'mdi:timer', 1], - 'HWTimerThursday': - ['hwcTimer.Thursday', None, 'mdi:timer', 1], - 'HWTimerFriday': - ['hwcTimer.Friday', None, 'mdi:timer', 1], - 'HWTimerSaturday': - ['hwcTimer.Saturday', None, 'mdi:timer', 1], - 'HWTimerSunday': - ['hwcTimer.Sunday', None, 'mdi:timer', 1], - 'WaterPressure': - ['WaterPressure', 'bar', 'mdi:water-pump', 0], - 'Zone1RoomZoneMapping': - ['z1RoomZoneMapping', None, 'mdi:label', 0], - 'Zone1NightTemperature': - ['z1NightTemp', '°C', 'mdi:weather-night', 0], - 'Zone1DayTemperature': - ['z1DayTemp', '°C', 'mdi:weather-sunny', 0], - 'Zone1HolidayTemperature': - ['z1HolidayTemp', '°C', 'mdi:thermometer', 0], - 'Zone1RoomTemperature': - ['z1RoomTemp', '°C', 'mdi:thermometer', 0], - 'Zone1ActualRoomTemperatureDesired': - ['z1ActualRoomTempDesired', '°C', 'mdi:thermometer', 0], - 'Zone1TimerMonday': - ['z1Timer.Monday', None, 'mdi:timer', 1], - 'Zone1TimerTuesday': - ['z1Timer.Tuesday', None, 'mdi:timer', 1], - 'Zone1TimerWednesday': - ['z1Timer.Wednesday', None, 'mdi:timer', 1], - 'Zone1TimerThursday': - ['z1Timer.Thursday', None, 'mdi:timer', 1], - 'Zone1TimerFriday': - ['z1Timer.Friday', None, 'mdi:timer', 1], - 'Zone1TimerSaturday': - ['z1Timer.Saturday', None, 'mdi:timer', 1], - 'Zone1TimerSunday': - ['z1Timer.Sunday', None, 'mdi:timer', 1], - 'Zone1OperativeMode': - ['z1OpMode', None, 'mdi:math-compass', 3], - 'ContinuosHeating': - ['ContinuosHeating', '°C', 'mdi:weather-snowy', 0], - 'PowerEnergyConsumptionLastMonth': - ['PrEnergySumHcLastMonth', ENERGY_KILO_WATT_HOUR, 'mdi:flash', 0], - 'PowerEnergyConsumptionThisMonth': - ['PrEnergySumHcThisMonth', ENERGY_KILO_WATT_HOUR, 'mdi:flash', 0] + "700": { + "ActualFlowTemperatureDesired": [ + "Hc1ActualFlowTempDesired", + "°C", + "mdi:thermometer", + 0, + ], + "MaxFlowTemperatureDesired": [ + "Hc1MaxFlowTempDesired", + "°C", + "mdi:thermometer", + 0, + ], + "MinFlowTemperatureDesired": [ + "Hc1MinFlowTempDesired", + "°C", + "mdi:thermometer", + 0, + ], + "PumpStatus": ["Hc1PumpStatus", None, "mdi:toggle-switch", 2], + "HCSummerTemperatureLimit": [ + "Hc1SummerTempLimit", + "°C", + "mdi:weather-sunny", + 0, + ], + "HolidayTemperature": ["HolidayTemp", "°C", "mdi:thermometer", 0], + "HWTemperatureDesired": ["HwcTempDesired", "°C", "mdi:thermometer", 0], + "HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer", 1], + "HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer", 1], + "HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer", 1], + "HWTimerThursday": ["hwcTimer.Thursday", None, "mdi:timer", 1], + "HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer", 1], + "HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer", 1], + "HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer", 1], + "WaterPressure": ["WaterPressure", "bar", "mdi:water-pump", 0], + "Zone1RoomZoneMapping": ["z1RoomZoneMapping", None, "mdi:label", 0], + "Zone1NightTemperature": ["z1NightTemp", "°C", "mdi:weather-night", 0], + "Zone1DayTemperature": ["z1DayTemp", "°C", "mdi:weather-sunny", 0], + "Zone1HolidayTemperature": ["z1HolidayTemp", "°C", "mdi:thermometer", 0], + "Zone1RoomTemperature": ["z1RoomTemp", "°C", "mdi:thermometer", 0], + "Zone1ActualRoomTemperatureDesired": [ + "z1ActualRoomTempDesired", + "°C", + "mdi:thermometer", + 0, + ], + "Zone1TimerMonday": ["z1Timer.Monday", None, "mdi:timer", 1], + "Zone1TimerTuesday": ["z1Timer.Tuesday", None, "mdi:timer", 1], + "Zone1TimerWednesday": ["z1Timer.Wednesday", None, "mdi:timer", 1], + "Zone1TimerThursday": ["z1Timer.Thursday", None, "mdi:timer", 1], + "Zone1TimerFriday": ["z1Timer.Friday", None, "mdi:timer", 1], + "Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer", 1], + "Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer", 1], + "Zone1OperativeMode": ["z1OpMode", None, "mdi:math-compass", 3], + "ContinuosHeating": ["ContinuosHeating", "°C", "mdi:weather-snowy", 0], + "PowerEnergyConsumptionLastMonth": [ + "PrEnergySumHcLastMonth", + ENERGY_KILO_WATT_HOUR, + "mdi:flash", + 0, + ], + "PowerEnergyConsumptionThisMonth": [ + "PrEnergySumHcThisMonth", + ENERGY_KILO_WATT_HOUR, + "mdi:flash", + 0, + ], }, - 'ehp': { - 'HWTemperature': - ['HwcTemp', '°C', 'mdi:thermometer', 4], - 'OutsideTemp': - ['OutsideTemp', '°C', 'mdi:thermometer', 4] + "ehp": { + "HWTemperature": ["HwcTemp", "°C", "mdi:thermometer", 4], + "OutsideTemp": ["OutsideTemp", "°C", "mdi:thermometer", 4], + }, + "bai": { + "ReturnTemperature": ["ReturnTemp", "°C", "mdi:thermometer", 4], + "CentralHeatingPump": ["WP", None, "mdi:toggle-switch", 2], + "HeatingSwitch": ["HeatingSwitch", None, "mdi:toggle-switch", 2], + "FlowTemperature": ["FlowTemp", "°C", "mdi:thermometer", 4], + "Flame": ["Flame", None, "mdi:toggle-switch", 2], + "PowerEnergyConsumptionHeatingCircuit": [ + "PrEnergySumHc1", + ENERGY_KILO_WATT_HOUR, + "mdi:flash", + 0, + ], + "PowerEnergyConsumptionHotWaterCircuit": [ + "PrEnergySumHwc1", + ENERGY_KILO_WATT_HOUR, + "mdi:flash", + 0, + ], + "RoomThermostat": ["DCRoomthermostat", None, "mdi:toggle-switch", 2], + "HeatingPartLoad": ["PartloadHcKW", ENERGY_KILO_WATT_HOUR, "mdi:flash", 0], }, - 'bai': { - 'ReturnTemperature': - ['ReturnTemp', '°C', 'mdi:thermometer', 4], - 'CentralHeatingPump': - ['WP', None, 'mdi:toggle-switch', 2], - 'HeatingSwitch': - ['HeatingSwitch', None, 'mdi:toggle-switch', 2], - 'FlowTemperature': - ['FlowTemp', '°C', 'mdi:thermometer', 4], - 'Flame': - ['Flame', None, 'mdi:toggle-switch', 2], - 'PowerEnergyConsumptionHeatingCircuit': - ['PrEnergySumHc1', ENERGY_KILO_WATT_HOUR, 'mdi:flash', 0], - 'PowerEnergyConsumptionHotWaterCircuit': - ['PrEnergySumHwc1', ENERGY_KILO_WATT_HOUR, 'mdi:flash', 0], - 'RoomThermostat': - ['DCRoomthermostat', None, 'mdi:toggle-switch', 2], - 'HeatingPartLoad': - ['PartloadHcKW', ENERGY_KILO_WATT_HOUR, 'mdi:flash', 0] - } } diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index f73bb09b509..37b7d2dd060 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -6,12 +6,12 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN -TIME_FRAME1_BEGIN = 'time_frame1_begin' -TIME_FRAME1_END = 'time_frame1_end' -TIME_FRAME2_BEGIN = 'time_frame2_begin' -TIME_FRAME2_END = 'time_frame2_end' -TIME_FRAME3_BEGIN = 'time_frame3_begin' -TIME_FRAME3_END = 'time_frame3_end' +TIME_FRAME1_BEGIN = "time_frame1_begin" +TIME_FRAME1_END = "time_frame1_end" +TIME_FRAME2_BEGIN = "time_frame2_begin" +TIME_FRAME2_END = "time_frame2_end" +TIME_FRAME3_BEGIN = "time_frame3_begin" +TIME_FRAME3_END = "time_frame3_end" _LOGGER = logging.getLogger(__name__) @@ -19,13 +19,14 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Ebus sensor.""" ebusd_api = hass.data[DOMAIN] - monitored_conditions = discovery_info['monitored_conditions'] - name = discovery_info['client_name'] + monitored_conditions = discovery_info["monitored_conditions"] + name = discovery_info["client_name"] dev = [] for condition in monitored_conditions: - dev.append(EbusdSensor( - ebusd_api, discovery_info['sensor_types'][condition], name)) + dev.append( + EbusdSensor(ebusd_api, discovery_info["sensor_types"][condition], name) + ) add_entities(dev, True) @@ -43,7 +44,7 @@ class EbusdSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self._client_name, self._name) + return "{} {}".format(self._client_name, self._name) @property def state(self): @@ -60,17 +61,17 @@ class EbusdSensor(Entity): TIME_FRAME2_BEGIN: None, TIME_FRAME2_END: None, TIME_FRAME3_BEGIN: None, - TIME_FRAME3_END: None + TIME_FRAME3_END: None, } - time_frame = self._state.split(';') + time_frame = self._state.split(";") for index, item in enumerate(sorted(schedule.items())): if index < len(time_frame): - parsed = datetime.datetime.strptime( - time_frame[index], '%H:%M') + parsed = datetime.datetime.strptime(time_frame[index], "%H:%M") parsed = parsed.replace( datetime.datetime.now().year, datetime.datetime.now().month, - datetime.datetime.now().day) + datetime.datetime.now().day, + ) schedule[item[0]] = parsed.isoformat() return schedule return None diff --git a/homeassistant/components/ecoal_boiler/__init__.py b/homeassistant/components/ecoal_boiler/__init__.py index 796324d9337..40769c9990a 100644 --- a/homeassistant/components/ecoal_boiler/__init__.py +++ b/homeassistant/components/ecoal_boiler/__init__.py @@ -3,16 +3,21 @@ import logging import voluptuous as vol -from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_USERNAME, - CONF_MONITORED_CONDITIONS, CONF_SENSORS, - CONF_SWITCHES) +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + CONF_MONITORED_CONDITIONS, + CONF_SENSORS, + CONF_SWITCHES, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform _LOGGER = logging.getLogger(__name__) DOMAIN = "ecoal_boiler" -DATA_ECOAL_BOILER = 'data_' + DOMAIN +DATA_ECOAL_BOILER = "data_" + DOMAIN DEFAULT_USERNAME = "admin" DEFAULT_PASSWORD = "admin" @@ -29,37 +34,48 @@ AVAILABLE_PUMPS = { # Available temp sensor ids with assigned HA names # Available as sensors AVAILABLE_SENSORS = { - "outdoor_temp": 'Outdoor temperature', - "indoor_temp": 'Indoor temperature', - "indoor2_temp": 'Indoor temperature 2', - "domestic_hot_water_temp": 'Domestic hot water temperature', - "target_domestic_hot_water_temp": 'Target hot water temperature', - "feedwater_in_temp": 'Feedwater input temperature', - "feedwater_out_temp": 'Feedwater output temperature', - "target_feedwater_temp": 'Target feedwater temperature', - "fuel_feeder_temp": 'Fuel feeder temperature', - "exhaust_temp": 'Exhaust temperature', + "outdoor_temp": "Outdoor temperature", + "indoor_temp": "Indoor temperature", + "indoor2_temp": "Indoor temperature 2", + "domestic_hot_water_temp": "Domestic hot water temperature", + "target_domestic_hot_water_temp": "Target hot water temperature", + "feedwater_in_temp": "Feedwater input temperature", + "feedwater_out_temp": "Feedwater output temperature", + "target_feedwater_temp": "Target feedwater temperature", + "fuel_feeder_temp": "Fuel feeder temperature", + "exhaust_temp": "Exhaust temperature", } -SWITCH_SCHEMA = vol.Schema({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_PUMPS)): - vol.All(cv.ensure_list, [vol.In(AVAILABLE_PUMPS)]) -}) +SWITCH_SCHEMA = vol.Schema( + { + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_PUMPS)): vol.All( + cv.ensure_list, [vol.In(AVAILABLE_PUMPS)] + ) + } +) -SENSOR_SCHEMA = vol.Schema({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_SENSORS)): - vol.All(cv.ensure_list, [vol.In(AVAILABLE_SENSORS)]) -}) +SENSOR_SCHEMA = vol.Schema( + { + vol.Optional( + CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_SENSORS) + ): vol.All(cv.ensure_list, [vol.In(AVAILABLE_SENSORS)]) + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, hass_config): @@ -74,16 +90,18 @@ def setup(hass, hass_config): ecoal_contr = ECoalController(host, username, passwd) if ecoal_contr.version is None: # Wrong credentials nor network config - _LOGGER.error("Unable to read controller status from %s@%s" - " (wrong host/credentials)", username, host, ) + _LOGGER.error( + "Unable to read controller status from %s@%s" " (wrong host/credentials)", + username, + host, + ) return False - _LOGGER.debug("Detected controller version: %r @%s", - ecoal_contr.version, host, ) + _LOGGER.debug("Detected controller version: %r @%s", ecoal_contr.version, host) hass.data[DATA_ECOAL_BOILER] = ecoal_contr # Setup switches switches = conf[CONF_SWITCHES][CONF_MONITORED_CONDITIONS] - load_platform(hass, 'switch', DOMAIN, switches, hass_config) + load_platform(hass, "switch", DOMAIN, switches, hass_config) # Setup temp sensors sensors = conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS] - load_platform(hass, 'sensor', DOMAIN, sensors, hass_config) + load_platform(hass, "sensor", DOMAIN, sensors, hass_config) return True diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index 5f9ae6a919d..cb8b7436b51 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -14,30 +14,36 @@ from homeassistant.util.json import save_json _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -CONF_HOLD_TEMP = 'hold_temp' +CONF_HOLD_TEMP = "hold_temp" -DOMAIN = 'ecobee' +DOMAIN = "ecobee" -ECOBEE_CONFIG_FILE = 'ecobee.conf' +ECOBEE_CONFIG_FILE = "ecobee.conf" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) NETWORK = None -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def request_configuration(network, hass, config): """Request configuration steps from the user.""" configurator = hass.components.configurator - if 'ecobee' in _CONFIGURING: + if "ecobee" in _CONFIGURING: configurator.notify_errors( - _CONFIGURING['ecobee'], "Failed to register, please try again.") + _CONFIGURING["ecobee"], "Failed to register, please try again." + ) return @@ -47,13 +53,15 @@ def request_configuration(network, hass, config): network.update() setup_ecobee(hass, network, config) - _CONFIGURING['ecobee'] = configurator.request_config( - "Ecobee", ecobee_configuration_callback, + _CONFIGURING["ecobee"] = configurator.request_config( + "Ecobee", + ecobee_configuration_callback, description=( - 'Please authorize this app at https://www.ecobee.com/consumer' - 'portal/index.html with pin code: ' + network.pin), + "Please authorize this app at https://www.ecobee.com/consumer" + "portal/index.html with pin code: " + network.pin + ), description_image="/static/images/config_ecobee_thermostat.png", - submit_caption="I have authorized the app." + submit_caption="I have authorized the app.", ) @@ -64,17 +72,16 @@ def setup_ecobee(hass, network, config): request_configuration(network, hass, config) return - if 'ecobee' in _CONFIGURING: + if "ecobee" in _CONFIGURING: configurator = hass.components.configurator - configurator.request_done(_CONFIGURING.pop('ecobee')) + configurator.request_done(_CONFIGURING.pop("ecobee")) hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP) - discovery.load_platform( - hass, 'climate', DOMAIN, {'hold_temp': hold_temp}, config) - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'weather', DOMAIN, {}, config) + discovery.load_platform(hass, "climate", DOMAIN, {"hold_temp": hold_temp}, config) + discovery.load_platform(hass, "sensor", DOMAIN, {}, config) + discovery.load_platform(hass, "binary_sensor", DOMAIN, {}, config) + discovery.load_platform(hass, "weather", DOMAIN, {}, config) class EcobeeData: @@ -83,13 +90,14 @@ class EcobeeData: def __init__(self, config_file): """Init the Ecobee data object.""" from pyecobee import Ecobee + self.ecobee = Ecobee(config_file) @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from pyecobee.""" self.ecobee.update() - _LOGGER.info("Ecobee data updated successfully") + _LOGGER.debug("Ecobee data updated successfully") def setup(hass, config): @@ -100,7 +108,7 @@ def setup(hass, config): """ global NETWORK - if 'ecobee' in _CONFIGURING: + if "ecobee" in _CONFIGURING: return # Create ecobee.conf if it doesn't exist diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 0989b9ded97..a3cd49ff458 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -2,7 +2,7 @@ from homeassistant.components import ecobee from homeassistant.components.binary_sensor import BinarySensorDevice -ECOBEE_CONFIG_FILE = 'ecobee.conf' +ECOBEE_CONFIG_FILE = "ecobee.conf" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -13,11 +13,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = list() for index in range(len(data.ecobee.thermostats)): for sensor in data.ecobee.get_remote_sensors(index): - for item in sensor['capability']: - if item['type'] != 'occupancy': + for item in sensor["capability"]: + if item["type"] != "occupancy": continue - dev.append(EcobeeBinarySensor(sensor['name'], index)) + dev.append(EcobeeBinarySensor(sensor["name"], index)) add_entities(dev, True) @@ -27,11 +27,11 @@ class EcobeeBinarySensor(BinarySensorDevice): def __init__(self, sensor_name, sensor_index): """Initialize the Ecobee sensor.""" - self._name = sensor_name + ' Occupancy' + self._name = sensor_name + " Occupancy" self.sensor_name = sensor_name self.index = sensor_index self._state = None - self._device_class = 'occupancy' + self._device_class = "occupancy" @property def name(self): @@ -41,7 +41,7 @@ class EcobeeBinarySensor(BinarySensorDevice): @property def is_on(self): """Return the status of the sensor.""" - return self._state == 'true' + return self._state == "true" @property def device_class(self): @@ -53,7 +53,6 @@ class EcobeeBinarySensor(BinarySensorDevice): data = ecobee.NETWORK data.update() for sensor in data.ecobee.get_remote_sensors(self.index): - for item in sensor['capability']: - if (item['type'] == 'occupancy' and - self.sensor_name == sensor['name']): - self._state = item['value'] + for item in sensor["capability"]: + if item["type"] == "occupancy" and self.sensor_name == sensor["name"]: + self._state = item["value"] diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 058d9f43f83..d9af0f93e11 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -8,67 +8,110 @@ import voluptuous as vol from homeassistant.components import ecobee from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - DOMAIN, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_FAN_MODE, - PRESET_AWAY, FAN_AUTO, FAN_ON, CURRENT_HVAC_OFF, CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, SUPPORT_PRESET_MODE + DOMAIN, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_AUTO, + HVAC_MODE_OFF, + ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_HIGH, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_AUX_HEAT, + SUPPORT_TARGET_TEMPERATURE_RANGE, + SUPPORT_FAN_MODE, + PRESET_AWAY, + FAN_AUTO, + FAN_ON, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, + SUPPORT_PRESET_MODE, + PRESET_NONE, + CURRENT_HVAC_FAN, + CURRENT_HVAC_DRY, ) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT, TEMP_CELSIUS) + ATTR_ENTITY_ID, + STATE_ON, + ATTR_TEMPERATURE, + TEMP_FAHRENHEIT, +) import homeassistant.helpers.config_validation as cv _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time' -ATTR_RESUME_ALL = 'resume_all' +ATTR_FAN_MIN_ON_TIME = "fan_min_on_time" +ATTR_RESUME_ALL = "resume_all" DEFAULT_RESUME_ALL = False -PRESET_TEMPERATURE = 'temp' -PRESET_VACATION = 'vacation' -PRESET_AUX_HEAT_ONLY = 'aux_heat_only' -PRESET_HOLD_NEXT_TRANSITION = 'next_transition' -PRESET_HOLD_INDEFINITE = 'indefinite' -AWAY_MODE = 'awayMode' +PRESET_TEMPERATURE = "temp" +PRESET_VACATION = "vacation" +PRESET_HOLD_NEXT_TRANSITION = "next_transition" +PRESET_HOLD_INDEFINITE = "indefinite" +AWAY_MODE = "awayMode" +PRESET_HOME = "home" +PRESET_SLEEP = "sleep" # Order matters, because for reverse mapping we don't want to map HEAT to AUX -ECOBEE_HVAC_TO_HASS = collections.OrderedDict([ - ('heat', HVAC_MODE_HEAT), - ('cool', HVAC_MODE_COOL), - ('auto', HVAC_MODE_AUTO), - ('off', HVAC_MODE_OFF), - ('auxHeatOnly', HVAC_MODE_HEAT), -]) +ECOBEE_HVAC_TO_HASS = collections.OrderedDict( + [ + ("heat", HVAC_MODE_HEAT), + ("cool", HVAC_MODE_COOL), + ("auto", HVAC_MODE_AUTO), + ("off", HVAC_MODE_OFF), + ("auxHeatOnly", HVAC_MODE_HEAT), + ] +) -PRESET_TO_ECOBEE_HOLD = { - PRESET_HOLD_NEXT_TRANSITION: 'nextTransition', - PRESET_HOLD_INDEFINITE: 'indefinite', +ECOBEE_HVAC_ACTION_TO_HASS = { + # Map to None if we do not know how to represent. + "heatPump": CURRENT_HVAC_HEAT, + "heatPump2": CURRENT_HVAC_HEAT, + "heatPump3": CURRENT_HVAC_HEAT, + "compCool1": CURRENT_HVAC_COOL, + "compCool2": CURRENT_HVAC_COOL, + "auxHeat1": CURRENT_HVAC_HEAT, + "auxHeat2": CURRENT_HVAC_HEAT, + "auxHeat3": CURRENT_HVAC_HEAT, + "fan": CURRENT_HVAC_FAN, + "humidifier": None, + "dehumidifier": CURRENT_HVAC_DRY, + "ventilator": CURRENT_HVAC_FAN, + "economizer": CURRENT_HVAC_FAN, + "compHotWater": None, + "auxHotWater": None, } -PRESET_MODES = [ - PRESET_AWAY, - PRESET_TEMPERATURE, - PRESET_HOLD_NEXT_TRANSITION, - PRESET_HOLD_INDEFINITE -] +PRESET_TO_ECOBEE_HOLD = { + PRESET_HOLD_NEXT_TRANSITION: "nextTransition", + PRESET_HOLD_INDEFINITE: "indefinite", +} -SERVICE_SET_FAN_MIN_ON_TIME = 'ecobee_set_fan_min_on_time' -SERVICE_RESUME_PROGRAM = 'ecobee_resume_program' +SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time" +SERVICE_RESUME_PROGRAM = "ecobee_resume_program" -SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), -}) +SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), + } +) -RESUME_PROGRAM_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean, -}) +RESUME_PROGRAM_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean, + } +) -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | - SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_RANGE | - SUPPORT_FAN_MODE) +SUPPORT_FLAGS = ( + SUPPORT_TARGET_TEMPERATURE + | SUPPORT_PRESET_MODE + | SUPPORT_AUX_HEAT + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_FAN_MODE +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -76,12 +119,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return data = ecobee.NETWORK - hold_temp = discovery_info['hold_temp'] + hold_temp = discovery_info["hold_temp"] _LOGGER.info( - "Loading ecobee thermostat component with hold_temp set to %s", - hold_temp) - devices = [Thermostat(data, index, hold_temp) - for index in range(len(data.ecobee.thermostats))] + "Loading ecobee thermostat component with hold_temp set to %s", hold_temp + ) + devices = [ + Thermostat(data, index, hold_temp) + for index in range(len(data.ecobee.thermostats)) + ] add_entities(devices) def fan_min_on_time_set_service(service): @@ -90,8 +135,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME] if entity_id: - target_thermostats = [device for device in devices - if device.entity_id in entity_id] + target_thermostats = [ + device for device in devices if device.entity_id in entity_id + ] else: target_thermostats = devices @@ -106,8 +152,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): resume_all = service.data.get(ATTR_RESUME_ALL) if entity_id: - target_thermostats = [device for device in devices - if device.entity_id in entity_id] + target_thermostats = [ + device for device in devices if device.entity_id in entity_id + ] else: target_thermostats = devices @@ -117,12 +164,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): thermostat.schedule_update_ha_state(True) hass.services.register( - DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service, - schema=SET_FAN_MIN_ON_TIME_SCHEMA) + DOMAIN, + SERVICE_SET_FAN_MIN_ON_TIME, + fan_min_on_time_set_service, + schema=SET_FAN_MIN_ON_TIME_SCHEMA, + ) hass.services.register( - DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, - schema=RESUME_PROGRAM_SCHEMA) + DOMAIN, + SERVICE_RESUME_PROGRAM, + resume_program_set_service, + schema=RESUME_PROGRAM_SCHEMA, + ) class Thermostat(ClimateDevice): @@ -132,15 +185,24 @@ class Thermostat(ClimateDevice): """Initialize the thermostat.""" self.data = data self.thermostat_index = thermostat_index - self.thermostat = self.data.ecobee.get_thermostat( - self.thermostat_index) - self._name = self.thermostat['name'] + self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + self._name = self.thermostat["name"] self.hold_temp = hold_temp self.vacation = None - self._climate_list = self.climate_list - self._operation_list = [ - HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_OFF - ] + + self._operation_list = [] + if self.thermostat["settings"]["heatStages"]: + self._operation_list.append(HVAC_MODE_HEAT) + if self.thermostat["settings"]["coolStages"]: + self._operation_list.append(HVAC_MODE_COOL) + if len(self._operation_list) == 2: + self._operation_list.insert(0, HVAC_MODE_AUTO) + self._operation_list.append(HVAC_MODE_OFF) + + self._preset_modes = { + comfort["climateRef"]: comfort["name"] + for comfort in self.thermostat["program"]["climates"] + } self._fan_modes = [FAN_AUTO, FAN_ON] self.update_without_throttle = False @@ -152,8 +214,12 @@ class Thermostat(ClimateDevice): else: self.data.update() - self.thermostat = self.data.ecobee.get_thermostat( - self.thermostat_index) + self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + + @property + def available(self): + """Return if device is available.""" + return self.thermostat["runtime"]["connected"] @property def supported_features(self): @@ -163,33 +229,30 @@ class Thermostat(ClimateDevice): @property def name(self): """Return the name of the Ecobee Thermostat.""" - return self.thermostat['name'] + return self.thermostat["name"] @property def temperature_unit(self): """Return the unit of measurement.""" - if self.thermostat['settings']['useCelsius']: - return TEMP_CELSIUS - return TEMP_FAHRENHEIT @property def current_temperature(self): """Return the current temperature.""" - return self.thermostat['runtime']['actualTemperature'] / 10.0 + return self.thermostat["runtime"]["actualTemperature"] / 10.0 @property def target_temperature_low(self): """Return the lower bound temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_AUTO: - return self.thermostat['runtime']['desiredHeat'] / 10.0 + return self.thermostat["runtime"]["desiredHeat"] / 10.0 return None @property def target_temperature_high(self): """Return the upper bound temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_AUTO: - return self.thermostat['runtime']['desiredCool'] / 10.0 + return self.thermostat["runtime"]["desiredCool"] / 10.0 return None @property @@ -198,22 +261,22 @@ class Thermostat(ClimateDevice): if self.hvac_mode == HVAC_MODE_AUTO: return None if self.hvac_mode == HVAC_MODE_HEAT: - return self.thermostat['runtime']['desiredHeat'] / 10.0 + return self.thermostat["runtime"]["desiredHeat"] / 10.0 if self.hvac_mode == HVAC_MODE_COOL: - return self.thermostat['runtime']['desiredCool'] / 10.0 + return self.thermostat["runtime"]["desiredCool"] / 10.0 return None @property def fan(self): """Return the current fan status.""" - if 'fan' in self.thermostat['equipmentStatus']: + if "fan" in self.thermostat["equipmentStatus"]: return STATE_ON return HVAC_MODE_OFF @property def fan_mode(self): """Return the fan setting.""" - return self.thermostat['runtime']['desiredFanMode'] + return self.thermostat["runtime"]["desiredFanMode"] @property def fan_modes(self): @@ -223,163 +286,184 @@ class Thermostat(ClimateDevice): @property def preset_mode(self): """Return current preset mode.""" - events = self.thermostat['events'] + events = self.thermostat["events"] for event in events: - if not event['running']: + if not event["running"]: continue - if event['type'] == 'hold': - if event['holdClimateRef'] == 'away': - if int(event['endDate'][0:4]) - \ - int(event['startDate'][0:4]) <= 1: - # A temporary hold from away climate is a hold - return PRESET_AWAY - # A permanent hold from away climate - return PRESET_AWAY - if event['holdClimateRef'] != "": - # Any other hold based on climate - return event['holdClimateRef'] + if event["type"] == "hold": + if event["holdClimateRef"] in self._preset_modes: + return self._preset_modes[event["holdClimateRef"]] + # Any hold not based on a climate is a temp hold return PRESET_TEMPERATURE - if event['type'].startswith('auto'): + if event["type"].startswith("auto"): # All auto modes are treated as holds - return event['type'][4:].lower() - if event['type'] == 'vacation': - self.vacation = event['name'] + return event["type"][4:].lower() + if event["type"] == "vacation": + self.vacation = event["name"] return PRESET_VACATION - if self.is_aux_heat: - return PRESET_AUX_HEAT_ONLY - return None @property def hvac_mode(self): """Return current operation.""" - return ECOBEE_HVAC_TO_HASS[self.thermostat['settings']['hvacMode']] + return ECOBEE_HVAC_TO_HASS[self.thermostat["settings"]["hvacMode"]] @property def hvac_modes(self): """Return the operation modes list.""" return self._operation_list - @property - def climate_mode(self): - """Return current mode, as the user-visible name.""" - cur = self.thermostat['program']['currentClimateRef'] - climates = self.thermostat['program']['climates'] - current = list(filter(lambda x: x['climateRef'] == cur, climates)) - return current[0]['name'] - @property def current_humidity(self) -> Optional[int]: """Return the current humidity.""" - return self.thermostat['runtime']['actualHumidity'] + return self.thermostat["runtime"]["actualHumidity"] @property def hvac_action(self): - """Return current HVAC action.""" - status = self.thermostat['equipmentStatus'] - operation = None + """Return current HVAC action. - if status == '': - operation = CURRENT_HVAC_OFF - elif 'Cool' in status: - operation = CURRENT_HVAC_COOL - elif 'auxHeat' in status or 'heatPump' in status: - operation = CURRENT_HVAC_HEAT + Ecobee returns a CSV string with different equipment that is active. + We are prioritizing any heating/cooling equipment, otherwase look at + drying/fanning. Idle if nothing going on. - return operation + We are unable to map all actions to HA equivalents. + """ + if self.thermostat["equipmentStatus"] == "": + return CURRENT_HVAC_IDLE + + actions = [ + ECOBEE_HVAC_ACTION_TO_HASS[status] + for status in self.thermostat["equipmentStatus"].split(",") + if ECOBEE_HVAC_ACTION_TO_HASS[status] is not None + ] + + for action in ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_FAN, + ): + if action in actions: + return action + + return CURRENT_HVAC_IDLE @property def device_state_attributes(self): """Return device specific state attributes.""" - status = self.thermostat['equipmentStatus'] + status = self.thermostat["equipmentStatus"] return { "fan": self.fan, - "climate_mode": self.climate_mode, "equipment_running": status, - "climate_list": self.climate_list, - "fan_min_on_time": self.thermostat['settings']['fanMinOnTime'] + "fan_min_on_time": self.thermostat["settings"]["fanMinOnTime"], } @property def is_aux_heat(self): """Return true if aux heater.""" - return 'auxHeat' in self.thermostat['equipmentStatus'] + return "auxHeat" in self.thermostat["equipmentStatus"] - def set_preset(self, preset): + def set_preset_mode(self, preset_mode): """Activate a preset.""" - if preset == self.preset_mode: + if preset_mode == self.preset_mode: return self.update_without_throttle = True # If we are currently in vacation mode, cancel it. if self.preset_mode == PRESET_VACATION: - self.data.ecobee.delete_vacation( - self.thermostat_index, self.vacation) + self.data.ecobee.delete_vacation(self.thermostat_index, self.vacation) - if preset == PRESET_AWAY: - self.data.ecobee.set_climate_hold(self.thermostat_index, 'away', - 'indefinite') + if preset_mode == PRESET_AWAY: + self.data.ecobee.set_climate_hold( + self.thermostat_index, "away", "indefinite" + ) - elif preset == PRESET_TEMPERATURE: + elif preset_mode == PRESET_TEMPERATURE: self.set_temp_hold(self.current_temperature) - elif preset in (PRESET_HOLD_NEXT_TRANSITION, PRESET_HOLD_INDEFINITE): + elif preset_mode in (PRESET_HOLD_NEXT_TRANSITION, PRESET_HOLD_INDEFINITE): self.data.ecobee.set_climate_hold( - self.thermostat_index, PRESET_TO_ECOBEE_HOLD[preset], - self.hold_preference()) + self.thermostat_index, + PRESET_TO_ECOBEE_HOLD[preset_mode], + self.hold_preference(), + ) - elif preset is None: + elif preset_mode == PRESET_NONE: self.data.ecobee.resume_program(self.thermostat_index) + elif preset_mode in self.preset_modes: + climate_ref = None + + for comfort in self.thermostat["program"]["climates"]: + if comfort["name"] == preset_mode: + climate_ref = comfort["climateRef"] + break + + if climate_ref is not None: + self.data.ecobee.set_climate_hold( + self.thermostat_index, climate_ref, self.hold_preference() + ) + else: + _LOGGER.warning("Received unknown preset mode: %s", preset_mode) + else: - _LOGGER.warning("Received invalid preset: %s", preset) + self.data.ecobee.set_climate_hold( + self.thermostat_index, preset_mode, self.hold_preference() + ) @property def preset_modes(self): """Return available preset modes.""" - return PRESET_MODES + return list(self._preset_modes.values()) def set_auto_temp_hold(self, heat_temp, cool_temp): """Set temperature hold in auto mode.""" if cool_temp is not None: cool_temp_setpoint = cool_temp else: - cool_temp_setpoint = ( - self.thermostat['runtime']['desiredCool'] / 10.0) + cool_temp_setpoint = self.thermostat["runtime"]["desiredCool"] / 10.0 if heat_temp is not None: heat_temp_setpoint = heat_temp else: - heat_temp_setpoint = ( - self.thermostat['runtime']['desiredCool'] / 10.0) + heat_temp_setpoint = self.thermostat["runtime"]["desiredCool"] / 10.0 - self.data.ecobee.set_hold_temp(self.thermostat_index, - cool_temp_setpoint, heat_temp_setpoint, - self.hold_preference()) - _LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, " - "cool=%s, is=%s", heat_temp, - isinstance(heat_temp, (int, float)), cool_temp, - isinstance(cool_temp, (int, float))) + self.data.ecobee.set_hold_temp( + self.thermostat_index, + cool_temp_setpoint, + heat_temp_setpoint, + self.hold_preference(), + ) + _LOGGER.debug( + "Setting ecobee hold_temp to: heat=%s, is=%s, " "cool=%s, is=%s", + heat_temp, + isinstance(heat_temp, (int, float)), + cool_temp, + isinstance(cool_temp, (int, float)), + ) self.update_without_throttle = True def set_fan_mode(self, fan_mode): """Set the fan mode. Valid values are "on" or "auto".""" - if fan_mode.lower() != STATE_ON and \ - fan_mode.lower() != HVAC_MODE_AUTO: + if fan_mode.lower() != STATE_ON and fan_mode.lower() != HVAC_MODE_AUTO: error = "Invalid fan_mode value: Valid values are 'on' or 'auto'" _LOGGER.error(error) return - cool_temp = self.thermostat['runtime']['desiredCool'] / 10.0 - heat_temp = self.thermostat['runtime']['desiredHeat'] / 10.0 - self.data.ecobee.set_fan_mode(self.thermostat_index, fan_mode, - cool_temp, heat_temp, - self.hold_preference()) + cool_temp = self.thermostat["runtime"]["desiredCool"] / 10.0 + heat_temp = self.thermostat["runtime"]["desiredHeat"] / 10.0 + self.data.ecobee.set_fan_mode( + self.thermostat_index, + fan_mode, + cool_temp, + heat_temp, + self.hold_preference(), + ) _LOGGER.info("Setting fan mode to: %s", fan_mode) @@ -394,12 +478,11 @@ class Thermostat(ClimateDevice): heatCoolMinDelta property. https://www.ecobee.com/home/developer/api/examples/ex5.shtml """ - if self.hvac_mode == HVAC_MODE_HEAT or \ - self.hvac_mode == HVAC_MODE_COOL: + if self.hvac_mode == HVAC_MODE_HEAT or self.hvac_mode == HVAC_MODE_COOL: heat_temp = temp cool_temp = temp else: - delta = self.thermostat['settings']['heatCoolMinDelta'] / 10 + delta = self.thermostat["settings"]["heatCoolMinDelta"] / 10 heat_temp = temp - delta cool_temp = temp + delta self.set_auto_temp_hold(heat_temp, cool_temp) @@ -410,14 +493,14 @@ class Thermostat(ClimateDevice): high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) temp = kwargs.get(ATTR_TEMPERATURE) - if self.hvac_mode == HVAC_MODE_AUTO and \ - (low_temp is not None or high_temp is not None): + if self.hvac_mode == HVAC_MODE_AUTO and ( + low_temp is not None or high_temp is not None + ): self.set_auto_temp_hold(low_temp, high_temp) elif temp is not None: self.set_temp_hold(temp) else: - _LOGGER.error( - "Missing valid arguments for set_temperature in %s", kwargs) + _LOGGER.error("Missing valid arguments for set_temperature in %s", kwargs) def set_humidity(self, humidity): """Set the humidity level.""" @@ -425,8 +508,9 @@ class Thermostat(ClimateDevice): def set_hvac_mode(self, hvac_mode): """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" - ecobee_value = next((k for k, v in ECOBEE_HVAC_TO_HASS.items() - if v == hvac_mode), None) + ecobee_value = next( + (k for k, v in ECOBEE_HVAC_TO_HASS.items() if v == hvac_mode), None + ) if ecobee_value is None: _LOGGER.error("Invalid mode for set_hvac_mode: %s", hvac_mode) return @@ -435,30 +519,24 @@ class Thermostat(ClimateDevice): def set_fan_min_on_time(self, fan_min_on_time): """Set the minimum fan on time.""" - self.data.ecobee.set_fan_min_on_time( - self.thermostat_index, fan_min_on_time) + self.data.ecobee.set_fan_min_on_time(self.thermostat_index, fan_min_on_time) self.update_without_throttle = True def resume_program(self, resume_all): """Resume the thermostat schedule program.""" self.data.ecobee.resume_program( - self.thermostat_index, 'true' if resume_all else 'false') + self.thermostat_index, "true" if resume_all else "false" + ) self.update_without_throttle = True def hold_preference(self): """Return user preference setting for hold time.""" # Values returned from thermostat are 'useEndTime4hour', # 'useEndTime2hour', 'nextTransition', 'indefinite', 'askMe' - default = self.thermostat['settings']['holdAction'] - if default == 'nextTransition': + default = self.thermostat["settings"]["holdAction"] + if default == "nextTransition": return default # add further conditions if other hold durations should be # supported; note that this should not include 'indefinite' # as an indefinite away hold is interpreted as away_mode - return 'nextTransition' - - @property - def climate_list(self): - """Return the list of climates currently available.""" - climates = self.thermostat['program']['climates'] - return list(map((lambda x: x['name']), climates)) + return "nextTransition" diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index d2aa7f0b515..31cca1e676f 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -3,7 +3,7 @@ "name": "Ecobee", "documentation": "https://www.home-assistant.io/components/ecobee", "requirements": [ - "python-ecobee-api==0.0.18" + "python-ecobee-api==0.0.21" ], "dependencies": ["configurator"], "codeowners": [] diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index d6e4e8f0c63..bb6861a1492 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -5,16 +5,15 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components import ecobee -from homeassistant.components.notify import ( - BaseNotificationService, PLATFORM_SCHEMA) +from homeassistant.components.notify import BaseNotificationService, PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) -CONF_INDEX = 'index' +CONF_INDEX = "index" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_INDEX, default=0): cv.positive_int, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_INDEX, default=0): cv.positive_int} +) def get_service(hass, config, discovery_info=None): diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 436903a645f..d21f937dd20 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -1,14 +1,17 @@ """Support for Ecobee sensors.""" from homeassistant.components import ecobee from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT) + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + TEMP_FAHRENHEIT, +) from homeassistant.helpers.entity import Entity -ECOBEE_CONFIG_FILE = 'ecobee.conf' +ECOBEE_CONFIG_FILE = "ecobee.conf" SENSOR_TYPES = { - 'temperature': ['Temperature', TEMP_FAHRENHEIT], - 'humidity': ['Humidity', '%'] + "temperature": ["Temperature", TEMP_FAHRENHEIT], + "humidity": ["Humidity", "%"], } @@ -20,11 +23,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = list() for index in range(len(data.ecobee.thermostats)): for sensor in data.ecobee.get_remote_sensors(index): - for item in sensor['capability']: - if item['type'] not in ('temperature', 'humidity'): + for item in sensor["capability"]: + if item["type"] not in ("temperature", "humidity"): continue - dev.append(EcobeeSensor(sensor['name'], item['type'], index)) + dev.append(EcobeeSensor(sensor["name"], item["type"], index)) add_entities(dev, True) @@ -34,7 +37,7 @@ class EcobeeSensor(Entity): def __init__(self, sensor_name, sensor_type, sensor_index): """Initialize the sensor.""" - self._name = '{} {}'.format(sensor_name, SENSOR_TYPES[sensor_type][0]) + self._name = "{} {}".format(sensor_name, SENSOR_TYPES[sensor_type][0]) self.sensor_name = sensor_name self.type = sensor_type self.index = sensor_index @@ -68,11 +71,9 @@ class EcobeeSensor(Entity): data = ecobee.NETWORK data.update() for sensor in data.ecobee.get_remote_sensors(self.index): - for item in sensor['capability']: - if (item['type'] == self.type and - self.sensor_name == sensor['name']): - if (self.type == 'temperature' and - item['value'] != 'unknown'): - self._state = float(item['value']) / 10 + for item in sensor["capability"]: + if item["type"] == self.type and self.sensor_name == sensor["name"]: + if self.type == "temperature" and item["value"] != "unknown": + self._state = float(item["value"]) / 10 else: - self._state = item['value'] + self._state = item["value"] diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index f5058434f38..0680ef67f82 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -3,14 +3,19 @@ from datetime import datetime from homeassistant.components import ecobee from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_SPEED, WeatherEntity) + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_SPEED, + WeatherEntity, +) from homeassistant.const import TEMP_FAHRENHEIT -ATTR_FORECAST_TEMP_HIGH = 'temphigh' -ATTR_FORECAST_PRESSURE = 'pressure' -ATTR_FORECAST_VISIBILITY = 'visibility' -ATTR_FORECAST_HUMIDITY = 'humidity' +ATTR_FORECAST_TEMP_HIGH = "temphigh" +ATTR_FORECAST_PRESSURE = "pressure" +ATTR_FORECAST_VISIBILITY = "visibility" +ATTR_FORECAST_HUMIDITY = "humidity" MISSING_DATA = -5002 @@ -23,8 +28,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = ecobee.NETWORK for index in range(len(data.ecobee.thermostats)): thermostat = data.ecobee.get_thermostat(index) - if 'weather' in thermostat: - dev.append(EcobeeWeather(thermostat['name'], index)) + if "weather" in thermostat: + dev.append(EcobeeWeather(thermostat["name"], index)) add_entities(dev, True) @@ -41,7 +46,7 @@ class EcobeeWeather(WeatherEntity): def get_forecast(self, index, param): """Retrieve forecast parameter.""" try: - forecast = self.weather['forecasts'][index] + forecast = self.weather["forecasts"][index] return forecast[param] except (ValueError, IndexError, KeyError): raise ValueError @@ -55,7 +60,7 @@ class EcobeeWeather(WeatherEntity): def condition(self): """Return the current condition.""" try: - return self.get_forecast(0, 'condition') + return self.get_forecast(0, "condition") except ValueError: return None @@ -63,7 +68,7 @@ class EcobeeWeather(WeatherEntity): def temperature(self): """Return the temperature.""" try: - return float(self.get_forecast(0, 'temperature')) / 10 + return float(self.get_forecast(0, "temperature")) / 10 except ValueError: return None @@ -76,7 +81,7 @@ class EcobeeWeather(WeatherEntity): def pressure(self): """Return the pressure.""" try: - return int(self.get_forecast(0, 'pressure')) + return int(self.get_forecast(0, "pressure")) except ValueError: return None @@ -84,7 +89,7 @@ class EcobeeWeather(WeatherEntity): def humidity(self): """Return the humidity.""" try: - return int(self.get_forecast(0, 'relativeHumidity')) + return int(self.get_forecast(0, "relativeHumidity")) except ValueError: return None @@ -92,7 +97,7 @@ class EcobeeWeather(WeatherEntity): def visibility(self): """Return the visibility.""" try: - return int(self.get_forecast(0, 'visibility')) + return int(self.get_forecast(0, "visibility")) except ValueError: return None @@ -100,7 +105,7 @@ class EcobeeWeather(WeatherEntity): def wind_speed(self): """Return the wind speed.""" try: - return int(self.get_forecast(0, 'windSpeed')) + return int(self.get_forecast(0, "windSpeed")) except ValueError: return None @@ -108,7 +113,7 @@ class EcobeeWeather(WeatherEntity): def wind_bearing(self): """Return the wind direction.""" try: - return int(self.get_forecast(0, 'windBearing')) + return int(self.get_forecast(0, "windBearing")) except ValueError: return None @@ -116,8 +121,8 @@ class EcobeeWeather(WeatherEntity): def attribution(self): """Return the attribution.""" if self.weather: - station = self.weather.get('weatherStation', "UNKNOWN") - time = self.weather.get('timestamp', "UNKNOWN") + station = self.weather.get("weatherStation", "UNKNOWN") + time = self.weather.get("timestamp", "UNKNOWN") return "Ecobee weather provided by {} at {}".format(station, time) return None @@ -126,28 +131,27 @@ class EcobeeWeather(WeatherEntity): """Return the forecast array.""" try: forecasts = [] - for day in self.weather['forecasts']: - date_time = datetime.strptime(day['dateTime'], - '%Y-%m-%d %H:%M:%S').isoformat() + for day in self.weather["forecasts"]: + date_time = datetime.strptime( + day["dateTime"], "%Y-%m-%d %H:%M:%S" + ).isoformat() forecast = { ATTR_FORECAST_TIME: date_time, - ATTR_FORECAST_CONDITION: day['condition'], - ATTR_FORECAST_TEMP: float(day['tempHigh']) / 10, + ATTR_FORECAST_CONDITION: day["condition"], + ATTR_FORECAST_TEMP: float(day["tempHigh"]) / 10, } - if day['tempHigh'] == MISSING_DATA: + if day["tempHigh"] == MISSING_DATA: break - if day['tempLow'] != MISSING_DATA: - forecast[ATTR_FORECAST_TEMP_LOW] = \ - float(day['tempLow']) / 10 - if day['pressure'] != MISSING_DATA: - forecast[ATTR_FORECAST_PRESSURE] = int(day['pressure']) - if day['windSpeed'] != MISSING_DATA: - forecast[ATTR_FORECAST_WIND_SPEED] = int(day['windSpeed']) - if day['visibility'] != MISSING_DATA: - forecast[ATTR_FORECAST_WIND_SPEED] = int(day['visibility']) - if day['relativeHumidity'] != MISSING_DATA: - forecast[ATTR_FORECAST_HUMIDITY] = \ - int(day['relativeHumidity']) + if day["tempLow"] != MISSING_DATA: + forecast[ATTR_FORECAST_TEMP_LOW] = float(day["tempLow"]) / 10 + if day["pressure"] != MISSING_DATA: + forecast[ATTR_FORECAST_PRESSURE] = int(day["pressure"]) + if day["windSpeed"] != MISSING_DATA: + forecast[ATTR_FORECAST_WIND_SPEED] = int(day["windSpeed"]) + if day["visibility"] != MISSING_DATA: + forecast[ATTR_FORECAST_WIND_SPEED] = int(day["visibility"]) + if day["relativeHumidity"] != MISSING_DATA: + forecast[ATTR_FORECAST_HUMIDITY] = int(day["relativeHumidity"]) forecasts.append(forecast) return forecasts except (ValueError, IndexError, KeyError): @@ -158,4 +162,4 @@ class EcobeeWeather(WeatherEntity): data = ecobee.NETWORK data.update() thermostat = data.ecobee.get_thermostat(self._index) - self.weather = thermostat.get('weather', None) + self.weather = thermostat.get("weather", None) diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 4c47e24d705..1c8deae5b99 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -5,58 +5,71 @@ import logging import voluptuous as vol from homeassistant.components.water_heater import ( - DOMAIN, PLATFORM_SCHEMA, STATE_ECO, STATE_ELECTRIC, STATE_GAS, - STATE_HEAT_PUMP, STATE_HIGH_DEMAND, STATE_OFF, STATE_PERFORMANCE, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, WaterHeaterDevice) + DOMAIN, + PLATFORM_SCHEMA, + STATE_ECO, + STATE_ELECTRIC, + STATE_GAS, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_OFF, + STATE_PERFORMANCE, + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterDevice, +) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_PASSWORD, CONF_USERNAME, - TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + CONF_PASSWORD, + CONF_USERNAME, + TEMP_FAHRENHEIT, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_VACATION_START = 'next_vacation_start_date' -ATTR_VACATION_END = 'next_vacation_end_date' -ATTR_ON_VACATION = 'on_vacation' -ATTR_TODAYS_ENERGY_USAGE = 'todays_energy_usage' -ATTR_IN_USE = 'in_use' +ATTR_VACATION_START = "next_vacation_start_date" +ATTR_VACATION_END = "next_vacation_end_date" +ATTR_ON_VACATION = "on_vacation" +ATTR_TODAYS_ENERGY_USAGE = "todays_energy_usage" +ATTR_IN_USE = "in_use" -ATTR_START_DATE = 'start_date' -ATTR_END_DATE = 'end_date' +ATTR_START_DATE = "start_date" +ATTR_END_DATE = "end_date" -SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE) +SUPPORT_FLAGS_HEATER = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE -SERVICE_ADD_VACATION = 'econet_add_vacation' -SERVICE_DELETE_VACATION = 'econet_delete_vacation' +SERVICE_ADD_VACATION = "econet_add_vacation" +SERVICE_DELETE_VACATION = "econet_delete_vacation" -ADD_VACATION_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_START_DATE): cv.positive_int, - vol.Required(ATTR_END_DATE): cv.positive_int, -}) +ADD_VACATION_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_START_DATE): cv.positive_int, + vol.Required(ATTR_END_DATE): cv.positive_int, + } +) -DELETE_VACATION_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) +DELETE_VACATION_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) -ECONET_DATA = 'econet' +ECONET_DATA = "econet" ECONET_STATE_TO_HA = { - 'Energy Saver': STATE_ECO, - 'gas': STATE_GAS, - 'High Demand': STATE_HIGH_DEMAND, - 'Off': STATE_OFF, - 'Performance': STATE_PERFORMANCE, - 'Heat Pump Only': STATE_HEAT_PUMP, - 'Electric-Only': STATE_ELECTRIC, - 'Electric': STATE_ELECTRIC, - 'Heat Pump': STATE_HEAT_PUMP + "Energy Saver": STATE_ECO, + "gas": STATE_GAS, + "High Demand": STATE_HIGH_DEMAND, + "Off": STATE_OFF, + "Performance": STATE_PERFORMANCE, + "Heat Pump Only": STATE_HEAT_PUMP, + "Electric-Only": STATE_ELECTRIC, + "Electric": STATE_ELECTRIC, + "Heat Pump": STATE_HEAT_PUMP, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -64,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): from pyeconet.api import PyEcoNet hass.data[ECONET_DATA] = {} - hass.data[ECONET_DATA]['water_heaters'] = [] + hass.data[ECONET_DATA]["water_heaters"] = [] username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -72,17 +85,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): econet = PyEcoNet(username, password) water_heaters = econet.get_water_heaters() hass_water_heaters = [ - EcoNetWaterHeater(water_heater) for water_heater in water_heaters] + EcoNetWaterHeater(water_heater) for water_heater in water_heaters + ] add_entities(hass_water_heaters) - hass.data[ECONET_DATA]['water_heaters'].extend(hass_water_heaters) + hass.data[ECONET_DATA]["water_heaters"].extend(hass_water_heaters) def service_handle(service): """Handle the service calls.""" - entity_ids = service.data.get('entity_id') - all_heaters = hass.data[ECONET_DATA]['water_heaters'] + entity_ids = service.data.get("entity_id") + all_heaters = hass.data[ECONET_DATA]["water_heaters"] _heaters = [ - x for x in all_heaters - if not entity_ids or x.entity_id in entity_ids] + x for x in all_heaters if not entity_ids or x.entity_id in entity_ids + ] for _water_heater in _heaters: if service.service == SERVICE_ADD_VACATION: @@ -95,11 +109,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _water_heater.schedule_update_ha_state(True) - hass.services.register(DOMAIN, SERVICE_ADD_VACATION, service_handle, - schema=ADD_VACATION_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_ADD_VACATION, service_handle, schema=ADD_VACATION_SCHEMA + ) - hass.services.register(DOMAIN, SERVICE_DELETE_VACATION, service_handle, - schema=DELETE_VACATION_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_DELETE_VACATION, service_handle, schema=DELETE_VACATION_SCHEMA + ) class EcoNetWaterHeater(WaterHeaterDevice): @@ -118,8 +134,11 @@ class EcoNetWaterHeater(WaterHeaterDevice): self.ha_state_to_econet[value] = key for mode in self.supported_modes: if mode not in ECONET_STATE_TO_HA: - error = "Invalid operation mode mapping. " + mode + \ - " doesn't map. Please report this." + error = ( + "Invalid operation mode mapping. " + + mode + + " doesn't map. Please report this." + ) _LOGGER.error(error) @property diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py index da87af722a6..76566912d12 100644 --- a/homeassistant/components/ecovacs/__init__.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -5,8 +5,7 @@ import string import voluptuous as vol -from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -17,19 +16,24 @@ DOMAIN = "ecovacs" CONF_COUNTRY = "country" CONF_CONTINENT = "continent" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string), - vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string), - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string), + vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) ECOVACS_DEVICES = "ecovacs_devices" # Generate a random device ID on each bootup -ECOVACS_API_DEVICEID = ''.join( +ECOVACS_API_DEVICEID = "".join( random.choice(string.ascii_uppercase + string.digits) for _ in range(8) ) @@ -42,11 +46,13 @@ def setup(hass, config): from sucks import EcoVacsAPI, VacBot - ecovacs_api = EcoVacsAPI(ECOVACS_API_DEVICEID, - config[DOMAIN].get(CONF_USERNAME), - EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), - config[DOMAIN].get(CONF_COUNTRY), - config[DOMAIN].get(CONF_CONTINENT)) + ecovacs_api = EcoVacsAPI( + ECOVACS_API_DEVICEID, + config[DOMAIN].get(CONF_USERNAME), + EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), + config[DOMAIN].get(CONF_COUNTRY), + config[DOMAIN].get(CONF_CONTINENT), + ) devices = ecovacs_api.devices() _LOGGER.debug("Ecobot devices: %s", devices) @@ -54,21 +60,26 @@ def setup(hass, config): for device in devices: _LOGGER.info( "Discovered Ecovacs device on account: %s with nickname %s", - device['did'], device['nick']) - vacbot = VacBot(ecovacs_api.uid, - ecovacs_api.REALM, - ecovacs_api.resource, - ecovacs_api.user_access_token, - device, - config[DOMAIN].get(CONF_CONTINENT).lower(), - monitor=True) + device["did"], + device["nick"], + ) + vacbot = VacBot( + ecovacs_api.uid, + ecovacs_api.REALM, + ecovacs_api.resource, + ecovacs_api.user_access_token, + device, + config[DOMAIN].get(CONF_CONTINENT).lower(), + monitor=True, + ) hass.data[ECOVACS_DEVICES].append(vacbot) def stop(event: object) -> None: """Shut down open connections to Ecovacs XMPP server.""" for device in hass.data[ECOVACS_DEVICES]: - _LOGGER.info("Shutting down connection to Ecovacs device %s", - device.vacuum['did']) + _LOGGER.info( + "Shutting down connection to Ecovacs device %s", device.vacuum["did"] + ) device.disconnect() # Listen for HA stop to disconnect. diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index ee374871d31..fdaf6291be5 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -2,9 +2,18 @@ import logging from homeassistant.components.vacuum import ( - SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, - SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, VacuumDevice) + SUPPORT_BATTERY, + SUPPORT_CLEAN_SPOT, + SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, + SUPPORT_RETURN_HOME, + SUPPORT_SEND_COMMAND, + SUPPORT_STATUS, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + VacuumDevice, +) from homeassistant.helpers.icon import icon_for_battery_level from . import ECOVACS_DEVICES @@ -12,12 +21,20 @@ from . import ECOVACS_DEVICES _LOGGER = logging.getLogger(__name__) SUPPORT_ECOVACS = ( - SUPPORT_BATTERY | SUPPORT_RETURN_HOME | SUPPORT_CLEAN_SPOT | - SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | SUPPORT_LOCATE | - SUPPORT_STATUS | SUPPORT_SEND_COMMAND | SUPPORT_FAN_SPEED) + SUPPORT_BATTERY + | SUPPORT_RETURN_HOME + | SUPPORT_CLEAN_SPOT + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_LOCATE + | SUPPORT_STATUS + | SUPPORT_SEND_COMMAND + | SUPPORT_FAN_SPEED +) -ATTR_ERROR = 'error' -ATTR_COMPONENT_PREFIX = 'component_' +ATTR_ERROR = "error" +ATTR_COMPONENT_PREFIX = "component_" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -36,11 +53,11 @@ class EcovacsVacuum(VacuumDevice): """Initialize the Ecovacs Vacuum.""" self.device = device self.device.connect_and_wait_until_ready() - if self.device.vacuum.get('nick', None) is not None: - self._name = '{}'.format(self.device.vacuum['nick']) + if self.device.vacuum.get("nick", None) is not None: + self._name = "{}".format(self.device.vacuum["nick"]) else: # In case there is no nickname defined, use the device id - self._name = '{}'.format(self.device.vacuum['did']) + self._name = "{}".format(self.device.vacuum["did"]) self._fan_speed = None self._error = None @@ -48,12 +65,9 @@ class EcovacsVacuum(VacuumDevice): async def async_added_to_hass(self) -> None: """Set up the event listeners now that hass is ready.""" - self.device.statusEvents.subscribe(lambda _: - self.schedule_update_ha_state()) - self.device.batteryEvents.subscribe(lambda _: - self.schedule_update_ha_state()) - self.device.lifespanEvents.subscribe(lambda _: - self.schedule_update_ha_state()) + self.device.statusEvents.subscribe(lambda _: self.schedule_update_ha_state()) + self.device.batteryEvents.subscribe(lambda _: self.schedule_update_ha_state()) + self.device.lifespanEvents.subscribe(lambda _: self.schedule_update_ha_state()) self.device.errorEvents.subscribe(self.on_error) def on_error(self, error): @@ -62,15 +76,14 @@ class EcovacsVacuum(VacuumDevice): This will not change the entity's state. If the error caused the state to change, that will come through as a separate on_status event """ - if error == 'no_error': + if error == "no_error": self._error = None else: self._error = error - self.hass.bus.fire('ecovacs_error', { - 'entity_id': self.entity_id, - 'error': error - }) + self.hass.bus.fire( + "ecovacs_error", {"entity_id": self.entity_id, "error": error} + ) self.schedule_update_ha_state() @property @@ -81,7 +94,7 @@ class EcovacsVacuum(VacuumDevice): @property def unique_id(self) -> str: """Return an unique ID.""" - return self.device.vacuum.get('did', None) + return self.device.vacuum.get("did", None) @property def is_on(self): @@ -111,13 +124,15 @@ class EcovacsVacuum(VacuumDevice): def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" from sucks import Charge + self.device.run(Charge()) @property def battery_icon(self): """Return the battery icon for the vacuum cleaner.""" return icon_for_battery_level( - battery_level=self.battery_level, charging=self.is_charging) + battery_level=self.battery_level, charging=self.is_charging + ) @property def battery_level(self): @@ -136,11 +151,13 @@ class EcovacsVacuum(VacuumDevice): def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" from sucks import FAN_SPEED_NORMAL, FAN_SPEED_HIGH + return [FAN_SPEED_NORMAL, FAN_SPEED_HIGH] def turn_on(self, **kwargs): """Turn the vacuum on and start cleaning.""" from sucks import Clean + self.device.run(Clean()) def turn_off(self, **kwargs): @@ -150,28 +167,32 @@ class EcovacsVacuum(VacuumDevice): def stop(self, **kwargs): """Stop the vacuum cleaner.""" from sucks import Stop + self.device.run(Stop()) def clean_spot(self, **kwargs): """Perform a spot clean-up.""" from sucks import Spot + self.device.run(Spot()) def locate(self, **kwargs): """Locate the vacuum cleaner.""" from sucks import PlaySound + self.device.run(PlaySound()) def set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if self.is_on: from sucks import Clean - self.device.run(Clean( - mode=self.device.clean_status, speed=fan_speed)) + + self.device.run(Clean(mode=self.device.clean_status, speed=fan_speed)) def send_command(self, command, params=None, **kwargs): """Send a command to a vacuum cleaner.""" from sucks import VacBotCommand + self.device.run(VacBotCommand(command, params)) @property diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index aad279934e5..5492582ebed 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -13,28 +13,36 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_UNKNOWN, TEMP_CELSIUS) + CONF_NAME, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + STATE_UNKNOWN, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_BEACONS = 'beacons' -CONF_BT_DEVICE_ID = 'bt_device_id' -CONF_INSTANCE = 'instance' -CONF_NAMESPACE = 'namespace' +CONF_BEACONS = "beacons" +CONF_BT_DEVICE_ID = "bt_device_id" +CONF_INSTANCE = "instance" +CONF_NAMESPACE = "namespace" -BEACON_SCHEMA = vol.Schema({ - vol.Required(CONF_NAMESPACE): cv.string, - vol.Required(CONF_INSTANCE): cv.string, - vol.Optional(CONF_NAME): cv.string -}) +BEACON_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAMESPACE): cv.string, + vol.Required(CONF_INSTANCE): cv.string, + vol.Optional(CONF_NAME): cv.string, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_BT_DEVICE_ID, default=0): cv.positive_int, - vol.Required(CONF_BEACONS): vol.Schema({cv.string: BEACON_SCHEMA}), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_BT_DEVICE_ID, default=0): cv.positive_int, + vol.Required(CONF_BEACONS): vol.Schema({cv.string: BEACON_SCHEMA}), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -80,8 +88,12 @@ def get_from_conf(config, config_key, length): """Retrieve value from config and validate length.""" string = config.get(config_key) if len(string) != length: - _LOGGER.error("Error in config parameter %s: Must be exactly %d " - "bytes. Device will not be added", config_key, length/2) + _LOGGER.error( + "Error in config parameter %s: Must be exactly %d " + "bytes. Device will not be added", + config_key, + length / 2, + ) return None return string @@ -133,16 +145,22 @@ class Monitor: def callback(bt_addr, _, packet, additional_info): """Handle new packets.""" self.process_packet( - additional_info['namespace'], additional_info['instance'], - packet.temperature) + additional_info["namespace"], + additional_info["instance"], + packet.temperature, + ) from beacontools import ( # pylint: disable=import-error - BeaconScanner, EddystoneFilter, EddystoneTLMFrame) - device_filters = [EddystoneFilter(d.namespace, d.instance) - for d in devices] + BeaconScanner, + EddystoneFilter, + EddystoneTLMFrame, + ) + + device_filters = [EddystoneFilter(d.namespace, d.instance) for d in devices] self.scanner = BeaconScanner( - callback, bt_device_id, device_filters, EddystoneTLMFrame) + callback, bt_device_id, device_filters, EddystoneTLMFrame + ) self.scanning = False def start(self): @@ -151,13 +169,13 @@ class Monitor: self.scanner.start() self.scanning = True else: - _LOGGER.debug( - "start() called, but scanner is already running") + _LOGGER.debug("start() called, but scanner is already running") def process_packet(self, namespace, instance, temperature): """Assign temperature to device.""" - _LOGGER.debug("Received temperature for <%s,%s>: %d", - namespace, instance, temperature) + _LOGGER.debug( + "Received temperature for <%s,%s>: %d", namespace, instance, temperature + ) for dev in self.devices: if dev.namespace == namespace and dev.instance == instance: @@ -173,5 +191,4 @@ class Monitor: _LOGGER.debug("Stopped") self.scanning = False else: - _LOGGER.debug( - "stop() called but scanner was not running") + _LOGGER.debug("stop() called but scanner was not running") diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index 535ae65800f..f1d8f8046ef 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -3,23 +3,24 @@ import logging import voluptuous as vol -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) -from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Edimax Smart Plug' -DEFAULT_PASSWORD = '1234' -DEFAULT_USERNAME = 'admin' +DEFAULT_NAME = "Edimax Smart Plug" +DEFAULT_PASSWORD = "1234" +DEFAULT_USERNAME = "admin" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -66,11 +67,11 @@ class SmartPlugSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" - self.smartplug.state = 'ON' + self.smartplug.state = "ON" def turn_off(self, **kwargs): """Turn the switch off.""" - self.smartplug.state = 'OFF' + self.smartplug.state = "OFF" def update(self): """Update edimax switch.""" @@ -84,4 +85,4 @@ class SmartPlugSwitch(SwitchDevice): except (TypeError, ValueError): self._now_energy_day = None - self._state = self.smartplug.state == 'ON' + self._state = self.smartplug.state == "ON" diff --git a/homeassistant/components/edp_redy/__init__.py b/homeassistant/components/edp_redy/__init__.py index af012064194..8c079078176 100644 --- a/homeassistant/components/edp_redy/__init__.py +++ b/homeassistant/components/edp_redy/__init__.py @@ -4,8 +4,7 @@ import logging import voluptuous as vol -from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_START) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_START from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, discovery, dispatcher import homeassistant.helpers.config_validation as cv @@ -15,27 +14,34 @@ from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) -DOMAIN = 'edp_redy' -EDP_REDY = 'edp_redy' -DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) +DOMAIN = "edp_redy" +EDP_REDY = "edp_redy" +DATA_UPDATE_TOPIC = "{0}_data_update".format(DOMAIN) UPDATE_INTERVAL = 60 -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): """Set up the EDP re:dy component.""" from edp_redy import EdpRedySession - session = EdpRedySession(config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD], - aiohttp_client.async_get_clientsession(hass), - hass.loop) + session = EdpRedySession( + config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD], + aiohttp_client.async_get_clientsession(hass), + hass.loop, + ) hass.data[EDP_REDY] = session platform_loaded = False @@ -46,16 +52,18 @@ async def async_setup(hass, config): nonlocal platform_loaded # pylint: disable=used-before-assignment if not platform_loaded: - for component in ['sensor', 'switch']: - await discovery.async_load_platform(hass, component, - DOMAIN, {}, config) + for component in ["sensor", "switch"]: + await discovery.async_load_platform( + hass, component, DOMAIN, {}, config + ) platform_loaded = True dispatcher.async_dispatcher_send(hass, DATA_UPDATE_TOPIC) # schedule next update - async_track_point_in_time(hass, async_update_and_sched, - time + timedelta(seconds=UPDATE_INTERVAL)) + async_track_point_in_time( + hass, async_update_and_sched, time + timedelta(seconds=UPDATE_INTERVAL) + ) async def start_component(event): _LOGGER.debug("Starting updates") @@ -84,7 +92,8 @@ class EdpRedyDevice(Entity): async def async_added_to_hass(self): """Subscribe to the data updates topic.""" dispatcher.async_dispatcher_connect( - self.hass, DATA_UPDATE_TOPIC, self._data_updated) + self.hass, DATA_UPDATE_TOPIC, self._data_updated + ) @property def name(self): @@ -120,8 +129,7 @@ class EdpRedyDevice(Entity): """Parse data received from the server.""" if "OutOfOrder" in data: try: - self._is_available = not data['OutOfOrder'] + self._is_available = not data["OutOfOrder"] except ValueError: - _LOGGER.error( - "Could not parse OutOfOrder for %s", self._id) + _LOGGER.error("Could not parse OutOfOrder for %s", self._id) self._is_available = False diff --git a/homeassistant/components/edp_redy/sensor.py b/homeassistant/components/edp_redy/sensor.py index cf9766ede66..f8fffefb5da 100644 --- a/homeassistant/components/edp_redy/sensor.py +++ b/homeassistant/components/edp_redy/sensor.py @@ -9,11 +9,10 @@ from . import EDP_REDY, EdpRedyDevice _LOGGER = logging.getLogger(__name__) # Load power in watts (W) -ATTR_ACTIVE_POWER = 'active_power' +ATTR_ACTIVE_POWER = "active_power" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Perform the setup for re:dy devices.""" from edp_redy.session import ACTIVE_POWER_ID @@ -22,13 +21,14 @@ async def async_setup_platform( # Create sensors for modules for device_json in session.modules_dict.values(): - if 'HA_POWER_METER' not in device_json['Capabilities']: + if "HA_POWER_METER" not in device_json["Capabilities"]: continue devices.append(EdpRedyModuleSensor(session, device_json)) # Create a sensor for global active power - devices.append(EdpRedySensor(session, ACTIVE_POWER_ID, "Power Home", - 'mdi:flash', POWER_WATT)) + devices.append( + EdpRedySensor(session, ACTIVE_POWER_ID, "Power Home", "mdi:flash", POWER_WATT) + ) async_add_entities(devices, True) @@ -72,8 +72,9 @@ class EdpRedyModuleSensor(EdpRedyDevice, Entity): def __init__(self, session, device_json): """Initialize the sensor.""" - super().__init__(session, device_json['PKID'], - "Power {0}".format(device_json['Name'])) + super().__init__( + session, device_json["PKID"], "Power {0}".format(device_json["Name"]) + ) @property def state(self): @@ -83,7 +84,7 @@ class EdpRedyModuleSensor(EdpRedyDevice, Entity): @property def icon(self): """Return the icon to use in the frontend.""" - return 'mdi:flash' + return "mdi:flash" @property def unit_of_measurement(self): @@ -104,10 +105,10 @@ class EdpRedyModuleSensor(EdpRedyDevice, Entity): _LOGGER.debug("Sensor data: %s", str(data)) - for state_var in data['StateVars']: - if state_var['Name'] == 'ActivePower': + for state_var in data["StateVars"]: + if state_var["Name"] == "ActivePower": try: - self._state = float(state_var['Value']) * 1000 + self._state = float(state_var["Value"]) * 1000 except ValueError: _LOGGER.error("Could not parse power for %s", self._id) self._state = 0 diff --git a/homeassistant/components/edp_redy/switch.py b/homeassistant/components/edp_redy/switch.py index 3f6dfe6b82d..18078fab537 100644 --- a/homeassistant/components/edp_redy/switch.py +++ b/homeassistant/components/edp_redy/switch.py @@ -8,16 +8,15 @@ from . import EDP_REDY, EdpRedyDevice _LOGGER = logging.getLogger(__name__) # Load power in watts (W) -ATTR_ACTIVE_POWER = 'active_power' +ATTR_ACTIVE_POWER = "active_power" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Perform the setup for re:dy devices.""" session = hass.data[EDP_REDY] devices = [] for device_json in session.modules_dict.values(): - if 'HA_SWITCH' not in device_json['Capabilities']: + if "HA_SWITCH" not in device_json["Capabilities"]: continue devices.append(EdpRedySwitch(session, device_json)) @@ -29,14 +28,14 @@ class EdpRedySwitch(EdpRedyDevice, SwitchDevice): def __init__(self, session, device_json): """Initialize the switch.""" - super().__init__(session, device_json['PKID'], device_json['Name']) + super().__init__(session, device_json["PKID"], device_json["Name"]) self._active_power = None @property def icon(self): """Return the icon to use in the frontend.""" - return 'mdi:power-plug' + return "mdi:power-plug" @property def is_on(self): @@ -66,8 +65,7 @@ class EdpRedySwitch(EdpRedyDevice, SwitchDevice): self.async_schedule_update_ha_state() async def _async_send_state_cmd(self, state): - state_json = {'devModuleId': self._id, 'key': 'RelayState', - 'value': state} + state_json = {"devModuleId": self._id, "key": "RelayState", "value": state} return await self._session.async_set_state_var(state_json) async def async_update(self): @@ -82,12 +80,12 @@ class EdpRedySwitch(EdpRedyDevice, SwitchDevice): """Parse data received from the server.""" super()._parse_data(data) - for state_var in data['StateVars']: - if state_var['Name'] == 'RelayState': - self._state = state_var['Value'] == 'true' - elif state_var['Name'] == 'ActivePower': + for state_var in data["StateVars"]: + if state_var["Name"] == "RelayState": + self._state = state_var["Value"] == "true" + elif state_var["Name"] == "ActivePower": try: - self._active_power = float(state_var['Value']) * 1000 + self._active_power = float(state_var["Value"]) * 1000 except ValueError: _LOGGER.error("Could not parse power for %s", self._id) self._active_power = None diff --git a/homeassistant/components/ee_brightbox/device_tracker.py b/homeassistant/components/ee_brightbox/device_tracker.py index 6af5065ed2e..81dbf9eab1f 100644 --- a/homeassistant/components/ee_brightbox/device_tracker.py +++ b/homeassistant/components/ee_brightbox/device_tracker.py @@ -4,24 +4,29 @@ import logging import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_VERSION = 'version' +CONF_VERSION = "version" -CONF_DEFAULT_IP = '192.168.1.1' -CONF_DEFAULT_USERNAME = 'admin' +CONF_DEFAULT_IP = "192.168.1.1" +CONF_DEFAULT_USERNAME = "admin" CONF_DEFAULT_VERSION = 2 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_VERSION, default=CONF_DEFAULT_VERSION): cv.positive_int, - vol.Required(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, - vol.Required(CONF_USERNAME, default=CONF_DEFAULT_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_VERSION, default=CONF_DEFAULT_VERSION): cv.positive_int, + vol.Required(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, + vol.Required(CONF_USERNAME, default=CONF_DEFAULT_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } +) def get_scanner(hass, config): @@ -55,18 +60,18 @@ class EEBrightBoxScanner(DeviceScanner): from eebrightbox import EEBrightBox with EEBrightBox(self.config) as ee_brightbox: - self.devices = {d['mac']: d for d in ee_brightbox.get_devices()} + self.devices = {d["mac"]: d for d in ee_brightbox.get_devices()} - macs = [d['mac'] for d in self.devices.values() if d['activity_ip']] + macs = [d["mac"] for d in self.devices.values() if d["activity_ip"]] - _LOGGER.debug('Scan devices %s', macs) + _LOGGER.debug("Scan devices %s", macs) return macs def get_device_name(self, device): """Get the name of a device from hostname.""" if device in self.devices: - return self.devices[device]['hostname'] or None + return self.devices[device]["hostname"] or None return None @@ -81,20 +86,20 @@ class EEBrightBoxScanner(DeviceScanner): - last_active """ port_map = { - 'wl1': 'wifi5Ghz', - 'wl0': 'wifi2.4Ghz', - 'eth0': 'eth0', - 'eth1': 'eth1', - 'eth2': 'eth2', - 'eth3': 'eth3', + "wl1": "wifi5Ghz", + "wl0": "wifi2.4Ghz", + "eth0": "eth0", + "eth1": "eth1", + "eth2": "eth2", + "eth3": "eth3", } if device in self.devices: return { - 'ip': self.devices[device]['ip'], - 'mac': self.devices[device]['mac'], - 'port': port_map[self.devices[device]['port']], - 'last_active': self.devices[device]['time_last_active'], + "ip": self.devices[device]["ip"], + "mac": self.devices[device]["mac"], + "port": port_map[self.devices[device]["port"]], + "last_active": self.devices[device]["time_last_active"], } return {} diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index eb8912abe18..53c89097a59 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -5,51 +5,54 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_CURRENCY, POWER_WATT, - ENERGY_KILO_WATT_HOUR) +from homeassistant.const import CONF_CURRENCY, POWER_WATT, ENERGY_KILO_WATT_HOUR import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -_RESOURCE = 'https://engage.efergy.com/mobile_proxy/' +_RESOURCE = "https://engage.efergy.com/mobile_proxy/" -CONF_APPTOKEN = 'app_token' -CONF_UTC_OFFSET = 'utc_offset' -CONF_MONITORED_VARIABLES = 'monitored_variables' -CONF_SENSOR_TYPE = 'type' +CONF_APPTOKEN = "app_token" +CONF_UTC_OFFSET = "utc_offset" +CONF_MONITORED_VARIABLES = "monitored_variables" +CONF_SENSOR_TYPE = "type" -CONF_PERIOD = 'period' +CONF_PERIOD = "period" -CONF_INSTANT = 'instant_readings' -CONF_AMOUNT = 'amount' -CONF_BUDGET = 'budget' -CONF_COST = 'cost' -CONF_CURRENT_VALUES = 'current_values' +CONF_INSTANT = "instant_readings" +CONF_AMOUNT = "amount" +CONF_BUDGET = "budget" +CONF_COST = "cost" +CONF_CURRENT_VALUES = "current_values" -DEFAULT_PERIOD = 'year' -DEFAULT_UTC_OFFSET = '0' +DEFAULT_PERIOD = "year" +DEFAULT_UTC_OFFSET = "0" SENSOR_TYPES = { - CONF_INSTANT: ['Energy Usage', POWER_WATT], - CONF_AMOUNT: ['Energy Consumed', ENERGY_KILO_WATT_HOUR], - CONF_BUDGET: ['Energy Budget', None], - CONF_COST: ['Energy Cost', None], - CONF_CURRENT_VALUES: ['Per-Device Usage', POWER_WATT] + CONF_INSTANT: ["Energy Usage", POWER_WATT], + CONF_AMOUNT: ["Energy Consumed", ENERGY_KILO_WATT_HOUR], + CONF_BUDGET: ["Energy Budget", None], + CONF_COST: ["Energy Cost", None], + CONF_CURRENT_VALUES: ["Per-Device Usage", POWER_WATT], } TYPES_SCHEMA = vol.In(SENSOR_TYPES) -SENSORS_SCHEMA = vol.Schema({ - vol.Required(CONF_SENSOR_TYPE): TYPES_SCHEMA, - vol.Optional(CONF_CURRENCY, default=''): cv.string, - vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.string, -}) +SENSORS_SCHEMA = vol.Schema( + { + vol.Required(CONF_SENSOR_TYPE): TYPES_SCHEMA, + vol.Optional(CONF_CURRENCY, default=""): cv.string, + vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.string, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_APPTOKEN): cv.string, - vol.Optional(CONF_UTC_OFFSET, default=DEFAULT_UTC_OFFSET): cv.string, - vol.Required(CONF_MONITORED_VARIABLES): [SENSORS_SCHEMA] -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_APPTOKEN): cv.string, + vol.Optional(CONF_UTC_OFFSET, default=DEFAULT_UTC_OFFSET): cv.string, + vol.Required(CONF_MONITORED_VARIABLES): [SENSORS_SCHEMA], + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -60,17 +63,31 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] for variable in config[CONF_MONITORED_VARIABLES]: if variable[CONF_SENSOR_TYPE] == CONF_CURRENT_VALUES: - url_string = '{}getCurrentValuesSummary?token={}'.format( - _RESOURCE, app_token) + url_string = "{}getCurrentValuesSummary?token={}".format( + _RESOURCE, app_token + ) response = requests.get(url_string, timeout=10) for sensor in response.json(): - sid = sensor['sid'] - dev.append(EfergySensor( - variable[CONF_SENSOR_TYPE], app_token, utc_offset, - variable[CONF_PERIOD], variable[CONF_CURRENCY], sid)) - dev.append(EfergySensor( - variable[CONF_SENSOR_TYPE], app_token, utc_offset, - variable[CONF_PERIOD], variable[CONF_CURRENCY])) + sid = sensor["sid"] + dev.append( + EfergySensor( + variable[CONF_SENSOR_TYPE], + app_token, + utc_offset, + variable[CONF_PERIOD], + variable[CONF_CURRENCY], + sid, + ) + ) + dev.append( + EfergySensor( + variable[CONF_SENSOR_TYPE], + app_token, + utc_offset, + variable[CONF_PERIOD], + variable[CONF_CURRENCY], + ) + ) add_entities(dev, True) @@ -78,12 +95,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class EfergySensor(Entity): """Implementation of an Efergy sensor.""" - def __init__(self, sensor_type, app_token, utc_offset, period, - currency, sid=None): + def __init__(self, sensor_type, app_token, utc_offset, period, currency, sid=None): """Initialize the sensor.""" self.sid = sid if sid: - self._name = 'efergy_{}'.format(sid) + self._name = "efergy_{}".format(sid) else: self._name = SENSOR_TYPES[sensor_type][0] self.type = sensor_type @@ -92,9 +108,8 @@ class EfergySensor(Entity): self._state = None self.period = period self.currency = currency - if self.type == 'cost': - self._unit_of_measurement = '{}/{}'.format( - self.currency, self.period) + if self.type == "cost": + self._unit_of_measurement = "{}/{}".format(self.currency, self.period) else: self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] @@ -116,33 +131,34 @@ class EfergySensor(Entity): def update(self): """Get the Efergy monitor data from the web service.""" try: - if self.type == 'instant_readings': - url_string = '{}getInstant?token={}'.format( - _RESOURCE, self.app_token) + if self.type == "instant_readings": + url_string = "{}getInstant?token={}".format(_RESOURCE, self.app_token) response = requests.get(url_string, timeout=10) - self._state = response.json()['reading'] - elif self.type == 'amount': - url_string = '{}getEnergy?token={}&offset={}&period={}'.format( - _RESOURCE, self.app_token, self.utc_offset, self.period) + self._state = response.json()["reading"] + elif self.type == "amount": + url_string = "{}getEnergy?token={}&offset={}&period={}".format( + _RESOURCE, self.app_token, self.utc_offset, self.period + ) response = requests.get(url_string, timeout=10) - self._state = response.json()['sum'] - elif self.type == 'budget': - url_string = '{}getBudget?token={}'.format( - _RESOURCE, self.app_token) + self._state = response.json()["sum"] + elif self.type == "budget": + url_string = "{}getBudget?token={}".format(_RESOURCE, self.app_token) response = requests.get(url_string, timeout=10) - self._state = response.json()['status'] - elif self.type == 'cost': - url_string = '{}getCost?token={}&offset={}&period={}'.format( - _RESOURCE, self.app_token, self.utc_offset, self.period) + self._state = response.json()["status"] + elif self.type == "cost": + url_string = "{}getCost?token={}&offset={}&period={}".format( + _RESOURCE, self.app_token, self.utc_offset, self.period + ) response = requests.get(url_string, timeout=10) - self._state = response.json()['sum'] - elif self.type == 'current_values': - url_string = '{}getCurrentValuesSummary?token={}'.format( - _RESOURCE, self.app_token) + self._state = response.json()["sum"] + elif self.type == "current_values": + url_string = "{}getCurrentValuesSummary?token={}".format( + _RESOURCE, self.app_token + ) response = requests.get(url_string, timeout=10) for sensor in response.json(): - if self.sid == sensor['sid']: - measurement = next(iter(sensor['data'][0].values())) + if self.sid == sensor["sid"]: + measurement = next(iter(sensor["data"][0].values())) self._state = measurement else: self._state = None diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py index cf0bb20f0fc..e17ea8f065d 100644 --- a/homeassistant/components/egardia/__init__.py +++ b/homeassistant/components/egardia/__init__.py @@ -5,67 +5,82 @@ import requests import voluptuous as vol from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP) + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_DISCOVER_DEVICES = 'egardia_sensor' +ATTR_DISCOVER_DEVICES = "egardia_sensor" -CONF_REPORT_SERVER_CODES = 'report_server_codes' -CONF_REPORT_SERVER_ENABLED = 'report_server_enabled' -CONF_REPORT_SERVER_PORT = 'report_server_port' -CONF_VERSION = 'version' +CONF_REPORT_SERVER_CODES = "report_server_codes" +CONF_REPORT_SERVER_ENABLED = "report_server_enabled" +CONF_REPORT_SERVER_PORT = "report_server_port" +CONF_VERSION = "version" -DEFAULT_NAME = 'Egardia' +DEFAULT_NAME = "Egardia" DEFAULT_PORT = 80 DEFAULT_REPORT_SERVER_ENABLED = False DEFAULT_REPORT_SERVER_PORT = 52010 -DEFAULT_VERSION = 'GATE-01' -DOMAIN = 'egardia' +DEFAULT_VERSION = "GATE-01" +DOMAIN = "egardia" -EGARDIA_DEVICE = 'egardiadevice' -EGARDIA_NAME = 'egardianame' -EGARDIA_REPORT_SERVER_CODES = 'egardia_rs_codes' -EGARDIA_REPORT_SERVER_ENABLED = 'egardia_rs_enabled' -EGARDIA_SERVER = 'egardia_server' +EGARDIA_DEVICE = "egardiadevice" +EGARDIA_NAME = "egardianame" +EGARDIA_REPORT_SERVER_CODES = "egardia_rs_codes" +EGARDIA_REPORT_SERVER_ENABLED = "egardia_rs_enabled" +EGARDIA_SERVER = "egardia_server" -NOTIFICATION_ID = 'egardia_notification' -NOTIFICATION_TITLE = 'Egardia' +NOTIFICATION_ID = "egardia_notification" +NOTIFICATION_TITLE = "Egardia" -REPORT_SERVER_CODES_IGNORE = 'ignore' +REPORT_SERVER_CODES_IGNORE = "ignore" -SERVER_CODE_SCHEMA = vol.Schema({ - vol.Optional('arm'): vol.All(cv.ensure_list_csv, [cv.string]), - vol.Optional('disarm'): vol.All(cv.ensure_list_csv, [cv.string]), - vol.Optional('armhome'): vol.All(cv.ensure_list_csv, [cv.string]), - vol.Optional('triggered'): vol.All(cv.ensure_list_csv, [cv.string]), - vol.Optional('ignore'): vol.All(cv.ensure_list_csv, [cv.string]), -}) +SERVER_CODE_SCHEMA = vol.Schema( + { + vol.Optional("arm"): vol.All(cv.ensure_list_csv, [cv.string]), + vol.Optional("disarm"): vol.All(cv.ensure_list_csv, [cv.string]), + vol.Optional("armhome"): vol.All(cv.ensure_list_csv, [cv.string]), + vol.Optional("triggered"): vol.All(cv.ensure_list_csv, [cv.string]), + vol.Optional("ignore"): vol.All(cv.ensure_list_csv, [cv.string]), + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_REPORT_SERVER_CODES, default={}): SERVER_CODE_SCHEMA, - vol.Optional(CONF_REPORT_SERVER_ENABLED, - default=DEFAULT_REPORT_SERVER_ENABLED): cv.boolean, - vol.Optional(CONF_REPORT_SERVER_PORT, - default=DEFAULT_REPORT_SERVER_PORT): cv.port, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_REPORT_SERVER_CODES, default={}): SERVER_CODE_SCHEMA, + vol.Optional( + CONF_REPORT_SERVER_ENABLED, default=DEFAULT_REPORT_SERVER_ENABLED + ): cv.boolean, + vol.Optional( + CONF_REPORT_SERVER_PORT, default=DEFAULT_REPORT_SERVER_PORT + ): cv.port, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): """Set up the Egardia platform.""" from pythonegardia import egardiadevice from pythonegardia import egardiaserver + conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) @@ -76,10 +91,13 @@ def setup(hass, config): rs_port = conf.get(CONF_REPORT_SERVER_PORT) try: device = hass.data[EGARDIA_DEVICE] = egardiadevice.EgardiaDevice( - host, port, username, password, '', version) + host, port, username, password, "", version + ) except requests.exceptions.RequestException: - _LOGGER.error("An error occurred accessing your Egardia device. " - "Please check configuration") + _LOGGER.error( + "An error occurred accessing your Egardia device. " + "Please check configuration" + ) return False except egardiadevice.UnauthorizedError: _LOGGER.error("Unable to authorize. Wrong password or username") @@ -89,11 +107,12 @@ def setup(hass, config): _LOGGER.debug("Setting up EgardiaServer") try: if EGARDIA_SERVER not in hass.data: - server = egardiaserver.EgardiaServer('', rs_port) + server = egardiaserver.EgardiaServer("", rs_port) bound = server.bind() if not bound: - raise IOError("Binding error occurred while " + - "starting EgardiaServer.") + raise IOError( + "Binding error occurred while " + "starting EgardiaServer." + ) hass.data[EGARDIA_SERVER] = server server.start() @@ -105,16 +124,17 @@ def setup(hass, config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event) except IOError: - _LOGGER.error( - "Binding error occurred while starting EgardiaServer") + _LOGGER.error("Binding error occurred while starting EgardiaServer") return False - discovery.load_platform(hass, 'alarm_control_panel', DOMAIN, - discovered=conf, hass_config=config) + discovery.load_platform( + hass, "alarm_control_panel", DOMAIN, discovered=conf, hass_config=config + ) # Get the sensors from the device and add those sensors = device.getsensors() - discovery.load_platform(hass, 'binary_sensor', DOMAIN, - {ATTR_DISCOVER_DEVICES: sensors}, config) + discovery.load_platform( + hass, "binary_sensor", DOMAIN, {ATTR_DISCOVER_DEVICES: sensors}, config + ) return True diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index ab48181f9ed..22a458ae9aa 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -5,24 +5,32 @@ import requests import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) from . import ( - CONF_REPORT_SERVER_CODES, CONF_REPORT_SERVER_ENABLED, - CONF_REPORT_SERVER_PORT, EGARDIA_DEVICE, EGARDIA_SERVER, - REPORT_SERVER_CODES_IGNORE) + CONF_REPORT_SERVER_CODES, + CONF_REPORT_SERVER_ENABLED, + CONF_REPORT_SERVER_PORT, + EGARDIA_DEVICE, + EGARDIA_SERVER, + REPORT_SERVER_CODES_IGNORE, +) _LOGGER = logging.getLogger(__name__) STATES = { - 'ARM': STATE_ALARM_ARMED_AWAY, - 'DAY HOME': STATE_ALARM_ARMED_HOME, - 'DISARM': STATE_ALARM_DISARMED, - 'ARMHOME': STATE_ALARM_ARMED_HOME, - 'HOME': STATE_ALARM_ARMED_HOME, - 'NIGHT HOME': STATE_ALARM_ARMED_NIGHT, - 'TRIGGERED': STATE_ALARM_TRIGGERED + "ARM": STATE_ALARM_ARMED_AWAY, + "DAY HOME": STATE_ALARM_ARMED_HOME, + "DISARM": STATE_ALARM_DISARMED, + "ARMHOME": STATE_ALARM_ARMED_HOME, + "HOME": STATE_ALARM_ARMED_HOME, + "NIGHT HOME": STATE_ALARM_ARMED_NIGHT, + "TRIGGERED": STATE_ALARM_TRIGGERED, } @@ -31,11 +39,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return device = EgardiaAlarm( - discovery_info['name'], + discovery_info["name"], hass.data[EGARDIA_DEVICE], discovery_info[CONF_REPORT_SERVER_ENABLED], discovery_info.get(CONF_REPORT_SERVER_CODES), - discovery_info[CONF_REPORT_SERVER_PORT]) + discovery_info[CONF_REPORT_SERVER_PORT], + ) add_entities([device], True) @@ -43,8 +52,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class EgardiaAlarm(alarm.AlarmControlPanel): """Representation of a Egardia alarm.""" - def __init__(self, name, egardiasystem, - rs_enabled=False, rs_codes=None, rs_port=52010): + def __init__( + self, name, egardiasystem, rs_enabled=False, rs_codes=None, rs_port=52010 + ): """Initialize the Egardia alarm.""" self._name = name self._egardiasystem = egardiasystem @@ -57,8 +67,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel): """Add Egardiaserver callback if enabled.""" if self._rs_enabled: _LOGGER.debug("Registering callback to Egardiaserver") - self.hass.data[EGARDIA_SERVER].register_callback( - self.handle_status_event) + self.hass.data[EGARDIA_SERVER].register_callback(self.handle_status_event) @property def name(self): @@ -79,7 +88,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel): def handle_status_event(self, event): """Handle the Egardia system status event.""" - statuscode = event.get('status') + statuscode = event.get("status") if statuscode is not None: status = self.lookupstatusfromcode(statuscode) self.parsestatus(status) @@ -87,10 +96,15 @@ class EgardiaAlarm(alarm.AlarmControlPanel): def lookupstatusfromcode(self, statuscode): """Look at the rs_codes and returns the status from the code.""" - status = next(( - status_group.upper() for status_group, codes - in self._rs_codes.items() for code in codes - if statuscode == code), 'UNKNOWN') + status = next( + ( + status_group.upper() + for status_group, codes in self._rs_codes.items() + for code in codes + if statuscode == code + ), + "UNKNOWN", + ) return status def parsestatus(self, status): @@ -115,21 +129,29 @@ class EgardiaAlarm(alarm.AlarmControlPanel): try: self._egardiasystem.alarm_disarm() except requests.exceptions.RequestException as err: - _LOGGER.error("Egardia device exception occurred when " - "sending disarm command: %s", err) + _LOGGER.error( + "Egardia device exception occurred when " "sending disarm command: %s", + err, + ) def alarm_arm_home(self, code=None): """Send arm home command.""" try: self._egardiasystem.alarm_arm_home() except requests.exceptions.RequestException as err: - _LOGGER.error("Egardia device exception occurred when " - "sending arm home command: %s", err) + _LOGGER.error( + "Egardia device exception occurred when " + "sending arm home command: %s", + err, + ) def alarm_arm_away(self, code=None): """Send arm away command.""" try: self._egardiasystem.alarm_arm_away() except requests.exceptions.RequestException as err: - _LOGGER.error("Egardia device exception occurred when " - "sending arm away command: %s", err) + _LOGGER.error( + "Egardia device exception occurred when " + "sending arm away command: %s", + err, + ) diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py index 965b2dd1d55..157e4f1fe8c 100644 --- a/homeassistant/components/egardia/binary_sensor.py +++ b/homeassistant/components/egardia/binary_sensor.py @@ -9,17 +9,15 @@ from . import ATTR_DISCOVER_DEVICES, EGARDIA_DEVICE _LOGGER = logging.getLogger(__name__) EGARDIA_TYPE_TO_DEVICE_CLASS = { - 'IR Sensor': 'motion', - 'Door Contact': 'opening', - 'IR': 'motion', + "IR Sensor": "motion", + "Door Contact": "opening", + "IR": "motion", } -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Initialize the platform.""" - if (discovery_info is None or - discovery_info[ATTR_DISCOVER_DEVICES] is None): + if discovery_info is None or discovery_info[ATTR_DISCOVER_DEVICES] is None: return disc_info = discovery_info[ATTR_DISCOVER_DEVICES] @@ -27,14 +25,17 @@ async def async_setup_platform(hass, config, async_add_entities, async_add_entities( ( EgardiaBinarySensor( - sensor_id=disc_info[sensor]['id'], - name=disc_info[sensor]['name'], + sensor_id=disc_info[sensor]["id"], + name=disc_info[sensor]["name"], egardia_system=hass.data[EGARDIA_DEVICE], device_class=EGARDIA_TYPE_TO_DEVICE_CLASS.get( - disc_info[sensor]['type'], None) + disc_info[sensor]["type"], None + ), ) for sensor in disc_info - ), True) + ), + True, + ) class EgardiaBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index d74218796a3..2479ea5440f 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -6,76 +6,86 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_BINARY_SENSORS, - ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP) + CONF_USERNAME, + CONF_PASSWORD, + CONF_SENSORS, + CONF_BINARY_SENSORS, + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect) + async_dispatcher_send, + async_dispatcher_connect, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) -CONF_PARTNER = 'partner' +CONF_PARTNER = "partner" -DATA_EIGHT = 'eight_sleep' +DATA_EIGHT = "eight_sleep" DEFAULT_PARTNER = False -DOMAIN = 'eight_sleep' +DOMAIN = "eight_sleep" -HEAT_ENTITY = 'heat' -USER_ENTITY = 'user' +HEAT_ENTITY = "heat" +USER_ENTITY = "user" HEAT_SCAN_INTERVAL = timedelta(seconds=60) USER_SCAN_INTERVAL = timedelta(seconds=300) -SIGNAL_UPDATE_HEAT = 'eight_heat_update' -SIGNAL_UPDATE_USER = 'eight_user_update' +SIGNAL_UPDATE_HEAT = "eight_heat_update" +SIGNAL_UPDATE_USER = "eight_user_update" NAME_MAP = { - 'left_current_sleep': 'Left Sleep Session', - 'left_last_sleep': 'Left Previous Sleep Session', - 'left_bed_state': 'Left Bed State', - 'left_presence': 'Left Bed Presence', - 'left_bed_temp': 'Left Bed Temperature', - 'left_sleep_stage': 'Left Sleep Stage', - 'right_current_sleep': 'Right Sleep Session', - 'right_last_sleep': 'Right Previous Sleep Session', - 'right_bed_state': 'Right Bed State', - 'right_presence': 'Right Bed Presence', - 'right_bed_temp': 'Right Bed Temperature', - 'right_sleep_stage': 'Right Sleep Stage', - 'room_temp': 'Room Temperature', + "left_current_sleep": "Left Sleep Session", + "left_last_sleep": "Left Previous Sleep Session", + "left_bed_state": "Left Bed State", + "left_presence": "Left Bed Presence", + "left_bed_temp": "Left Bed Temperature", + "left_sleep_stage": "Left Sleep Stage", + "right_current_sleep": "Right Sleep Session", + "right_last_sleep": "Right Previous Sleep Session", + "right_bed_state": "Right Bed State", + "right_presence": "Right Bed Presence", + "right_bed_temp": "Right Bed Temperature", + "right_sleep_stage": "Right Sleep Stage", + "room_temp": "Room Temperature", } -SENSORS = ['current_sleep', - 'last_sleep', - 'bed_state', - 'bed_temp', - 'sleep_stage'] +SENSORS = ["current_sleep", "last_sleep", "bed_state", "bed_temp", "sleep_stage"] -SERVICE_HEAT_SET = 'heat_set' +SERVICE_HEAT_SET = "heat_set" -ATTR_TARGET_HEAT = 'target' -ATTR_HEAT_DURATION = 'duration' +ATTR_TARGET_HEAT = "target" +ATTR_HEAT_DURATION = "duration" VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)) VALID_DURATION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=28800)) -SERVICE_EIGHT_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, - ATTR_TARGET_HEAT: VALID_TARGET_HEAT, - ATTR_HEAT_DURATION: VALID_DURATION, - }) +SERVICE_EIGHT_SCHEMA = vol.Schema( + { + ATTR_ENTITY_ID: cv.entity_ids, + ATTR_TARGET_HEAT: VALID_TARGET_HEAT, + ATTR_HEAT_DURATION: VALID_DURATION, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PARTNER, default=DEFAULT_PARTNER): cv.boolean, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PARTNER, default=DEFAULT_PARTNER): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -88,7 +98,7 @@ async def async_setup(hass, config): partner = conf.get(CONF_PARTNER) if hass.config.time_zone is None: - _LOGGER.error('Timezone is not set in Home Assistant.') + _LOGGER.error("Timezone is not set in Home Assistant.") return False timezone = hass.config.time_zone @@ -109,7 +119,8 @@ async def async_setup(hass, config): async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT) async_track_point_in_utc_time( - hass, async_update_heat_data, utcnow() + HEAT_SCAN_INTERVAL) + hass, async_update_heat_data, utcnow() + HEAT_SCAN_INTERVAL + ) async def async_update_user_data(now): """Update user data from eight in USER_SCAN_INTERVAL.""" @@ -117,7 +128,8 @@ async def async_setup(hass, config): async_dispatcher_send(hass, SIGNAL_UPDATE_USER) async_track_point_in_utc_time( - hass, async_update_user_data, utcnow() + USER_SCAN_INTERVAL) + hass, async_update_user_data, utcnow() + USER_SCAN_INTERVAL + ) await async_update_heat_data(None) await async_update_user_data(None) @@ -129,22 +141,24 @@ async def async_setup(hass, config): for user in eight.users: obj = eight.users[user] for sensor in SENSORS: - sensors.append('{}_{}'.format(obj.side, sensor)) - binary_sensors.append('{}_presence'.format(obj.side)) - sensors.append('room_temp') + sensors.append("{}_{}".format(obj.side, sensor)) + binary_sensors.append("{}_presence".format(obj.side)) + sensors.append("room_temp") else: # No users, cannot continue return False - hass.async_create_task(discovery.async_load_platform( - hass, 'sensor', DOMAIN, { - CONF_SENSORS: sensors, - }, config)) + hass.async_create_task( + discovery.async_load_platform( + hass, "sensor", DOMAIN, {CONF_SENSORS: sensors}, config + ) + ) - hass.async_create_task(discovery.async_load_platform( - hass, 'binary_sensor', DOMAIN, { - CONF_BINARY_SENSORS: binary_sensors, - }, config)) + hass.async_create_task( + discovery.async_load_platform( + hass, "binary_sensor", DOMAIN, {CONF_BINARY_SENSORS: binary_sensors}, config + ) + ) async def async_service_handler(service): """Handle eight sleep service calls.""" @@ -155,7 +169,7 @@ async def async_setup(hass, config): duration = params.pop(ATTR_HEAT_DURATION, 0) for sens in sensor: - side = sens.split('_')[1] + side = sens.split("_")[1] userid = eight.fetch_userid(side) usrobj = eight.users[userid] await usrobj.set_heating_level(target, duration) @@ -164,8 +178,8 @@ async def async_setup(hass, config): # Register services hass.services.async_register( - DOMAIN, SERVICE_HEAT_SET, async_service_handler, - schema=SERVICE_EIGHT_SCHEMA) + DOMAIN, SERVICE_HEAT_SET, async_service_handler, schema=SERVICE_EIGHT_SCHEMA + ) async def stop_eight(event): """Handle stopping eight api session.""" @@ -185,13 +199,13 @@ class EightSleepUserEntity(Entity): async def async_added_to_hass(self): """Register update dispatcher.""" + @callback def async_eight_user_update(): """Update callback.""" self.async_schedule_update_ha_state(True) - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_USER, async_eight_user_update) + async_dispatcher_connect(self.hass, SIGNAL_UPDATE_USER, async_eight_user_update) @property def should_poll(self): @@ -208,13 +222,13 @@ class EightSleepHeatEntity(Entity): async def async_added_to_hass(self): """Register update dispatcher.""" + @callback def async_eight_heat_update(): """Update callback.""" self.async_schedule_update_ha_state(True) - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_HEAT, async_eight_heat_update) + async_dispatcher_connect(self.hass, SIGNAL_UPDATE_HEAT, async_eight_heat_update) @property def should_poll(self): diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index b3842106723..7d7ebecafee 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -8,13 +8,12 @@ from . import CONF_BINARY_SENSORS, DATA_EIGHT, NAME_MAP, EightSleepHeatEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the eight sleep binary sensor.""" if discovery_info is None: return - name = 'Eight' + name = "Eight" sensors = discovery_info[CONF_BINARY_SENSORS] eight = hass.data[DATA_EIGHT] @@ -35,15 +34,19 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice): self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = '{} {}'.format(name, self._mapped_name) + self._name = "{} {}".format(name, self._mapped_name) self._state = None - self._side = self._sensor.split('_')[0] + self._side = self._sensor.split("_")[0] self._userid = self._eight.fetch_userid(self._side) self._usrobj = self._eight.users[self._userid] - _LOGGER.debug("Presence Sensor: %s, Side: %s, User: %s", - self._sensor, self._side, self._userid) + _LOGGER.debug( + "Presence Sensor: %s, Side: %s, User: %s", + self._sensor, + self._side, + self._userid, + ) @property def name(self): diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index b7b0f588155..afc06986ea6 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -2,53 +2,56 @@ import logging from . import ( - CONF_SENSORS, DATA_EIGHT, NAME_MAP, EightSleepHeatEntity, - EightSleepUserEntity) + CONF_SENSORS, + DATA_EIGHT, + NAME_MAP, + EightSleepHeatEntity, + EightSleepUserEntity, +) -ATTR_ROOM_TEMP = 'Room Temperature' -ATTR_AVG_ROOM_TEMP = 'Average Room Temperature' -ATTR_BED_TEMP = 'Bed Temperature' -ATTR_AVG_BED_TEMP = 'Average Bed Temperature' -ATTR_RESP_RATE = 'Respiratory Rate' -ATTR_AVG_RESP_RATE = 'Average Respiratory Rate' -ATTR_HEART_RATE = 'Heart Rate' -ATTR_AVG_HEART_RATE = 'Average Heart Rate' -ATTR_SLEEP_DUR = 'Time Slept' -ATTR_LIGHT_PERC = 'Light Sleep %' -ATTR_DEEP_PERC = 'Deep Sleep %' -ATTR_REM_PERC = 'REM Sleep %' -ATTR_TNT = 'Tosses & Turns' -ATTR_SLEEP_STAGE = 'Sleep Stage' -ATTR_TARGET_HEAT = 'Target Heating Level' -ATTR_ACTIVE_HEAT = 'Heating Active' -ATTR_DURATION_HEAT = 'Heating Time Remaining' -ATTR_PROCESSING = 'Processing' -ATTR_SESSION_START = 'Session Start' +ATTR_ROOM_TEMP = "Room Temperature" +ATTR_AVG_ROOM_TEMP = "Average Room Temperature" +ATTR_BED_TEMP = "Bed Temperature" +ATTR_AVG_BED_TEMP = "Average Bed Temperature" +ATTR_RESP_RATE = "Respiratory Rate" +ATTR_AVG_RESP_RATE = "Average Respiratory Rate" +ATTR_HEART_RATE = "Heart Rate" +ATTR_AVG_HEART_RATE = "Average Heart Rate" +ATTR_SLEEP_DUR = "Time Slept" +ATTR_LIGHT_PERC = "Light Sleep %" +ATTR_DEEP_PERC = "Deep Sleep %" +ATTR_REM_PERC = "REM Sleep %" +ATTR_TNT = "Tosses & Turns" +ATTR_SLEEP_STAGE = "Sleep Stage" +ATTR_TARGET_HEAT = "Target Heating Level" +ATTR_ACTIVE_HEAT = "Heating Active" +ATTR_DURATION_HEAT = "Heating Time Remaining" +ATTR_PROCESSING = "Processing" +ATTR_SESSION_START = "Session Start" _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the eight sleep sensors.""" if discovery_info is None: return - name = 'Eight' + name = "Eight" sensors = discovery_info[CONF_SENSORS] eight = hass.data[DATA_EIGHT] if hass.config.units.is_metric: - units = 'si' + units = "si" else: - units = 'us' + units = "us" all_sensors = [] for sensor in sensors: - if 'bed_state' in sensor: + if "bed_state" in sensor: all_sensors.append(EightHeatSensor(name, eight, sensor)) - elif 'room_temp' in sensor: + elif "room_temp" in sensor: all_sensors.append(EightRoomSensor(name, eight, sensor, units)) else: all_sensors.append(EightUserSensor(name, eight, sensor, units)) @@ -65,15 +68,19 @@ class EightHeatSensor(EightSleepHeatEntity): self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = '{} {}'.format(name, self._mapped_name) + self._name = "{} {}".format(name, self._mapped_name) self._state = None - self._side = self._sensor.split('_')[0] + self._side = self._sensor.split("_")[0] self._userid = self._eight.fetch_userid(self._side) self._usrobj = self._eight.users[self._userid] - _LOGGER.debug("Heat Sensor: %s, Side: %s, User: %s", - self._sensor, self._side, self._userid) + _LOGGER.debug( + "Heat Sensor: %s, Side: %s, User: %s", + self._sensor, + self._side, + self._userid, + ) @property def name(self): @@ -88,7 +95,7 @@ class EightHeatSensor(EightSleepHeatEntity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return '%' + return "%" async def async_update(self): """Retrieve latest state.""" @@ -113,19 +120,23 @@ class EightUserSensor(EightSleepUserEntity): super().__init__(eight) self._sensor = sensor - self._sensor_root = self._sensor.split('_', 1)[1] + self._sensor_root = self._sensor.split("_", 1)[1] self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = '{} {}'.format(name, self._mapped_name) + self._name = "{} {}".format(name, self._mapped_name) self._state = None self._attr = None self._units = units - self._side = self._sensor.split('_', 1)[0] + self._side = self._sensor.split("_", 1)[0] self._userid = self._eight.fetch_userid(self._side) self._usrobj = self._eight.users[self._userid] - _LOGGER.debug("User Sensor: %s, Side: %s, User: %s", - self._sensor, self._side, self._userid) + _LOGGER.debug( + "User Sensor: %s, Side: %s, User: %s", + self._sensor, + self._side, + self._userid, + ) @property def name(self): @@ -140,40 +151,40 @@ class EightUserSensor(EightSleepUserEntity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - if 'current_sleep' in self._sensor or 'last_sleep' in self._sensor: - return 'Score' - if 'bed_temp' in self._sensor: - if self._units == 'si': - return '°C' - return '°F' + if "current_sleep" in self._sensor or "last_sleep" in self._sensor: + return "Score" + if "bed_temp" in self._sensor: + if self._units == "si": + return "°C" + return "°F" return None @property def icon(self): """Icon to use in the frontend, if any.""" - if 'bed_temp' in self._sensor: - return 'mdi:thermometer' + if "bed_temp" in self._sensor: + return "mdi:thermometer" async def async_update(self): """Retrieve latest state.""" _LOGGER.debug("Updating User sensor: %s", self._sensor) - if 'current' in self._sensor: + if "current" in self._sensor: self._state = self._usrobj.current_sleep_score self._attr = self._usrobj.current_values - elif 'last' in self._sensor: + elif "last" in self._sensor: self._state = self._usrobj.last_sleep_score self._attr = self._usrobj.last_values - elif 'bed_temp' in self._sensor: - temp = self._usrobj.current_values['bed_temp'] + elif "bed_temp" in self._sensor: + temp = self._usrobj.current_values["bed_temp"] try: - if self._units == 'si': + if self._units == "si": self._state = round(temp, 2) else: - self._state = round((temp*1.8)+32, 2) + self._state = round((temp * 1.8) + 32, 2) except TypeError: self._state = None - elif 'sleep_stage' in self._sensor: - self._state = self._usrobj.current_values['stage'] + elif "sleep_stage" in self._sensor: + self._state = self._usrobj.current_values["stage"] @property def device_state_attributes(self): @@ -182,56 +193,59 @@ class EightUserSensor(EightSleepUserEntity): # Skip attributes if sensor type doesn't support return None - state_attr = {ATTR_SESSION_START: self._attr['date']} - state_attr[ATTR_TNT] = self._attr['tnt'] - state_attr[ATTR_PROCESSING] = self._attr['processing'] + state_attr = {ATTR_SESSION_START: self._attr["date"]} + state_attr[ATTR_TNT] = self._attr["tnt"] + state_attr[ATTR_PROCESSING] = self._attr["processing"] - sleep_time = sum(self._attr['breakdown'].values()) - \ - self._attr['breakdown']['awake'] + sleep_time = ( + sum(self._attr["breakdown"].values()) - self._attr["breakdown"]["awake"] + ) state_attr[ATTR_SLEEP_DUR] = sleep_time try: - state_attr[ATTR_LIGHT_PERC] = round(( - self._attr['breakdown']['light'] / sleep_time) * 100, 2) + state_attr[ATTR_LIGHT_PERC] = round( + (self._attr["breakdown"]["light"] / sleep_time) * 100, 2 + ) except ZeroDivisionError: state_attr[ATTR_LIGHT_PERC] = 0 try: - state_attr[ATTR_DEEP_PERC] = round(( - self._attr['breakdown']['deep'] / sleep_time) * 100, 2) + state_attr[ATTR_DEEP_PERC] = round( + (self._attr["breakdown"]["deep"] / sleep_time) * 100, 2 + ) except ZeroDivisionError: state_attr[ATTR_DEEP_PERC] = 0 try: - state_attr[ATTR_REM_PERC] = round(( - self._attr['breakdown']['rem'] / sleep_time) * 100, 2) + state_attr[ATTR_REM_PERC] = round( + (self._attr["breakdown"]["rem"] / sleep_time) * 100, 2 + ) except ZeroDivisionError: state_attr[ATTR_REM_PERC] = 0 try: - if self._units == 'si': - room_temp = round(self._attr['room_temp'], 2) + if self._units == "si": + room_temp = round(self._attr["room_temp"], 2) else: - room_temp = round((self._attr['room_temp']*1.8)+32, 2) + room_temp = round((self._attr["room_temp"] * 1.8) + 32, 2) except TypeError: room_temp = None try: - if self._units == 'si': - bed_temp = round(self._attr['bed_temp'], 2) + if self._units == "si": + bed_temp = round(self._attr["bed_temp"], 2) else: - bed_temp = round((self._attr['bed_temp']*1.8)+32, 2) + bed_temp = round((self._attr["bed_temp"] * 1.8) + 32, 2) except TypeError: bed_temp = None - if 'current' in self._sensor_root: - state_attr[ATTR_RESP_RATE] = round(self._attr['resp_rate'], 2) - state_attr[ATTR_HEART_RATE] = round(self._attr['heart_rate'], 2) - state_attr[ATTR_SLEEP_STAGE] = self._attr['stage'] + if "current" in self._sensor_root: + state_attr[ATTR_RESP_RATE] = round(self._attr["resp_rate"], 2) + state_attr[ATTR_HEART_RATE] = round(self._attr["heart_rate"], 2) + state_attr[ATTR_SLEEP_STAGE] = self._attr["stage"] state_attr[ATTR_ROOM_TEMP] = room_temp state_attr[ATTR_BED_TEMP] = bed_temp - elif 'last' in self._sensor_root: - state_attr[ATTR_AVG_RESP_RATE] = round(self._attr['resp_rate'], 2) - state_attr[ATTR_AVG_HEART_RATE] = round( - self._attr['heart_rate'], 2) + elif "last" in self._sensor_root: + state_attr[ATTR_AVG_RESP_RATE] = round(self._attr["resp_rate"], 2) + state_attr[ATTR_AVG_HEART_RATE] = round(self._attr["heart_rate"], 2) state_attr[ATTR_AVG_ROOM_TEMP] = room_temp state_attr[ATTR_AVG_BED_TEMP] = bed_temp @@ -247,7 +261,7 @@ class EightRoomSensor(EightSleepUserEntity): self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = '{} {}'.format(name, self._mapped_name) + self._name = "{} {}".format(name, self._mapped_name) self._state = None self._attr = None self._units = units @@ -267,21 +281,21 @@ class EightRoomSensor(EightSleepUserEntity): _LOGGER.debug("Updating Room sensor: %s", self._sensor) temp = self._eight.room_temperature() try: - if self._units == 'si': + if self._units == "si": self._state = round(temp, 2) else: - self._state = round((temp*1.8)+32, 2) + self._state = round((temp * 1.8) + 32, 2) except TypeError: self._state = None @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - if self._units == 'si': - return '°C' - return '°F' + if self._units == "si": + return "°C" + return "°F" @property def icon(self): """Icon to use in the frontend, if any.""" - return 'mdi:thermometer' + return "mdi:thermometer" diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index 03d6ad89591..1f21263a4d6 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -6,32 +6,33 @@ import asyncio import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, POWER_WATT) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, POWER_WATT from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) -CONF_CHANNEL_ID = 'channel_id' +CONF_CHANNEL_ID = "channel_id" -DEFAULT_NAME = 'ELIQ Online' +DEFAULT_NAME = "ELIQ Online" -ICON = 'mdi:gauge' +ICON = "mdi:gauge" SCAN_INTERVAL = timedelta(seconds=60) UNIT_OF_MEASUREMENT = POWER_WATT -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Required(CONF_CHANNEL_ID): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_CHANNEL_ID): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the ELIQ Online sensor.""" import eliqonline @@ -40,8 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, channel_id = config.get(CONF_CHANNEL_ID) session = async_get_clientsession(hass) - api = eliqonline.API(session=session, - access_token=access_token) + api = eliqonline.API(session=session, access_token=access_token) try: _LOGGER.debug("Probing for access to ELIQ Online API") @@ -92,5 +92,4 @@ class EliqSensor(Entity): except KeyError: _LOGGER.warning("Invalid response from ELIQ Online API") except (OSError, asyncio.TimeoutError) as error: - _LOGGER.warning("Could not connect to the ELIQ Online API: %s", - error) + _LOGGER.warning("Could not connect to the ELIQ Online API: %s", error) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 564f0e74c75..e26749e6f6b 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -4,92 +4,123 @@ import re import voluptuous as vol from homeassistant.const import ( - CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, CONF_PASSWORD, - CONF_TEMPERATURE_UNIT, CONF_USERNAME) + CONF_EXCLUDE, + CONF_HOST, + CONF_INCLUDE, + CONF_PASSWORD, + CONF_TEMPERATURE_UNIT, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant, callback # noqa from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType # noqa -DOMAIN = 'elkm1' +DOMAIN = "elkm1" -CONF_AREA = 'area' -CONF_COUNTER = 'counter' -CONF_ENABLED = 'enabled' -CONF_KEYPAD = 'keypad' -CONF_OUTPUT = 'output' -CONF_PLC = 'plc' -CONF_SETTING = 'setting' -CONF_TASK = 'task' -CONF_THERMOSTAT = 'thermostat' -CONF_ZONE = 'zone' +CONF_AREA = "area" +CONF_COUNTER = "counter" +CONF_ENABLED = "enabled" +CONF_KEYPAD = "keypad" +CONF_OUTPUT = "output" +CONF_PLC = "plc" +CONF_SETTING = "setting" +CONF_TASK = "task" +CONF_THERMOSTAT = "thermostat" +CONF_ZONE = "zone" +CONF_PREFIX = "prefix" _LOGGER = logging.getLogger(__name__) -SUPPORTED_DOMAINS = ['alarm_control_panel', 'climate', 'light', 'scene', - 'sensor', 'switch'] +SUPPORTED_DOMAINS = [ + "alarm_control_panel", + "climate", + "light", + "scene", + "sensor", + "switch", +] -SPEAK_SERVICE_SCHEMA = vol.Schema({ - vol.Required('number'): - vol.All(vol.Coerce(int), vol.Range(min=0, max=999)) -}) +SPEAK_SERVICE_SCHEMA = vol.Schema( + { + vol.Required("number"): vol.All(vol.Coerce(int), vol.Range(min=0, max=999)), + vol.Optional("prefix", default=""): cv.string, + } +) def _host_validator(config): """Validate that a host is properly configured.""" - if config[CONF_HOST].startswith('elks://'): + if config[CONF_HOST].startswith("elks://"): if CONF_USERNAME not in config or CONF_PASSWORD not in config: raise vol.Invalid("Specify username and password for elks://") - elif not config[CONF_HOST].startswith('elk://') and not config[ - CONF_HOST].startswith('serial://'): + elif not config[CONF_HOST].startswith("elk://") and not config[ + CONF_HOST + ].startswith("serial://"): raise vol.Invalid("Invalid host URL") return config def _elk_range_validator(rng): def _housecode_to_int(val): - match = re.search(r'^([a-p])(0[1-9]|1[0-6]|[1-9])$', val.lower()) + match = re.search(r"^([a-p])(0[1-9]|1[0-6]|[1-9])$", val.lower()) if match: - return (ord(match.group(1)) - ord('a')) * 16 + int(match.group(2)) + return (ord(match.group(1)) - ord("a")) * 16 + int(match.group(2)) raise vol.Invalid("Invalid range") def _elk_value(val): return int(val) if val.isdigit() else _housecode_to_int(val) - vals = [s.strip() for s in str(rng).split('-')] + vals = [s.strip() for s in str(rng).split("-")] start = _elk_value(vals[0]) end = start if len(vals) == 1 else _elk_value(vals[1]) return (start, end) -CONFIG_SCHEMA_SUBDOMAIN = vol.Schema({ - vol.Optional(CONF_ENABLED, default=True): cv.boolean, - vol.Optional(CONF_INCLUDE, default=[]): [_elk_range_validator], - vol.Optional(CONF_EXCLUDE, default=[]): [_elk_range_validator], -}) +def _has_all_unique_prefixes(value): + """Validate that each m1 configured has a unique prefix. -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_USERNAME, default=''): cv.string, - vol.Optional(CONF_PASSWORD, default=''): cv.string, - vol.Optional(CONF_TEMPERATURE_UNIT, default='F'): - cv.temperature_unit, - vol.Optional(CONF_AREA, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_COUNTER, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_KEYPAD, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_OUTPUT, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_PLC, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_SETTING, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_TASK, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_THERMOSTAT, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_ZONE, default={}): CONFIG_SCHEMA_SUBDOMAIN, - }, - _host_validator, - ) -}, extra=vol.ALLOW_EXTRA) + Uniqueness is determined case-independently. + """ + prefixes = [device[CONF_PREFIX] for device in value] + schema = vol.Schema(vol.Unique()) + schema(prefixes) + return value + + +DEVICE_SCHEMA_SUBDOMAIN = vol.Schema( + { + vol.Optional(CONF_ENABLED, default=True): cv.boolean, + vol.Optional(CONF_INCLUDE, default=[]): [_elk_range_validator], + vol.Optional(CONF_EXCLUDE, default=[]): [_elk_range_validator], + } +) + +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PREFIX, default=""): vol.All(cv.string, vol.Lower), + vol.Optional(CONF_USERNAME, default=""): cv.string, + vol.Optional(CONF_PASSWORD, default=""): cv.string, + vol.Optional(CONF_TEMPERATURE_UNIT, default="F"): cv.temperature_unit, + vol.Optional(CONF_AREA, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_COUNTER, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_KEYPAD, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_OUTPUT, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_PLC, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_SETTING, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_TASK, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_THERMOSTAT, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_ZONE, default={}): DEVICE_SCHEMA_SUBDOMAIN, + }, + _host_validator, +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA], _has_all_unique_prefixes)}, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: @@ -97,6 +128,9 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: from elkm1_lib.const import Max import elkm1_lib as elkm1 + devices = {} + elk_datas = {} + configs = { CONF_AREA: Max.AREAS.value, CONF_COUNTER: Max.COUNTERS.value, @@ -113,57 +147,87 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: for rng in ranges: if not rng[0] <= rng[1] <= len(values): raise vol.Invalid("Invalid range {}".format(rng)) - values[rng[0]-1:rng[1]] = [set_to] * (rng[1] - rng[0] + 1) + values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1) - conf = hass_config[DOMAIN] - config = {'temperature_unit': conf[CONF_TEMPERATURE_UNIT]} - config['panel'] = {'enabled': True, 'included': [True]} + for index, conf in enumerate(hass_config[DOMAIN]): + _LOGGER.debug("Setting up elkm1 #%d - %s", index, conf["host"]) - for item, max_ in configs.items(): - config[item] = {'enabled': conf[item][CONF_ENABLED], - 'included': [not conf[item]['include']] * max_} - try: - _included(conf[item]['include'], True, config[item]['included']) - _included(conf[item]['exclude'], False, config[item]['included']) - except (ValueError, vol.Invalid) as err: - _LOGGER.error("Config item: %s; %s", item, err) - return False + config = {"temperature_unit": conf[CONF_TEMPERATURE_UNIT]} + config["panel"] = {"enabled": True, "included": [True]} - elk = elkm1.Elk({'url': conf[CONF_HOST], 'userid': conf[CONF_USERNAME], - 'password': conf[CONF_PASSWORD]}) - elk.connect() + for item, max_ in configs.items(): + config[item] = { + "enabled": conf[item][CONF_ENABLED], + "included": [not conf[item]["include"]] * max_, + } + try: + _included(conf[item]["include"], True, config[item]["included"]) + _included(conf[item]["exclude"], False, config[item]["included"]) + except (ValueError, vol.Invalid) as err: + _LOGGER.error("Config item: %s; %s", item, err) + return False - _create_elk_services(hass, elk) + prefix = conf[CONF_PREFIX] + elk = elkm1.Elk( + { + "url": conf[CONF_HOST], + "userid": conf[CONF_USERNAME], + "password": conf[CONF_PASSWORD], + } + ) + elk.connect() - hass.data[DOMAIN] = {'elk': elk, 'config': config, 'keypads': {}} + devices[prefix] = elk + elk_datas[prefix] = { + "elk": elk, + "prefix": prefix, + "config": config, + "keypads": {}, + } + + _create_elk_services(hass, devices) + + hass.data[DOMAIN] = elk_datas for component in SUPPORTED_DOMAINS: hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN, {}, - hass_config)) + discovery.async_load_platform(hass, component, DOMAIN, {}, hass_config) + ) return True -def _create_elk_services(hass, elk): +def _create_elk_services(hass, elks): def _speak_word_service(service): - elk.panel.speak_word(service.data.get('number')) + prefix = service.data["prefix"] + elk = elks.get(prefix) + if elk is None: + _LOGGER.error("No elk m1 with prefix for speak_word: '%s'", prefix) + return + elk.panel.speak_word(service.data["number"]) def _speak_phrase_service(service): - elk.panel.speak_phrase(service.data.get('number')) + prefix = service.data["prefix"] + elk = elks.get(prefix) + if elk is None: + _LOGGER.error("No elk m1 with prefix for speak_phrase: '%s'", prefix) + return + elk.panel.speak_phrase(service.data["number"]) hass.services.async_register( - DOMAIN, 'speak_word', _speak_word_service, SPEAK_SERVICE_SCHEMA) + DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA + ) hass.services.async_register( - DOMAIN, 'speak_phrase', _speak_phrase_service, SPEAK_SERVICE_SCHEMA) + DOMAIN, "speak_phrase", _speak_phrase_service, SPEAK_SERVICE_SCHEMA + ) -def create_elk_entities(hass, elk_elements, element_type, class_, entities): +def create_elk_entities(elk_data, elk_elements, element_type, class_, entities): """Create the ElkM1 devices of a particular class.""" - elk_data = hass.data[DOMAIN] - if elk_data['config'][element_type]['enabled']: - elk = elk_data['elk'] + if elk_data["config"][element_type]["enabled"]: + elk = elk_data["elk"] + _LOGGER.debug("Creating elk entities for %s", elk) for element in elk_elements: - if elk_data['config'][element_type]['included'][element.index]: + if elk_data["config"][element_type]["included"][element.index]: entities.append(class_(element, elk, elk_data)) return entities @@ -175,14 +239,28 @@ class ElkEntity(Entity): """Initialize the base of all Elk devices.""" self._elk = elk self._element = element - self._temperature_unit = elk_data['config']['temperature_unit'] - self._unique_id = 'elkm1_{}'.format( - self._element.default_name('_').lower()) + self._prefix = elk_data["prefix"] + self._temperature_unit = elk_data["config"]["temperature_unit"] + # unique_id starts with elkm1_ iff there is no prefix + # it starts with elkm1m_{prefix} iff there is a prefix + # this is to avoid a conflict between + # prefix=foo, name=bar (which would be elkm1_foo_bar) + # - and - + # prefix="", name="foo bar" (which would be elkm1_foo_bar also) + # we could have used elkm1__foo_bar for the latter, but that + # would have been a breaking change + if self._prefix != "": + uid_start = "elkm1m_{prefix}".format(prefix=self._prefix) + else: + uid_start = "elkm1" + self._unique_id = "{uid_start}_{name}".format( + uid_start=uid_start, name=self._element.default_name("_") + ).lower() @property def name(self): """Name of the element.""" - return self._element.name + return "{p}{n}".format(p=self._prefix, n=self._element.name) @property def unique_id(self): @@ -207,7 +285,7 @@ class ElkEntity(Entity): def initial_attrs(self): """Return the underlying element's attributes as a dict.""" attrs = {} - attrs['index'] = self._element.index + 1 + attrs["index"] = self._element.index + 1 return attrs def _element_changed(self, element, changeset): diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index b885913a0df..275d94efa66 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -3,47 +3,63 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) + ATTR_CODE, + ATTR_ENTITY_ID, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMING, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, async_dispatcher_send) + async_dispatcher_connect, + async_dispatcher_send, +) from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities -SIGNAL_ARM_ENTITY = 'elkm1_arm' -SIGNAL_DISPLAY_MESSAGE = 'elkm1_display_message' +SIGNAL_ARM_ENTITY = "elkm1_arm" +SIGNAL_DISPLAY_MESSAGE = "elkm1_display_message" -ELK_ALARM_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids, - vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)), -}) +ELK_ALARM_SERVICE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids, + vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)), + } +) -DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids, - vol.Optional('clear', default=2): vol.In([0, 1, 2]), - vol.Optional('beep', default=False): cv.boolean, - vol.Optional('timeout', default=0): vol.Range(min=0, max=65535), - vol.Optional('line1', default=''): cv.string, - vol.Optional('line2', default=''): cv.string, -}) +DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids, + vol.Optional("clear", default=2): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), + vol.Optional("beep", default=False): cv.boolean, + vol.Optional("timeout", default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=65535) + ), + vol.Optional("line1", default=""): cv.string, + vol.Optional("line2", default=""): cv.string, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the ElkM1 alarm platform.""" if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - entities = create_elk_entities(hass, elk.areas, 'area', ElkArea, []) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data["elk"] + entities = create_elk_entities(elk_data, elk.areas, "area", ElkArea, entities) async_add_entities(entities, True) def _dispatch(signal, entity_ids, *args): for entity_id in entity_ids: - async_dispatcher_send( - hass, '{}_{}'.format(signal, entity_id), *args) + async_dispatcher_send(hass, "{}_{}".format(signal, entity_id), *args) def _arm_service(service): entity_ids = service.data.get(ATTR_ENTITY_ID, []) @@ -53,27 +69,36 @@ async def async_setup_platform(hass, config, async_add_entities, for service in _arm_services(): hass.services.async_register( - alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA) + alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA + ) def _display_message_service(service): entity_ids = service.data.get(ATTR_ENTITY_ID, []) data = service.data - args = (data['clear'], data['beep'], data['timeout'], - data['line1'], data['line2']) + args = ( + data["clear"], + data["beep"], + data["timeout"], + data["line1"], + data["line2"], + ) _dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args) hass.services.async_register( - alarm.DOMAIN, 'elkm1_alarm_display_message', - _display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA) + alarm.DOMAIN, + "elkm1_alarm_display_message", + _display_message_service, + DISPLAY_MESSAGE_SERVICE_SCHEMA, + ) def _arm_services(): from elkm1_lib.const import ArmLevel return { - 'elkm1_alarm_arm_vacation': ArmLevel.ARMED_VACATION.value, - 'elkm1_alarm_arm_home_instant': ArmLevel.ARMED_STAY_INSTANT.value, - 'elkm1_alarm_arm_night_instant': ArmLevel.ARMED_NIGHT_INSTANT.value, + "elkm1_alarm_arm_vacation": ArmLevel.ARMED_VACATION.value, + "elkm1_alarm_arm_home_instant": ArmLevel.ARMED_STAY_INSTANT.value, + "elkm1_alarm_arm_night_instant": ArmLevel.ARMED_NIGHT_INSTANT.value, } @@ -83,7 +108,7 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): def __init__(self, element, elk, elk_data): """Initialize Area as Alarm Control Panel.""" super().__init__(element, elk, elk_data) - self._changed_by_entity_id = '' + self._changed_by_entity_id = "" self._state = None async def async_added_to_hass(self): @@ -92,18 +117,23 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): for keypad in self._elk.keypads: keypad.add_callback(self._watch_keypad) async_dispatcher_connect( - self.hass, '{}_{}'.format(SIGNAL_ARM_ENTITY, self.entity_id), - self._arm_service) + self.hass, + "{}_{}".format(SIGNAL_ARM_ENTITY, self.entity_id), + self._arm_service, + ) async_dispatcher_connect( - self.hass, '{}_{}'.format(SIGNAL_DISPLAY_MESSAGE, self.entity_id), - self._display_message) + self.hass, + "{}_{}".format(SIGNAL_DISPLAY_MESSAGE, self.entity_id), + self._display_message, + ) def _watch_keypad(self, keypad, changeset): if keypad.area != self._element.index: return - if changeset.get('last_user') is not None: - self._changed_by_entity_id = self.hass.data[ - ELK_DOMAIN]['keypads'].get(keypad.index, '') + if changeset.get("last_user") is not None: + self._changed_by_entity_id = self.hass.data[ELK_DOMAIN][self._prefix][ + "keypads" + ].get(keypad.index, "") self.async_schedule_update_ha_state(True) @property @@ -123,17 +153,16 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): attrs = self.initial_attrs() elmt = self._element - attrs['is_exit'] = elmt.is_exit - attrs['timer1'] = elmt.timer1 - attrs['timer2'] = elmt.timer2 + attrs["is_exit"] = elmt.is_exit + attrs["timer1"] = elmt.timer1 + attrs["timer2"] = elmt.timer2 if elmt.armed_status is not None: - attrs['armed_status'] = \ - ArmedStatus(elmt.armed_status).name.lower() + attrs["armed_status"] = ArmedStatus(elmt.armed_status).name.lower() if elmt.arm_up_state is not None: - attrs['arm_up_state'] = ArmUpState(elmt.arm_up_state).name.lower() + attrs["arm_up_state"] = ArmUpState(elmt.arm_up_state).name.lower() if elmt.alarm_state is not None: - attrs['alarm_state'] = AlarmState(elmt.alarm_state).name.lower() - attrs['changed_by_entity_id'] = self._changed_by_entity_id + attrs["alarm_state"] = AlarmState(elmt.alarm_state).name.lower() + attrs["changed_by_entity_id"] = self._changed_by_entity_id return attrs def _element_changed(self, element, changeset): @@ -154,8 +183,9 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): elif self._area_is_in_alarm_state(): self._state = STATE_ALARM_TRIGGERED elif self._entry_exit_timer_is_running(): - self._state = STATE_ALARM_ARMING \ - if self._element.is_exit else STATE_ALARM_PENDING + self._state = ( + STATE_ALARM_ARMING if self._element.is_exit else STATE_ALARM_PENDING + ) else: self._state = elk_state_to_hass_state[self._element.armed_status] diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index c3e9bcce860..58273e71222 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -1,29 +1,44 @@ """Support for control of Elk-M1 connected thermostats.""" from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, HVAC_MODE_AUTO, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + HVAC_MODE_AUTO, HVAC_MODE_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_AUX_HEAT, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, - SUPPORT_TARGET_TEMPERATURE_RANGE) + SUPPORT_TARGET_TEMPERATURE_RANGE, +) from homeassistant.const import PRECISION_WHOLE, STATE_ON from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities -SUPPORT_HVAC = [HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_AUTO, - HVAC_MODE_FAN_ONLY] +SUPPORT_HVAC = [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_AUTO, + HVAC_MODE_FAN_ONLY, +] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the Elk-M1 thermostat platform.""" if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - async_add_entities(create_elk_entities( - hass, elk.thermostats, 'thermostat', ElkThermostat, []), True) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data["elk"] + entities = create_elk_entities( + elk_data, elk.thermostats, "thermostat", ElkThermostat, entities + ) + async_add_entities(entities, True) class ElkThermostat(ElkEntity, ClimateDevice): @@ -37,8 +52,7 @@ class ElkThermostat(ElkEntity, ClimateDevice): @property def supported_features(self): """Return the list of supported features.""" - return (SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT - | SUPPORT_TARGET_TEMPERATURE_RANGE) + return SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_RANGE @property def temperature_unit(self): @@ -54,8 +68,10 @@ class ElkThermostat(ElkEntity, ClimateDevice): def target_temperature(self): """Return the temperature we are trying to reach.""" from elkm1_lib.const import ThermostatMode + if (self._element.mode == ThermostatMode.HEAT.value) or ( - self._element.mode == ThermostatMode.EMERGENCY_HEAT.value): + self._element.mode == ThermostatMode.EMERGENCY_HEAT.value + ): return self._element.heat_setpoint if self._element.mode == ThermostatMode.COOL.value: return self._element.cool_setpoint @@ -100,6 +116,7 @@ class ElkThermostat(ElkEntity, ClimateDevice): def is_aux_heat(self): """Return if aux heater is on.""" from elkm1_lib.const import ThermostatMode + return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value @property @@ -116,6 +133,7 @@ class ElkThermostat(ElkEntity, ClimateDevice): def fan_mode(self): """Return the fan setting.""" from elkm1_lib.const import ThermostatFan + if self._element.fan == ThermostatFan.AUTO.value: return HVAC_MODE_AUTO if self._element.fan == ThermostatFan.ON.value: @@ -124,6 +142,7 @@ class ElkThermostat(ElkEntity, ClimateDevice): def _elk_set(self, mode, fan): from elkm1_lib.const import ThermostatSetting + if mode is not None: self._element.set(ThermostatSetting.MODE.value, mode) if fan is not None: @@ -132,25 +151,26 @@ class ElkThermostat(ElkEntity, ClimateDevice): async def async_set_hvac_mode(self, hvac_mode): """Set thermostat operation mode.""" from elkm1_lib.const import ThermostatFan, ThermostatMode + settings = { - HVAC_MODE_OFF: - (ThermostatMode.OFF.value, ThermostatFan.AUTO.value), + HVAC_MODE_OFF: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value), HVAC_MODE_HEAT: (ThermostatMode.HEAT.value, None), HVAC_MODE_COOL: (ThermostatMode.COOL.value, None), HVAC_MODE_AUTO: (ThermostatMode.AUTO.value, None), - HVAC_MODE_FAN_ONLY: - (ThermostatMode.OFF.value, ThermostatFan.ON.value) + HVAC_MODE_FAN_ONLY: (ThermostatMode.OFF.value, ThermostatFan.ON.value), } self._elk_set(settings[hvac_mode][0], settings[hvac_mode][1]) async def async_turn_aux_heat_on(self): """Turn auxiliary heater on.""" from elkm1_lib.const import ThermostatMode + self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None) async def async_turn_aux_heat_off(self): """Turn auxiliary heater off.""" from elkm1_lib.const import ThermostatMode + self._elk_set(ThermostatMode.HEAT.value, None) @property @@ -161,6 +181,7 @@ class ElkThermostat(ElkEntity, ClimateDevice): async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" from elkm1_lib.const import ThermostatFan + if fan_mode == HVAC_MODE_AUTO: self._elk_set(None, ThermostatFan.AUTO.value) elif fan_mode == STATE_ON: @@ -169,17 +190,17 @@ class ElkThermostat(ElkEntity, ClimateDevice): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" from elkm1_lib.const import ThermostatSetting + low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) if low_temp is not None: - self._element.set( - ThermostatSetting.HEAT_SETPOINT.value, round(low_temp)) + self._element.set(ThermostatSetting.HEAT_SETPOINT.value, round(low_temp)) if high_temp is not None: - self._element.set( - ThermostatSetting.COOL_SETPOINT.value, round(high_temp)) + self._element.set(ThermostatSetting.COOL_SETPOINT.value, round(high_temp)) def _element_changed(self, element, changeset): from elkm1_lib.const import ThermostatFan, ThermostatMode + mode_to_state = { ThermostatMode.OFF.value: HVAC_MODE_OFF, ThermostatMode.COOL.value: HVAC_MODE_COOL, @@ -188,6 +209,5 @@ class ElkThermostat(ElkEntity, ClimateDevice): ThermostatMode.AUTO.value: HVAC_MODE_AUTO, } self._state = mode_to_state.get(self._element.mode) - if self._state == HVAC_MODE_OFF and \ - self._element.fan == ThermostatFan.ON.value: + if self._state == HVAC_MODE_OFF and self._element.fan == ThermostatFan.ON.value: self._state = HVAC_MODE_FAN_ONLY diff --git a/homeassistant/components/elkm1/light.py b/homeassistant/components/elkm1/light.py index ee6fe09a7a2..10a9ae1b931 100644 --- a/homeassistant/components/elkm1/light.py +++ b/homeassistant/components/elkm1/light.py @@ -1,18 +1,19 @@ """Support for control of ElkM1 lighting (X10, UPB, etc).""" -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Elk light platform.""" if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - async_add_entities( - create_elk_entities(hass, elk.lights, 'plc', ElkLight, []), True) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data["elk"] + create_elk_entities(elk_data, elk.lights, "plc", ElkLight, entities) + async_add_entities(entities, True) class ElkLight(ElkEntity, Light): diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 73b48623260..466f9da7e90 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -3,7 +3,7 @@ "name": "Elkm1", "documentation": "https://www.home-assistant.io/components/elkm1", "requirements": [ - "elkm1-lib==0.7.13" + "elkm1-lib==0.7.15" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/elkm1/scene.py b/homeassistant/components/elkm1/scene.py index aaae8bb0a5c..dc5ea39d154 100644 --- a/homeassistant/components/elkm1/scene.py +++ b/homeassistant/components/elkm1/scene.py @@ -4,13 +4,15 @@ from homeassistant.components.scene import Scene from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the Elk-M1 scene platform.""" if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - entities = create_elk_entities(hass, elk.tasks, 'task', ElkTask, []) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data["elk"] + entities = create_elk_entities(elk_data, elk.tasks, "task", ElkTask, entities) async_add_entities(entities, True) diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 0e367265605..3f524b778db 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -2,23 +2,28 @@ from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the Elk-M1 sensor platform.""" if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - entities = create_elk_entities( - hass, elk.counters, 'counter', ElkCounter, []) - entities = create_elk_entities( - hass, elk.keypads, 'keypad', ElkKeypad, entities) - entities = create_elk_entities( - hass, [elk.panel], 'panel', ElkPanel, entities) - entities = create_elk_entities( - hass, elk.settings, 'setting', ElkSetting, entities) - entities = create_elk_entities( - hass, elk.zones, 'zone', ElkZone, entities) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data["elk"] + entities = create_elk_entities( + elk_data, elk.counters, "counter", ElkCounter, entities + ) + entities = create_elk_entities( + elk_data, elk.keypads, "keypad", ElkKeypad, entities + ) + entities = create_elk_entities( + elk_data, [elk.panel], "panel", ElkPanel, entities + ) + entities = create_elk_entities( + elk_data, elk.settings, "setting", ElkSetting, entities + ) + entities = create_elk_entities(elk_data, elk.zones, "zone", ElkZone, entities) async_add_entities(entities, True) @@ -47,7 +52,7 @@ class ElkCounter(ElkSensor): @property def icon(self): """Icon to use in the frontend.""" - return 'mdi:numeric' + return "mdi:numeric" def _element_changed(self, element, changeset): self._state = self._element.value @@ -69,7 +74,7 @@ class ElkKeypad(ElkSensor): @property def icon(self): """Icon to use in the frontend.""" - return 'mdi:thermometer-lines' + return "mdi:thermometer-lines" @property def device_state_attributes(self): @@ -77,13 +82,13 @@ class ElkKeypad(ElkSensor): from elkm1_lib.util import username attrs = self.initial_attrs() - attrs['area'] = self._element.area + 1 - attrs['temperature'] = self._element.temperature - attrs['last_user_time'] = self._element.last_user_time.isoformat() - attrs['last_user'] = self._element.last_user + 1 - attrs['code'] = self._element.code - attrs['last_user_name'] = username(self._elk, self._element.last_user) - attrs['last_keypress'] = self._element.last_keypress + attrs["area"] = self._element.area + 1 + attrs["temperature"] = self._element.temperature + attrs["last_user_time"] = self._element.last_user_time.isoformat() + attrs["last_user"] = self._element.last_user + 1 + attrs["code"] = self._element.code + attrs["last_user_name"] = username(self._elk, self._element.last_user) + attrs["last_keypress"] = self._element.last_keypress return attrs def _element_changed(self, element, changeset): @@ -92,8 +97,9 @@ class ElkKeypad(ElkSensor): async def async_added_to_hass(self): """Register callback for ElkM1 changes and update entity state.""" await super().async_added_to_hass() - self.hass.data[ELK_DOMAIN]['keypads'][ - self._element.index] = self.entity_id + elk_datas = self.hass.data[ELK_DOMAIN] + for elk_data in elk_datas.values(): + elk_data["keypads"][self._element.index] = self.entity_id class ElkPanel(ElkSensor): @@ -108,15 +114,16 @@ class ElkPanel(ElkSensor): def device_state_attributes(self): """Attributes of the sensor.""" attrs = self.initial_attrs() - attrs['system_trouble_status'] = self._element.system_trouble_status + attrs["system_trouble_status"] = self._element.system_trouble_status return attrs def _element_changed(self, element, changeset): if self._elk.is_connected(): - self._state = 'Paused' if self._element.remote_programming_status \ - else 'Connected' + self._state = ( + "Paused" if self._element.remote_programming_status else "Connected" + ) else: - self._state = 'Disconnected' + self._state = "Disconnected" class ElkSetting(ElkSensor): @@ -125,7 +132,7 @@ class ElkSetting(ElkSensor): @property def icon(self): """Icon to use in the frontend.""" - return 'mdi:numeric' + return "mdi:numeric" def _element_changed(self, element, changeset): self._state = self._element.value @@ -134,9 +141,9 @@ class ElkSetting(ElkSensor): def device_state_attributes(self): """Attributes of the sensor.""" from elkm1_lib.const import SettingFormat + attrs = self.initial_attrs() - attrs['value_format'] = SettingFormat( - self._element.value_format).name.lower() + attrs["value_format"] = SettingFormat(self._element.value_format).name.lower() return attrs @@ -147,52 +154,53 @@ class ElkZone(ElkSensor): def icon(self): """Icon to use in the frontend.""" from elkm1_lib.const import ZoneType + zone_icons = { - ZoneType.FIRE_ALARM.value: 'fire', - ZoneType.FIRE_VERIFIED.value: 'fire', - ZoneType.FIRE_SUPERVISORY.value: 'fire', - ZoneType.KEYFOB.value: 'key', - ZoneType.NON_ALARM.value: 'alarm-off', - ZoneType.MEDICAL_ALARM.value: 'medical-bag', - ZoneType.POLICE_ALARM.value: 'alarm-light', - ZoneType.POLICE_NO_INDICATION.value: 'alarm-light', - ZoneType.KEY_MOMENTARY_ARM_DISARM.value: 'power', - ZoneType.KEY_MOMENTARY_ARM_AWAY.value: 'power', - ZoneType.KEY_MOMENTARY_ARM_STAY.value: 'power', - ZoneType.KEY_MOMENTARY_DISARM.value: 'power', - ZoneType.KEY_ON_OFF.value: 'toggle-switch', - ZoneType.MUTE_AUDIBLES.value: 'volume-mute', - ZoneType.POWER_SUPERVISORY.value: 'power-plug', - ZoneType.TEMPERATURE.value: 'thermometer-lines', - ZoneType.ANALOG_ZONE.value: 'speedometer', - ZoneType.PHONE_KEY.value: 'phone-classic', - ZoneType.INTERCOM_KEY.value: 'deskphone' + ZoneType.FIRE_ALARM.value: "fire", + ZoneType.FIRE_VERIFIED.value: "fire", + ZoneType.FIRE_SUPERVISORY.value: "fire", + ZoneType.KEYFOB.value: "key", + ZoneType.NON_ALARM.value: "alarm-off", + ZoneType.MEDICAL_ALARM.value: "medical-bag", + ZoneType.POLICE_ALARM.value: "alarm-light", + ZoneType.POLICE_NO_INDICATION.value: "alarm-light", + ZoneType.KEY_MOMENTARY_ARM_DISARM.value: "power", + ZoneType.KEY_MOMENTARY_ARM_AWAY.value: "power", + ZoneType.KEY_MOMENTARY_ARM_STAY.value: "power", + ZoneType.KEY_MOMENTARY_DISARM.value: "power", + ZoneType.KEY_ON_OFF.value: "toggle-switch", + ZoneType.MUTE_AUDIBLES.value: "volume-mute", + ZoneType.POWER_SUPERVISORY.value: "power-plug", + ZoneType.TEMPERATURE.value: "thermometer-lines", + ZoneType.ANALOG_ZONE.value: "speedometer", + ZoneType.PHONE_KEY.value: "phone-classic", + ZoneType.INTERCOM_KEY.value: "deskphone", } - return 'mdi:{}'.format( - zone_icons.get(self._element.definition, 'alarm-bell')) + return "mdi:{}".format(zone_icons.get(self._element.definition, "alarm-bell")) @property def device_state_attributes(self): """Attributes of the sensor.""" - from elkm1_lib.const import ( - ZoneLogicalStatus, ZonePhysicalStatus, ZoneType) + from elkm1_lib.const import ZoneLogicalStatus, ZonePhysicalStatus, ZoneType attrs = self.initial_attrs() - attrs['physical_status'] = ZonePhysicalStatus( - self._element.physical_status).name.lower() - attrs['logical_status'] = ZoneLogicalStatus( - self._element.logical_status).name.lower() - attrs['definition'] = ZoneType( - self._element.definition).name.lower() - attrs['area'] = self._element.area + 1 - attrs['bypassed'] = self._element.bypassed - attrs['triggered_alarm'] = self._element.triggered_alarm + attrs["physical_status"] = ZonePhysicalStatus( + self._element.physical_status + ).name.lower() + attrs["logical_status"] = ZoneLogicalStatus( + self._element.logical_status + ).name.lower() + attrs["definition"] = ZoneType(self._element.definition).name.lower() + attrs["area"] = self._element.area + 1 + attrs["bypassed"] = self._element.bypassed + attrs["triggered_alarm"] = self._element.triggered_alarm return attrs @property def temperature_unit(self): """Return the temperature unit.""" from elkm1_lib.const import ZoneType + if self._element.definition == ZoneType.TEMPERATURE.value: return self._temperature_unit return None @@ -201,10 +209,11 @@ class ElkZone(ElkSensor): def unit_of_measurement(self): """Return the unit of measurement.""" from elkm1_lib.const import ZoneType + if self._element.definition == ZoneType.TEMPERATURE.value: return self._temperature_unit if self._element.definition == ZoneType.ANALOG_ZONE.value: - return 'V' + return "V" return None def _element_changed(self, element, changeset): @@ -216,5 +225,6 @@ class ElkZone(ElkSensor): elif self._element.definition == ZoneType.ANALOG_ZONE.value: self._state = self._element.voltage else: - self._state = pretty_const(ZoneLogicalStatus( - self._element.logical_status).name) + self._state = pretty_const( + ZoneLogicalStatus(self._element.logical_status).name + ) diff --git a/homeassistant/components/elkm1/switch.py b/homeassistant/components/elkm1/switch.py index df29491435e..e6dd82dc0ac 100644 --- a/homeassistant/components/elkm1/switch.py +++ b/homeassistant/components/elkm1/switch.py @@ -4,13 +4,17 @@ from homeassistant.components.switch import SwitchDevice from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the Elk-M1 switch platform.""" if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - entities = create_elk_entities(hass, elk.outputs, 'output', ElkOutput, []) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data["elk"] + entities = create_elk_entities( + elk_data, elk.outputs, "output", ElkOutput, entities + ) async_add_entities(entities, True) diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index bd97d10cecf..c6258e244e9 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -4,21 +4,25 @@ import logging import voluptuous as vol from homeassistant.components.switch import ( - SwitchDevice, PLATFORM_SCHEMA, ATTR_CURRENT_POWER_W) -from homeassistant.const import ( - CONF_NAME, CONF_DEVICE, EVENT_HOMEASSISTANT_STOP) + SwitchDevice, + PLATFORM_SCHEMA, + ATTR_CURRENT_POWER_W, +) +from homeassistant.const import CONF_NAME, CONF_DEVICE, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_TOTAL_ENERGY_KWH = 'total_energy_kwh' +ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh" -DEFAULT_NAME = 'PCA 301' +DEFAULT_NAME = "PCA 301" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_DEVICE): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_DEVICE): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -32,8 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: pca = pypca.PCA(usb_device) pca.open() - entities = [SmartPlugSwitch(pca, device, name) - for device in pca.get_devices()] + entities = [SmartPlugSwitch(pca, device, name) for device in pca.get_devices()] add_entities(entities, True) except SerialException as exc: @@ -89,15 +92,16 @@ class SmartPlugSwitch(SwitchDevice): """Update the PCA switch's state.""" try: self._emeter_params[ATTR_CURRENT_POWER_W] = "{:.1f}".format( - self._pca.get_current_power(self._device_id)) + self._pca.get_current_power(self._device_id) + ) self._emeter_params[ATTR_TOTAL_ENERGY_KWH] = "{:.2f}".format( - self._pca.get_total_consumption(self._device_id)) + self._pca.get_total_consumption(self._device_id) + ) self._available = True self._state = self._pca.get_state(self._device_id) except (OSError) as ex: if self._available: - _LOGGER.warning( - "Could not read state for %s: %s", self.name, ex) + _LOGGER.warning("Could not read state for %s: %s", self.name, ex) self._available = False diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index fa1c096707b..409dd8ec472 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -3,28 +3,44 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_STOP) + MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_TVSHOW, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_STOP, +) from homeassistant.const import ( - CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL, DEVICE_DEFAULT_NAME, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, - STATE_PAUSED, STATE_PLAYING) + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + DEVICE_DEFAULT_NAME, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -CONF_AUTO_HIDE = 'auto_hide' +CONF_AUTO_HIDE = "auto_hide" -MEDIA_TYPE_TRAILER = 'trailer' -MEDIA_TYPE_GENERIC_VIDEO = 'video' +MEDIA_TYPE_TRAILER = "trailer" +MEDIA_TYPE_GENERIC_VIDEO = "video" -DEFAULT_HOST = 'localhost' +DEFAULT_HOST = "localhost" DEFAULT_PORT = 8096 DEFAULT_SSL_PORT = 8920 DEFAULT_SSL = False @@ -32,20 +48,27 @@ DEFAULT_AUTO_HIDE = False _LOGGER = logging.getLogger(__name__) -SUPPORT_EMBY = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_PLAY +SUPPORT_EMBY = ( + SUPPORT_PAUSE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_STOP + | SUPPORT_SEEK + | SUPPORT_PLAY +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_AUTO_HIDE, default=DEFAULT_AUTO_HIDE): cv.boolean, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_AUTO_HIDE, default=DEFAULT_AUTO_HIDE): cv.boolean, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Emby platform.""" from pyemby import EmbyServer @@ -72,14 +95,16 @@ async def async_setup_platform(hass, config, async_add_entities, active_devices = [] for dev_id in emby.devices: active_devices.append(dev_id) - if dev_id not in active_emby_devices and \ - dev_id not in inactive_emby_devices: + if ( + dev_id not in active_emby_devices + and dev_id not in inactive_emby_devices + ): new = EmbyDevice(emby, dev_id) active_emby_devices[dev_id] = new new_devices.append(new) elif dev_id in inactive_emby_devices: - if emby.devices[dev_id].state != 'Off': + if emby.devices[dev_id].state != "Off": add = inactive_emby_devices.pop(dev_id) active_emby_devices[dev_id] = add _LOGGER.debug("Showing %s, item: %s", dev_id, add) @@ -135,8 +160,7 @@ class EmbyDevice(MediaPlayerDevice): async def async_added_to_hass(self): """Register callback.""" - self.emby.add_update_callback( - self.async_update_callback, self.device_id) + self.emby.add_update_callback(self.async_update_callback, self.device_id) @callback def async_update_callback(self, msg): @@ -184,8 +208,10 @@ class EmbyDevice(MediaPlayerDevice): @property def name(self): """Return the name of the device.""" - return ('Emby - {} - {}'.format(self.device.client, self.device.name) - or DEVICE_DEFAULT_NAME) + return ( + "Emby - {} - {}".format(self.device.client, self.device.name) + or DEVICE_DEFAULT_NAME + ) @property def should_poll(self): @@ -196,13 +222,13 @@ class EmbyDevice(MediaPlayerDevice): def state(self): """Return the state of the device.""" state = self.device.state - if state == 'Paused': + if state == "Paused": return STATE_PAUSED - if state == 'Playing': + if state == "Playing": return STATE_PLAYING - if state == 'Idle': + if state == "Idle": return STATE_IDLE - if state == 'Off': + if state == "Off": return STATE_OFF @property @@ -220,19 +246,19 @@ class EmbyDevice(MediaPlayerDevice): def media_content_type(self): """Content type of current playing media.""" media_type = self.device.media_type - if media_type == 'Episode': + if media_type == "Episode": return MEDIA_TYPE_TVSHOW - if media_type == 'Movie': + if media_type == "Movie": return MEDIA_TYPE_MOVIE - if media_type == 'Trailer': + if media_type == "Trailer": return MEDIA_TYPE_TRAILER - if media_type == 'Music': + if media_type == "Music": return MEDIA_TYPE_MUSIC - if media_type == 'Video': + if media_type == "Video": return MEDIA_TYPE_GENERIC_VIDEO - if media_type == 'Audio': + if media_type == "Audio": return MEDIA_TYPE_MUSIC - if media_type == 'TvChannel': + if media_type == "TvChannel": return MEDIA_TYPE_CHANNEL return None diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 6e059e1a30f..8d79b771fb9 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -8,52 +8,65 @@ import requests import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, CONF_URL, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, - CONF_ID, CONF_SCAN_INTERVAL, STATE_UNKNOWN, POWER_WATT) + CONF_API_KEY, + CONF_URL, + CONF_VALUE_TEMPLATE, + CONF_UNIT_OF_MEASUREMENT, + CONF_ID, + CONF_SCAN_INTERVAL, + STATE_UNKNOWN, + POWER_WATT, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers import template from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTR_FEEDID = 'FeedId' -ATTR_FEEDNAME = 'FeedName' -ATTR_LASTUPDATETIME = 'LastUpdated' -ATTR_LASTUPDATETIMESTR = 'LastUpdatedStr' -ATTR_SIZE = 'Size' -ATTR_TAG = 'Tag' -ATTR_USERID = 'UserId' +ATTR_FEEDID = "FeedId" +ATTR_FEEDNAME = "FeedName" +ATTR_LASTUPDATETIME = "LastUpdated" +ATTR_LASTUPDATETIMESTR = "LastUpdatedStr" +ATTR_SIZE = "Size" +ATTR_TAG = "Tag" +ATTR_USERID = "UserId" -CONF_EXCLUDE_FEEDID = 'exclude_feed_id' -CONF_ONLY_INCLUDE_FEEDID = 'include_only_feed_id' -CONF_SENSOR_NAMES = 'sensor_names' +CONF_EXCLUDE_FEEDID = "exclude_feed_id" +CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id" +CONF_SENSOR_NAMES = "sensor_names" DECIMALS = 2 DEFAULT_UNIT = POWER_WATT MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -ONLY_INCL_EXCL_NONE = 'only_include_exclude_or_none' +ONLY_INCL_EXCL_NONE = "only_include_exclude_or_none" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_URL): cv.string, - vol.Required(CONF_ID): cv.positive_int, - vol.Exclusive(CONF_ONLY_INCLUDE_FEEDID, ONLY_INCL_EXCL_NONE): - vol.All(cv.ensure_list, [cv.positive_int]), - vol.Exclusive(CONF_EXCLUDE_FEEDID, ONLY_INCL_EXCL_NONE): - vol.All(cv.ensure_list, [cv.positive_int]), - vol.Optional(CONF_SENSOR_NAMES): - vol.All({cv.positive_int: vol.All(cv.string, vol.Length(min=1))}), - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=DEFAULT_UNIT): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_URL): cv.string, + vol.Required(CONF_ID): cv.positive_int, + vol.Exclusive(CONF_ONLY_INCLUDE_FEEDID, ONLY_INCL_EXCL_NONE): vol.All( + cv.ensure_list, [cv.positive_int] + ), + vol.Exclusive(CONF_EXCLUDE_FEEDID, ONLY_INCL_EXCL_NONE): vol.All( + cv.ensure_list, [cv.positive_int] + ), + vol.Optional(CONF_SENSOR_NAMES): vol.All( + {cv.positive_int: vol.All(cv.string, vol.Length(min=1))} + ), + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=DEFAULT_UNIT): cv.string, + } +) def get_id(sensorid, feedtag, feedname, feedid, feeduserid): """Return unique identifier for feed / sensor.""" return "emoncms{}_{}_{}_{}_{}".format( - sensorid, feedtag, feedname, feedid, feeduserid) + sensorid, feedtag, feedname, feedid, feeduserid + ) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -94,30 +107,40 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if sensor_names is not None: name = sensor_names.get(int(elem["id"]), None) - sensors.append(EmonCmsSensor(hass, data, name, value_template, - unit_of_measurement, str(sensorid), - elem)) + sensors.append( + EmonCmsSensor( + hass, + data, + name, + value_template, + unit_of_measurement, + str(sensorid), + elem, + ) + ) add_entities(sensors) class EmonCmsSensor(Entity): """Implementation of an Emoncms sensor.""" - def __init__(self, hass, data, name, value_template, - unit_of_measurement, sensorid, elem): + def __init__( + self, hass, data, name, value_template, unit_of_measurement, sensorid, elem + ): """Initialize the sensor.""" if name is None: # Suppress ID in sensor name if it's 1, since most people won't # have more than one EmonCMS source and it's redundant to show the # ID if there's only one. - id_for_name = '' if str(sensorid) == '1' else sensorid + id_for_name = "" if str(sensorid) == "1" else sensorid # Use the feed name assigned in EmonCMS or fall back to the feed ID - feed_name = elem.get('name') or 'Feed {}'.format(elem['id']) + feed_name = elem.get("name") or "Feed {}".format(elem["id"]) self._name = "EmonCMS{} {}".format(id_for_name, feed_name) else: self._name = name self._identifier = get_id( - sensorid, elem["tag"], elem["name"], elem["id"], elem["userid"]) + sensorid, elem["tag"], elem["name"], elem["id"], elem["userid"] + ) self._hass = hass self._data = data self._value_template = value_template @@ -127,7 +150,8 @@ class EmonCmsSensor(Entity): if self._value_template is not None: self._state = self._value_template.render_with_possible_json_value( - elem["value"], STATE_UNKNOWN) + elem["value"], STATE_UNKNOWN + ) else: self._state = round(float(elem["value"]), DECIMALS) @@ -156,8 +180,7 @@ class EmonCmsSensor(Entity): ATTR_SIZE: self._elem["size"], ATTR_USERID: self._elem["userid"], ATTR_LASTUPDATETIME: self._elem["time"], - ATTR_LASTUPDATETIMESTR: template.timestamp_local( - float(self._elem["time"])), + ATTR_LASTUPDATETIMESTR: template.timestamp_local(float(self._elem["time"])), } def update(self): @@ -167,11 +190,21 @@ class EmonCmsSensor(Entity): if self._data.data is None: return - elem = next((elem for elem in self._data.data - if get_id(self._sensorid, elem["tag"], - elem["name"], elem["id"], - elem["userid"]) == self._identifier), - None) + elem = next( + ( + elem + for elem in self._data.data + if get_id( + self._sensorid, + elem["tag"], + elem["name"], + elem["id"], + elem["userid"], + ) + == self._identifier + ), + None, + ) if elem is None: return @@ -180,7 +213,8 @@ class EmonCmsSensor(Entity): if self._value_template is not None: self._state = self._value_template.render_with_possible_json_value( - elem["value"], STATE_UNKNOWN) + elem["value"], STATE_UNKNOWN + ) else: self._state = round(float(elem["value"]), DECIMALS) @@ -191,7 +225,7 @@ class EmonCmsData: def __init__(self, hass, url, apikey, interval): """Initialize the data object.""" self._apikey = apikey - self._url = '{}/feed/list.json'.format(url) + self._url = "{}/feed/list.json".format(url) self._interval = interval self._hass = hass self.data = None @@ -202,7 +236,8 @@ class EmonCmsData: try: parameters = {"apikey": self._apikey} req = requests.get( - self._url, params=parameters, allow_redirects=True, timeout=5) + self._url, params=parameters, allow_redirects=True, timeout=5 + ) except requests.exceptions.RequestException as exception: _LOGGER.error(exception) return @@ -210,6 +245,9 @@ class EmonCmsData: if req.status_code == 200: self.data = req.json() else: - _LOGGER.error("Please verify if the specified config value " - "'%s' is correct! (HTTP Status_code = %d)", - CONF_URL, req.status_code) + _LOGGER.error( + "Please verify if the specified config value " + "'%s' is correct! (HTTP Status_code = %d)", + CONF_URL, + req.status_code, + ) diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index 45fb358cecc..779a25872f9 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -7,26 +7,36 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_API_KEY, CONF_WHITELIST, CONF_URL, STATE_UNKNOWN, STATE_UNAVAILABLE, - CONF_SCAN_INTERVAL) + CONF_API_KEY, + CONF_WHITELIST, + CONF_URL, + STATE_UNKNOWN, + STATE_UNAVAILABLE, + CONF_SCAN_INTERVAL, +) from homeassistant.helpers import state as state_helper from homeassistant.helpers.event import track_point_in_time from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) -DOMAIN = 'emoncms_history' -CONF_INPUTNODE = 'inputnode' +DOMAIN = "emoncms_history" +CONF_INPUTNODE = "inputnode" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_URL): cv.string, - vol.Required(CONF_INPUTNODE): cv.positive_int, - vol.Required(CONF_WHITELIST): cv.entity_ids, - vol.Optional(CONF_SCAN_INTERVAL, default=30): cv.positive_int, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_URL): cv.string, + vol.Required(CONF_INPUTNODE): cv.positive_int, + vol.Required(CONF_WHITELIST): cv.entity_ids, + vol.Optional(CONF_SCAN_INTERVAL, default=30): cv.positive_int, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -37,12 +47,12 @@ def setup(hass, config): def send_data(url, apikey, node, payload): """Send payload data to Emoncms.""" try: - fullurl = '{}/input/post.json'.format(url) + fullurl = "{}/input/post.json".format(url) data = {"apikey": apikey, "data": payload} parameters = {"node": node} req = requests.post( - fullurl, params=parameters, data=data, allow_redirects=True, - timeout=5) + fullurl, params=parameters, data=data, allow_redirects=True, timeout=5 + ) except requests.exceptions.RequestException: _LOGGER.error("Error saving data '%s' to '%s'", payload, fullurl) @@ -51,7 +61,10 @@ def setup(hass, config): if req.status_code != 200: _LOGGER.error( "Error saving data %s to %s (http status code = %d)", - payload, fullurl, req.status_code) + payload, + fullurl, + req.status_code, + ) def update_emoncms(time): """Send whitelisted entities states regularly to Emoncms.""" @@ -60,8 +73,7 @@ def setup(hass, config): for entity_id in whitelist: state = hass.states.get(entity_id) - if state is None or state.state in ( - STATE_UNKNOWN, '', STATE_UNAVAILABLE): + if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE): continue try: @@ -70,15 +82,20 @@ def setup(hass, config): continue if payload_dict: - payload = "{%s}" % ",".join("{}:{}".format(key, val) - for key, val in - payload_dict.items()) + payload = "{%s}" % ",".join( + "{}:{}".format(key, val) for key, val in payload_dict.items() + ) - send_data(conf.get(CONF_URL), conf.get(CONF_API_KEY), - str(conf.get(CONF_INPUTNODE)), payload) + send_data( + conf.get(CONF_URL), + conf.get(CONF_API_KEY), + str(conf.get(CONF_INPUTNODE)), + payload, + ) - track_point_in_time(hass, update_emoncms, time + - timedelta(seconds=conf.get(CONF_SCAN_INTERVAL))) + track_point_in_time( + hass, update_emoncms, time + timedelta(seconds=conf.get(CONF_SCAN_INTERVAL)) + ) update_emoncms(dt_util.utcnow()) return True diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 2ef0aaca134..791085b46f3 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -1,13 +1,11 @@ -"""Support for local control of entities by emulating a Phillips Hue bridge.""" +"""Support for local control of entities by emulating a Philips Hue bridge.""" import logging from aiohttp import web import voluptuous as vol from homeassistant import util -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.deprecation import get_deprecated import homeassistant.helpers.config_validation as cv @@ -15,66 +13,85 @@ from homeassistant.util.json import load_json, save_json from homeassistant.components.http import real_ip from .hue_api import ( - HueUsernameView, HueAllLightsStateView, HueOneLightStateView, - HueOneLightChangeView, HueGroupView, HueAllGroupsStateView) + HueUsernameView, + HueAllLightsStateView, + HueOneLightStateView, + HueOneLightChangeView, + HueGroupView, + HueAllGroupsStateView, +) from .upnp import DescriptionXmlView, UPNPResponderThread -DOMAIN = 'emulated_hue' +DOMAIN = "emulated_hue" _LOGGER = logging.getLogger(__name__) -NUMBERS_FILE = 'emulated_hue_ids.json' +NUMBERS_FILE = "emulated_hue_ids.json" -CONF_ADVERTISE_IP = 'advertise_ip' -CONF_ADVERTISE_PORT = 'advertise_port' -CONF_ENTITIES = 'entities' -CONF_ENTITY_HIDDEN = 'hidden' -CONF_ENTITY_NAME = 'name' -CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' -CONF_EXPOSED_DOMAINS = 'exposed_domains' -CONF_HOST_IP = 'host_ip' -CONF_LISTEN_PORT = 'listen_port' -CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains' -CONF_TYPE = 'type' -CONF_UPNP_BIND_MULTICAST = 'upnp_bind_multicast' +CONF_ADVERTISE_IP = "advertise_ip" +CONF_ADVERTISE_PORT = "advertise_port" +CONF_ENTITIES = "entities" +CONF_ENTITY_HIDDEN = "hidden" +CONF_ENTITY_NAME = "name" +CONF_EXPOSE_BY_DEFAULT = "expose_by_default" +CONF_EXPOSED_DOMAINS = "exposed_domains" +CONF_HOST_IP = "host_ip" +CONF_LISTEN_PORT = "listen_port" +CONF_OFF_MAPS_TO_ON_DOMAINS = "off_maps_to_on_domains" +CONF_TYPE = "type" +CONF_UPNP_BIND_MULTICAST = "upnp_bind_multicast" -TYPE_ALEXA = 'alexa' -TYPE_GOOGLE = 'google_home' +TYPE_ALEXA = "alexa" +TYPE_GOOGLE = "google_home" DEFAULT_LISTEN_PORT = 8300 DEFAULT_UPNP_BIND_MULTICAST = True -DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene'] +DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ["script", "scene"] DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ - 'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan' + "switch", + "light", + "group", + "input_boolean", + "media_player", + "fan", ] DEFAULT_TYPE = TYPE_GOOGLE -CONFIG_ENTITY_SCHEMA = vol.Schema({ - vol.Optional(CONF_ENTITY_NAME): cv.string, - vol.Optional(CONF_ENTITY_HIDDEN): cv.boolean -}) +CONFIG_ENTITY_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ENTITY_NAME): cv.string, + vol.Optional(CONF_ENTITY_HIDDEN): cv.boolean, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_HOST_IP): cv.string, - vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): cv.port, - vol.Optional(CONF_ADVERTISE_IP): cv.string, - vol.Optional(CONF_ADVERTISE_PORT): cv.port, - vol.Optional(CONF_UPNP_BIND_MULTICAST): cv.boolean, - vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list, - vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, - vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list, - vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): - vol.Any(TYPE_ALEXA, TYPE_GOOGLE), - vol.Optional(CONF_ENTITIES): - vol.Schema({cv.entity_id: CONFIG_ENTITY_SCHEMA}) - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_HOST_IP): cv.string, + vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): cv.port, + vol.Optional(CONF_ADVERTISE_IP): cv.string, + vol.Optional(CONF_ADVERTISE_PORT): cv.port, + vol.Optional(CONF_UPNP_BIND_MULTICAST): cv.boolean, + vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list, + vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, + vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list, + vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): vol.Any( + TYPE_ALEXA, TYPE_GOOGLE + ), + vol.Optional(CONF_ENTITIES): vol.Schema( + {cv.entity_id: CONFIG_ENTITY_SCHEMA} + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -ATTR_EMULATED_HUE = 'emulated_hue' -ATTR_EMULATED_HUE_NAME = 'emulated_hue_name' -ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden' +ATTR_EMULATED_HUE = "emulated_hue" +ATTR_EMULATED_HUE_NAME = "emulated_hue_name" +ATTR_EMULATED_HUE_HIDDEN = "emulated_hue_hidden" async def async_setup(hass, yaml_config): @@ -82,7 +99,7 @@ async def async_setup(hass, yaml_config): config = Config(hass, yaml_config.get(DOMAIN, {})) app = web.Application() - app['hass'] = hass + app["hass"] = hass real_ip.setup_real_ip(app, False, []) # We misunderstood the startup signal. You're not allowed to change @@ -103,9 +120,12 @@ async def async_setup(hass, yaml_config): HueGroupView(config).register(app, app.router) upnp_listener = UPNPResponderThread( - config.host_ip_addr, config.listen_port, - config.upnp_bind_multicast, config.advertise_ip, - config.advertise_port) + config.host_ip_addr, + config.listen_port, + config.upnp_bind_multicast, + config.advertise_ip, + config.advertise_port, + ) async def stop_emulated_hue_bridge(event): """Stop the emulated hue bridge.""" @@ -129,14 +149,15 @@ async def async_setup(hass, yaml_config): try: await site.start() except OSError as error: - _LOGGER.error("Failed to create HTTP server at port %d: %s", - config.listen_port, error) + _LOGGER.error( + "Failed to create HTTP server at port %d: %s", config.listen_port, error + ) else: hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) + EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge + ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, - start_emulated_hue_bridge) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) return True @@ -153,8 +174,9 @@ class Config: if self.type == TYPE_ALEXA: _LOGGER.warning( - 'Emulated Hue running in legacy mode because type has been ' - 'specified. More info at https://goo.gl/M6tgz8') + "Emulated Hue running in legacy mode because type has been " + "specified. More info at https://goo.gl/M6tgz8" + ) # Get the IP address that will be passed to the Echo during discovery self.host_ip_addr = conf.get(CONF_HOST_IP) @@ -162,20 +184,22 @@ class Config: self.host_ip_addr = util.get_local_ip() _LOGGER.info( "Listen IP address not specified, auto-detected address is %s", - self.host_ip_addr) + self.host_ip_addr, + ) # Get the port that the Hue bridge will listen on self.listen_port = conf.get(CONF_LISTEN_PORT) if not isinstance(self.listen_port, int): self.listen_port = DEFAULT_LISTEN_PORT _LOGGER.info( - "Listen port not specified, defaulting to %s", - self.listen_port) + "Listen port not specified, defaulting to %s", self.listen_port + ) # Get whether or not UPNP binds to multicast address (239.255.255.250) # or to the unicast address (host_ip_addr) self.upnp_bind_multicast = conf.get( - CONF_UPNP_BIND_MULTICAST, DEFAULT_UPNP_BIND_MULTICAST) + CONF_UPNP_BIND_MULTICAST, DEFAULT_UPNP_BIND_MULTICAST + ) # Get domains that cause both "on" and "off" commands to map to "on" # This is primarily useful for things like scenes or scripts, which @@ -187,19 +211,17 @@ class Config: # Get whether or not entities should be exposed by default, or if only # explicitly marked ones will be exposed self.expose_by_default = conf.get( - CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT) + CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT + ) # Get domains that are exposed by default when expose_by_default is # True - self.exposed_domains = conf.get( - CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS) + self.exposed_domains = conf.get(CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS) # Calculated effective advertised IP and port for network isolation - self.advertise_ip = conf.get( - CONF_ADVERTISE_IP) or self.host_ip_addr + self.advertise_ip = conf.get(CONF_ADVERTISE_IP) or self.host_ip_addr - self.advertise_port = conf.get( - CONF_ADVERTISE_PORT) or self.listen_port + self.advertise_port = conf.get(CONF_ADVERTISE_PORT) or self.listen_port self.entities = conf.get(CONF_ENTITIES, {}) @@ -216,7 +238,7 @@ class Config: if entity_id == ent_id: return number - number = '1' + number = "1" if self.numbers: number = str(max(int(k) for k in self.numbers) + 1) self.numbers[number] = entity_id @@ -237,8 +259,10 @@ class Config: def get_entity_name(self, entity): """Get the name of an entity.""" - if entity.entity_id in self.entities and \ - CONF_ENTITY_NAME in self.entities[entity.entity_id]: + if ( + entity.entity_id in self.entities + and CONF_ENTITY_NAME in self.entities[entity.entity_id] + ): return self.entities[entity.entity_id][CONF_ENTITY_NAME] return entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name) @@ -248,7 +272,7 @@ class Config: Async friendly. """ - if entity.attributes.get('view') is not None: + if entity.attributes.get("view") is not None: # Ignore entities that are views return False @@ -256,10 +280,11 @@ class Config: explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None) explicit_hidden = entity.attributes.get(ATTR_EMULATED_HUE_HIDDEN, None) - if entity.entity_id in self.entities and \ - CONF_ENTITY_HIDDEN in self.entities[entity.entity_id]: - explicit_hidden = \ - self.entities[entity.entity_id][CONF_ENTITY_HIDDEN] + if ( + entity.entity_id in self.entities + and CONF_ENTITY_HIDDEN in self.entities[entity.entity_id] + ): + explicit_hidden = self.entities[entity.entity_id][CONF_ENTITY_HIDDEN] if explicit_expose is True or explicit_hidden is False: expose = True @@ -267,16 +292,17 @@ class Config: expose = False else: expose = None - get_deprecated(entity.attributes, ATTR_EMULATED_HUE_HIDDEN, - ATTR_EMULATED_HUE, None) - domain_exposed_by_default = \ + get_deprecated( + entity.attributes, ATTR_EMULATED_HUE_HIDDEN, ATTR_EMULATED_HUE, None + ) + domain_exposed_by_default = ( self.expose_by_default and domain in self.exposed_domains + ) # Expose an entity if the entity's domain is exposed by default and # the configuration doesn't explicitly exclude it from being # exposed, or if the entity is explicitly exposed - is_default_exposed = \ - domain_exposed_by_default and expose is not False + is_default_exposed = domain_exposed_by_default and expose is not False return is_default_exposed or expose diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 632fdab12a4..6d59d777e8b 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -5,33 +5,66 @@ from aiohttp import web from homeassistant import core from homeassistant.components import ( - climate, cover, fan, light, media_player, scene, script) + climate, + cover, + fan, + light, + media_player, + scene, + script, +) from homeassistant.components.climate.const import ( - SERVICE_SET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE) + SERVICE_SET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE, +) from homeassistant.components.cover import ( - ATTR_CURRENT_POSITION, ATTR_POSITION, SERVICE_SET_COVER_POSITION, - SUPPORT_SET_POSITION) + ATTR_CURRENT_POSITION, + ATTR_POSITION, + SERVICE_SET_COVER_POSITION, + SUPPORT_SET_POSITION, +) from homeassistant.components.fan import ( - ATTR_SPEED, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, - SUPPORT_SET_SPEED) + ATTR_SPEED, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_SET_SPEED, +) from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR) + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, +) from homeassistant.components.media_player.const import ( - ATTR_MEDIA_VOLUME_LEVEL, SUPPORT_VOLUME_SET) + ATTR_MEDIA_VOLUME_LEVEL, + SUPPORT_VOLUME_SET, +) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, - HTTP_BAD_REQUEST, HTTP_NOT_FOUND, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, - SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET, STATE_OFF, STATE_ON) + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + HTTP_BAD_REQUEST, + HTTP_NOT_FOUND, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_SET, + STATE_OFF, + STATE_ON, +) from homeassistant.util.network import is_local _LOGGER = logging.getLogger(__name__) -HUE_API_STATE_ON = 'on' -HUE_API_STATE_BRI = 'bri' -HUE_API_STATE_HUE = 'hue' -HUE_API_STATE_SAT = 'sat' +HUE_API_STATE_ON = "on" +HUE_API_STATE_BRI = "bri" +HUE_API_STATE_HUE = "hue" +HUE_API_STATE_SAT = "sat" HUE_API_STATE_HUE_MAX = 65535.0 HUE_API_STATE_SAT_MAX = 254.0 @@ -45,9 +78,9 @@ STATE_SATURATION = HUE_API_STATE_SAT class HueUsernameView(HomeAssistantView): """Handle requests to create a username for the emulated hue bridge.""" - url = '/api' - name = 'emulated_hue:api:create_username' - extra_urls = ['/api/'] + url = "/api" + name = "emulated_hue:api:create_username" + extra_urls = ["/api/"] requires_auth = False async def post(self, request): @@ -55,24 +88,22 @@ class HueUsernameView(HomeAssistantView): try: data = await request.json() except ValueError: - return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) - if 'devicetype' not in data: - return self.json_message('devicetype not specified', - HTTP_BAD_REQUEST) + if "devicetype" not in data: + return self.json_message("devicetype not specified", HTTP_BAD_REQUEST) if not is_local(request[KEY_REAL_IP]): - return self.json_message('only local IPs allowed', - HTTP_BAD_REQUEST) + return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) - return self.json([{'success': {'username': '12345678901234567890'}}]) + return self.json([{"success": {"username": "12345678901234567890"}}]) class HueAllGroupsStateView(HomeAssistantView): """Group handler.""" - url = '/api/{username}/groups' - name = 'emulated_hue:all_groups:state' + url = "/api/{username}/groups" + name = "emulated_hue:all_groups:state" requires_auth = False def __init__(self, config): @@ -83,18 +114,16 @@ class HueAllGroupsStateView(HomeAssistantView): def get(self, request, username): """Process a request to make the Brilliant Lightpad work.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message('only local IPs allowed', - HTTP_BAD_REQUEST) + return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) - return self.json({ - }) + return self.json({}) class HueGroupView(HomeAssistantView): """Group handler to get Logitech Pop working.""" - url = '/api/{username}/groups/0/action' - name = 'emulated_hue:groups:state' + url = "/api/{username}/groups/0/action" + name = "emulated_hue:groups:state" requires_auth = False def __init__(self, config): @@ -105,23 +134,26 @@ class HueGroupView(HomeAssistantView): def put(self, request, username): """Process a request to make the Logitech Pop working.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message('only local IPs allowed', - HTTP_BAD_REQUEST) + return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) - return self.json([{ - 'error': { - 'address': '/groups/0/action/scene', - 'type': 7, - 'description': 'invalid value, dummy for parameter, scene' - } - }]) + return self.json( + [ + { + "error": { + "address": "/groups/0/action/scene", + "type": 7, + "description": "invalid value, dummy for parameter, scene", + } + } + ] + ) class HueAllLightsStateView(HomeAssistantView): """Handle requests for getting and setting info about entities.""" - url = '/api/{username}/lights' - name = 'emulated_hue:lights:state' + url = "/api/{username}/lights" + name = "emulated_hue:lights:state" requires_auth = False def __init__(self, config): @@ -132,10 +164,9 @@ class HueAllLightsStateView(HomeAssistantView): def get(self, request, username): """Process a request to get the list of available lights.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message('only local IPs allowed', - HTTP_BAD_REQUEST) + return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) - hass = request.app['hass'] + hass = request.app["hass"] json_response = {} for entity in hass.states.async_all(): @@ -143,8 +174,7 @@ class HueAllLightsStateView(HomeAssistantView): state = get_entity_state(self.config, entity) number = self.config.entity_id_to_number(entity.entity_id) - json_response[number] = entity_to_json(self.config, - entity, state) + json_response[number] = entity_to_json(self.config, entity, state) return self.json(json_response) @@ -152,8 +182,8 @@ class HueAllLightsStateView(HomeAssistantView): class HueOneLightStateView(HomeAssistantView): """Handle requests for getting and setting info about entities.""" - url = '/api/{username}/lights/{entity_id}' - name = 'emulated_hue:light:state' + url = "/api/{username}/lights/{entity_id}" + name = "emulated_hue:light:state" requires_auth = False def __init__(self, config): @@ -164,19 +194,18 @@ class HueOneLightStateView(HomeAssistantView): def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message('only local IPs allowed', - HTTP_BAD_REQUEST) + return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) - hass = request.app['hass'] + hass = request.app["hass"] entity_id = self.config.number_to_entity_id(entity_id) entity = hass.states.get(entity_id) if entity is None: - _LOGGER.error('Entity not found: %s', entity_id) + _LOGGER.error("Entity not found: %s", entity_id) return web.Response(text="Entity not found", status=404) if not self.config.is_entity_exposed(entity): - _LOGGER.error('Entity not exposed: %s', entity_id) + _LOGGER.error("Entity not exposed: %s", entity_id) return web.Response(text="Entity not exposed", status=404) state = get_entity_state(self.config, entity) @@ -189,8 +218,8 @@ class HueOneLightStateView(HomeAssistantView): class HueOneLightChangeView(HomeAssistantView): """Handle requests for getting and setting info about entities.""" - url = '/api/{username}/lights/{entity_number}/state' - name = 'emulated_hue:light:state' + url = "/api/{username}/lights/{entity_number}/state" + name = "emulated_hue:light:state" requires_auth = False def __init__(self, config): @@ -200,38 +229,37 @@ class HueOneLightChangeView(HomeAssistantView): async def put(self, request, username, entity_number): """Process a request to set the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message('only local IPs allowed', - HTTP_BAD_REQUEST) + return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) config = self.config - hass = request.app['hass'] + hass = request.app["hass"] entity_id = config.number_to_entity_id(entity_number) if entity_id is None: - _LOGGER.error('Unknown entity number: %s', entity_number) - return self.json_message('Entity not found', HTTP_NOT_FOUND) + _LOGGER.error("Unknown entity number: %s", entity_number) + return self.json_message("Entity not found", HTTP_NOT_FOUND) entity = hass.states.get(entity_id) if entity is None: - _LOGGER.error('Entity not found: %s', entity_id) - return self.json_message('Entity not found', HTTP_NOT_FOUND) + _LOGGER.error("Entity not found: %s", entity_id) + return self.json_message("Entity not found", HTTP_NOT_FOUND) if not config.is_entity_exposed(entity): - _LOGGER.error('Entity not exposed: %s', entity_id) + _LOGGER.error("Entity not exposed: %s", entity_id) return web.Response(text="Entity not exposed", status=404) try: request_json = await request.json() except ValueError: - _LOGGER.error('Received invalid json') - return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + _LOGGER.error("Received invalid json") + return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) # Parse the request into requested "on" status and brightness parsed = parse_hue_api_put_light_body(request_json, entity) if parsed is None: - _LOGGER.error('Unable to parse data: %s', request_json) + _LOGGER.error("Unable to parse data: %s", request_json) return web.Response(text="Bad request", status=400) # Choose general HA domain @@ -270,12 +298,12 @@ class HueOneLightChangeView(HomeAssistantView): # If the requested entity is a script add some variables elif entity.domain == script.DOMAIN: - data['variables'] = { - 'requested_state': STATE_ON if parsed[STATE_ON] else STATE_OFF + data["variables"] = { + "requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF } if parsed[STATE_BRIGHTNESS] is not None: - data['variables']['requested_level'] = parsed[STATE_BRIGHTNESS] + data["variables"]["requested_level"] = parsed[STATE_BRIGHTNESS] # If the requested entity is a climate, set the temperature elif entity.domain == climate.DOMAIN: @@ -297,8 +325,7 @@ class HueOneLightChangeView(HomeAssistantView): domain = entity.domain service = SERVICE_VOLUME_SET # Convert 0-100 to 0.0-1.0 - data[ATTR_MEDIA_VOLUME_LEVEL] = \ - parsed[STATE_BRIGHTNESS] / 100.0 + data[ATTR_MEDIA_VOLUME_LEVEL] = parsed[STATE_BRIGHTNESS] / 100.0 # If the requested entity is a cover, convert to open_cover/close_cover elif entity.domain == cover.DOMAIN: @@ -343,27 +370,42 @@ class HueOneLightChangeView(HomeAssistantView): # Separate call to turn on needed if turn_on_needed: - hass.async_create_task(hass.services.async_call( - core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, - blocking=True)) + hass.async_create_task( + hass.services.async_call( + core.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + ) if service is not None: - hass.async_create_task(hass.services.async_call( - domain, service, data, blocking=True)) + hass.async_create_task( + hass.services.async_call(domain, service, data, blocking=True) + ) - json_response = \ - [create_hue_success_response( - entity_id, HUE_API_STATE_ON, parsed[STATE_ON])] + json_response = [ + create_hue_success_response(entity_id, HUE_API_STATE_ON, parsed[STATE_ON]) + ] if parsed[STATE_BRIGHTNESS] is not None: - json_response.append(create_hue_success_response( - entity_id, HUE_API_STATE_BRI, parsed[STATE_BRIGHTNESS])) + json_response.append( + create_hue_success_response( + entity_id, HUE_API_STATE_BRI, parsed[STATE_BRIGHTNESS] + ) + ) if parsed[STATE_HUE] is not None: - json_response.append(create_hue_success_response( - entity_id, HUE_API_STATE_HUE, parsed[STATE_HUE])) + json_response.append( + create_hue_success_response( + entity_id, HUE_API_STATE_HUE, parsed[STATE_HUE] + ) + ) if parsed[STATE_SATURATION] is not None: - json_response.append(create_hue_success_response( - entity_id, HUE_API_STATE_SAT, parsed[STATE_SATURATION])) + json_response.append( + create_hue_success_response( + entity_id, HUE_API_STATE_SAT, parsed[STATE_SATURATION] + ) + ) return self.json(json_response) @@ -396,32 +438,32 @@ def parse_hue_api_put_light_body(request_json, entity): if HUE_API_STATE_HUE in request_json: try: # Clamp brightness from 0 to 65535 - data[STATE_HUE] = \ - max(0, min(int(request_json[HUE_API_STATE_HUE]), - HUE_API_STATE_HUE_MAX)) + data[STATE_HUE] = max( + 0, min(int(request_json[HUE_API_STATE_HUE]), HUE_API_STATE_HUE_MAX) + ) except ValueError: return None if HUE_API_STATE_SAT in request_json: try: # Clamp saturation from 0 to 254 - data[STATE_SATURATION] = \ - max(0, min(int(request_json[HUE_API_STATE_SAT]), - HUE_API_STATE_SAT_MAX)) + data[STATE_SATURATION] = max( + 0, min(int(request_json[HUE_API_STATE_SAT]), HUE_API_STATE_SAT_MAX) + ) except ValueError: return None if HUE_API_STATE_BRI in request_json: try: # Clamp brightness from 0 to 255 - data[STATE_BRIGHTNESS] = \ - max(0, min(int(request_json[HUE_API_STATE_BRI]), - HUE_API_STATE_BRI_MAX)) + data[STATE_BRIGHTNESS] = max( + 0, min(int(request_json[HUE_API_STATE_BRI]), HUE_API_STATE_BRI_MAX) + ) except ValueError: return None if entity.domain == light.DOMAIN: - data[STATE_ON] = (data[STATE_BRIGHTNESS] > 0) + data[STATE_ON] = data[STATE_BRIGHTNESS] > 0 if not entity_features & SUPPORT_BRIGHTNESS: data[STATE_BRIGHTNESS] = None @@ -430,8 +472,12 @@ def parse_hue_api_put_light_body(request_json, entity): data[STATE_ON] = True elif entity.domain in [ - script.DOMAIN, media_player.DOMAIN, - fan.DOMAIN, cover.DOMAIN, climate.DOMAIN]: + script.DOMAIN, + media_player.DOMAIN, + fan.DOMAIN, + cover.DOMAIN, + climate.DOMAIN, + ]: # Convert 0-255 to 0-100 level = (data[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 data[STATE_BRIGHTNESS] = round(level) @@ -447,7 +493,7 @@ def get_entity_state(config, entity): STATE_BRIGHTNESS: None, STATE_HUE: None, STATE_ON: False, - STATE_SATURATION: None + STATE_SATURATION: None, } if cached_state is None: @@ -460,8 +506,7 @@ def get_entity_state(config, entity): sat = hue_sat[1] # convert hass hs values back to hue hs values data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX) - data[STATE_SATURATION] = \ - int((sat / 100.0) * HUE_API_STATE_SAT_MAX) + data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX) else: data[STATE_BRIGHTNESS] = 0 data[STATE_HUE] = 0 @@ -480,10 +525,10 @@ def get_entity_state(config, entity): data[STATE_BRIGHTNESS] = round(temperature * 255 / 100) elif entity.domain == media_player.DOMAIN: level = entity.attributes.get( - ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0) + ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0 + ) # Convert 0.0-1.0 to 0-255 - data[STATE_BRIGHTNESS] = \ - round(min(1.0, level) * HUE_API_STATE_BRI_MAX) + data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX) elif entity.domain == fan.DOMAIN: speed = entity.attributes.get(ATTR_SPEED, 0) # Convert 0.0-1.0 to 0-255 @@ -517,24 +562,33 @@ def get_entity_state(config, entity): def entity_to_json(config, entity, state): """Convert an entity to its Hue bridge JSON representation.""" + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if entity_features & SUPPORT_BRIGHTNESS: + return { + "state": { + HUE_API_STATE_ON: state[STATE_ON], + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + "reachable": True, + }, + "type": "Dimmable light", + "name": config.get_entity_name(entity), + "modelid": "HASS123", + "uniqueid": entity.entity_id, + "swversion": "123", + } return { - 'state': - { - HUE_API_STATE_ON: state[STATE_ON], - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], - 'reachable': True - }, - 'type': 'Dimmable light', - 'name': config.get_entity_name(entity), - 'modelid': 'HASS123', - 'uniqueid': entity.entity_id, - 'swversion': '123' + "state": {HUE_API_STATE_ON: state[STATE_ON], "reachable": True}, + "type": "On/off light", + "name": config.get_entity_name(entity), + "modelid": "HASS123", + "uniqueid": entity.entity_id, + "swversion": "123", } def create_hue_success_response(entity_id, attr, value): """Create a success response for an attribute set on a light.""" - success_key = '/lights/{}/state/{}'.format(entity_id, attr) - return {'success': {success_key: value}} + success_key = "/lights/{}/state/{}".format(entity_id, attr) + return {"success": {success_key: value}} diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index a163d4b2e91..412dfdd673e 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -15,8 +15,8 @@ _LOGGER = logging.getLogger(__name__) class DescriptionXmlView(HomeAssistantView): """Handles requests for the description.xml file.""" - url = '/description.xml' - name = 'description:xml' + url = "/description.xml" + name = "description:xml" requires_auth = False def __init__(self, config): @@ -49,9 +49,10 @@ class DescriptionXmlView(HomeAssistantView): """ resp_text = xml_template.format( - self.config.advertise_ip, self.config.advertise_port) + self.config.advertise_ip, self.config.advertise_port + ) - return web.Response(text=resp_text, content_type='text/xml') + return web.Response(text=resp_text, content_type="text/xml") class UPNPResponderThread(threading.Thread): @@ -59,8 +60,14 @@ class UPNPResponderThread(threading.Thread): _interrupted = False - def __init__(self, host_ip_addr, listen_port, upnp_bind_multicast, - advertise_ip, advertise_port): + def __init__( + self, + host_ip_addr, + listen_port, + upnp_bind_multicast, + advertise_ip, + advertise_port, + ): """Initialize the class.""" threading.Thread.__init__(self) @@ -81,9 +88,11 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 """ - self.upnp_response = resp_template.format( - advertise_ip, advertise_port).replace("\n", "\r\n") \ - .encode('utf-8') + self.upnp_response = ( + resp_template.format(advertise_ip, advertise_port) + .replace("\n", "\r\n") + .encode("utf-8") + ) def run(self): """Run the server.""" @@ -95,15 +104,14 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) ssdp_socket.setsockopt( - socket.SOL_IP, - socket.IP_MULTICAST_IF, - socket.inet_aton(self.host_ip_addr)) + socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.host_ip_addr) + ) ssdp_socket.setsockopt( socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, - socket.inet_aton("239.255.255.250") + - socket.inet_aton(self.host_ip_addr)) + socket.inet_aton("239.255.255.250") + socket.inet_aton(self.host_ip_addr), + ) if self.upnp_bind_multicast: ssdp_socket.bind(("", 1900)) @@ -116,9 +124,7 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 return try: - read, _, _ = select.select( - [ssdp_socket], [], - [ssdp_socket], 2) + read, _, _ = select.select([ssdp_socket], [], [ssdp_socket], 2) if ssdp_socket in read: data, addr = ssdp_socket.recvfrom(1024) @@ -130,16 +136,16 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 clean_socket_close(ssdp_socket) return - _LOGGER.error("UPNP Responder socket exception occurred: %s", - ex.__str__) + _LOGGER.error( + "UPNP Responder socket exception occurred: %s", ex.__str__ + ) # without the following continue, a second exception occurs # because the data object has not been initialized continue - if "M-SEARCH" in data.decode('utf-8', errors='ignore'): + if "M-SEARCH" in data.decode("utf-8", errors="ignore"): # SSDP M-SEARCH method received, respond to it with our info - resp_socket = socket.socket( - socket.AF_INET, socket.SOCK_DGRAM) + resp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) resp_socket.sendto(self.upnp_response, addr) resp_socket.close() diff --git a/homeassistant/components/emulated_roku/.translations/bg.json b/homeassistant/components/emulated_roku/.translations/bg.json index 77a96095c25..ddb3c3a36a9 100644 --- a/homeassistant/components/emulated_roku/.translations/bg.json +++ b/homeassistant/components/emulated_roku/.translations/bg.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" + }, "step": { "user": { "data": { - "host_ip": "\u0410\u0434\u0440\u0435\u0441" - } + "advertise_ip": "\u0420\u0430\u0437\u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u044f\u0432\u0430\u0439 IP \u0430\u0434\u0440\u0435\u0441", + "advertise_port": "\u0420\u0430\u0437\u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u044f\u0432\u0430\u0439 \u043f\u043e\u0440\u0442", + "host_ip": "\u0410\u0434\u0440\u0435\u0441", + "listen_port": "\u0421\u043b\u0443\u0448\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442", + "name": "\u0418\u043c\u0435", + "upnp_bind_multicast": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043c\u0443\u043b\u0442\u0438\u043a\u0430\u0441\u0442 (True/False)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0430" } - } + }, + "title": "EmulatedRoku" } } \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/pt-BR.json b/homeassistant/components/emulated_roku/.translations/pt-BR.json new file mode 100644 index 00000000000..0f82f93b383 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "O nome j\u00e1 existe" + }, + "step": { + "user": { + "data": { + "advertise_ip": "Anunciar IP", + "advertise_port": "Anunciar porta", + "host_ip": "IP do host", + "listen_port": "Porta de escuta", + "name": "Nome", + "upnp_bind_multicast": "Vincular multicast (Verdadeiro/Falso)" + }, + "title": "Definir configura\u00e7\u00e3o do servidor" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/__init__.py b/homeassistant/components/emulated_roku/__init__.py index 72d4dff72db..4e577929644 100644 --- a/homeassistant/components/emulated_roku/__init__.py +++ b/homeassistant/components/emulated_roku/__init__.py @@ -8,24 +8,38 @@ import homeassistant.helpers.config_validation as cv from .binding import EmulatedRoku from .config_flow import configured_servers from .const import ( - CONF_ADVERTISE_IP, CONF_ADVERTISE_PORT, CONF_HOST_IP, CONF_LISTEN_PORT, - CONF_SERVERS, CONF_UPNP_BIND_MULTICAST, DOMAIN) + CONF_ADVERTISE_IP, + CONF_ADVERTISE_PORT, + CONF_HOST_IP, + CONF_LISTEN_PORT, + CONF_SERVERS, + CONF_UPNP_BIND_MULTICAST, + DOMAIN, +) -SERVER_CONFIG_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_LISTEN_PORT): cv.port, - vol.Optional(CONF_HOST_IP): cv.string, - vol.Optional(CONF_ADVERTISE_IP): cv.string, - vol.Optional(CONF_ADVERTISE_PORT): cv.port, - vol.Optional(CONF_UPNP_BIND_MULTICAST): cv.boolean -}) +SERVER_CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_LISTEN_PORT): cv.port, + vol.Optional(CONF_HOST_IP): cv.string, + vol.Optional(CONF_ADVERTISE_IP): cv.string, + vol.Optional(CONF_ADVERTISE_PORT): cv.port, + vol.Optional(CONF_UPNP_BIND_MULTICAST): cv.boolean, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_SERVERS): - vol.All(cv.ensure_list, [SERVER_CONFIG_SCHEMA]), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_SERVERS): vol.All( + cv.ensure_list, [SERVER_CONFIG_SCHEMA] + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -39,11 +53,11 @@ async def async_setup(hass, config): for entry in conf[CONF_SERVERS]: if entry[CONF_NAME] not in existing_servers: - hass.async_create_task(hass.config_entries.flow.async_init( - DOMAIN, - context={'source': config_entries.SOURCE_IMPORT}, - data=entry - )) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=entry + ) + ) return True @@ -62,8 +76,15 @@ async def async_setup_entry(hass, config_entry): advertise_port = config.get(CONF_ADVERTISE_PORT) upnp_bind_multicast = config.get(CONF_UPNP_BIND_MULTICAST) - server = EmulatedRoku(hass, name, host_ip, listen_port, - advertise_ip, advertise_port, upnp_bind_multicast) + server = EmulatedRoku( + hass, + name, + host_ip, + listen_port, + advertise_ip, + advertise_port, + upnp_bind_multicast, + ) hass.data[DOMAIN][name] = server diff --git a/homeassistant/components/emulated_roku/binding.py b/homeassistant/components/emulated_roku/binding.py index b6a6719a37b..4c98af69848 100644 --- a/homeassistant/components/emulated_roku/binding.py +++ b/homeassistant/components/emulated_roku/binding.py @@ -1,30 +1,37 @@ """Bridge between emulated_roku and Home Assistant.""" import logging -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, EventOrigin LOGGER = logging.getLogger(__package__) -EVENT_ROKU_COMMAND = 'roku_command' +EVENT_ROKU_COMMAND = "roku_command" -ATTR_COMMAND_TYPE = 'type' -ATTR_SOURCE_NAME = 'source_name' -ATTR_KEY = 'key' -ATTR_APP_ID = 'app_id' +ATTR_COMMAND_TYPE = "type" +ATTR_SOURCE_NAME = "source_name" +ATTR_KEY = "key" +ATTR_APP_ID = "app_id" -ROKU_COMMAND_KEYDOWN = 'keydown' -ROKU_COMMAND_KEYUP = 'keyup' -ROKU_COMMAND_KEYPRESS = 'keypress' -ROKU_COMMAND_LAUNCH = 'launch' +ROKU_COMMAND_KEYDOWN = "keydown" +ROKU_COMMAND_KEYUP = "keyup" +ROKU_COMMAND_KEYPRESS = "keypress" +ROKU_COMMAND_LAUNCH = "launch" class EmulatedRoku: """Manages an emulated_roku server.""" - def __init__(self, hass, name, host_ip, listen_port, - advertise_ip, advertise_port, upnp_bind_multicast): + def __init__( + self, + hass, + name, + host_ip, + listen_port, + advertise_ip, + advertise_port, + upnp_bind_multicast, + ): """Initialize the properties.""" self.hass = hass @@ -44,8 +51,7 @@ class EmulatedRoku: async def setup(self): """Start the emulated_roku server.""" - from emulated_roku import EmulatedRokuServer, \ - EmulatedRokuCommandHandler + from emulated_roku import EmulatedRokuServer, EmulatedRokuCommandHandler class EventCommandHandler(EmulatedRokuCommandHandler): """emulated_roku command handler to turn commands into events.""" @@ -55,47 +61,70 @@ class EmulatedRoku: def on_keydown(self, roku_usn, key): """Handle keydown event.""" - self.hass.bus.async_fire(EVENT_ROKU_COMMAND, { - ATTR_SOURCE_NAME: roku_usn, - ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYDOWN, - ATTR_KEY: key - }, EventOrigin.local) + self.hass.bus.async_fire( + EVENT_ROKU_COMMAND, + { + ATTR_SOURCE_NAME: roku_usn, + ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYDOWN, + ATTR_KEY: key, + }, + EventOrigin.local, + ) def on_keyup(self, roku_usn, key): """Handle keyup event.""" - self.hass.bus.async_fire(EVENT_ROKU_COMMAND, { - ATTR_SOURCE_NAME: roku_usn, - ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYUP, - ATTR_KEY: key - }, EventOrigin.local) + self.hass.bus.async_fire( + EVENT_ROKU_COMMAND, + { + ATTR_SOURCE_NAME: roku_usn, + ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYUP, + ATTR_KEY: key, + }, + EventOrigin.local, + ) def on_keypress(self, roku_usn, key): """Handle keypress event.""" - self.hass.bus.async_fire(EVENT_ROKU_COMMAND, { - ATTR_SOURCE_NAME: roku_usn, - ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYPRESS, - ATTR_KEY: key - }, EventOrigin.local) + self.hass.bus.async_fire( + EVENT_ROKU_COMMAND, + { + ATTR_SOURCE_NAME: roku_usn, + ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYPRESS, + ATTR_KEY: key, + }, + EventOrigin.local, + ) def launch(self, roku_usn, app_id): """Handle launch event.""" - self.hass.bus.async_fire(EVENT_ROKU_COMMAND, { - ATTR_SOURCE_NAME: roku_usn, - ATTR_COMMAND_TYPE: ROKU_COMMAND_LAUNCH, - ATTR_APP_ID: app_id - }, EventOrigin.local) + self.hass.bus.async_fire( + EVENT_ROKU_COMMAND, + { + ATTR_SOURCE_NAME: roku_usn, + ATTR_COMMAND_TYPE: ROKU_COMMAND_LAUNCH, + ATTR_APP_ID: app_id, + }, + EventOrigin.local, + ) - LOGGER.debug("Intializing emulated_roku %s on %s:%s", - self.roku_usn, self.host_ip, self.listen_port) + LOGGER.debug( + "Intializing emulated_roku %s on %s:%s", + self.roku_usn, + self.host_ip, + self.listen_port, + ) handler = EventCommandHandler(self.hass) self._api_server = EmulatedRokuServer( - self.hass.loop, handler, - self.roku_usn, self.host_ip, self.listen_port, + self.hass.loop, + handler, + self.roku_usn, + self.host_ip, + self.listen_port, advertise_ip=self.advertise_ip, advertise_port=self.advertise_port, - bind_multicast=self.bind_multicast + bind_multicast=self.bind_multicast, ) async def emulated_roku_stop(event): @@ -111,22 +140,26 @@ class EmulatedRoku: self._unsub_start_listener = None await self._api_server.start() except OSError: - LOGGER.exception("Failed to start Emulated Roku %s on %s:%s", - self.roku_usn, self.host_ip, self.listen_port) + LOGGER.exception( + "Failed to start Emulated Roku %s on %s:%s", + self.roku_usn, + self.host_ip, + self.listen_port, + ) # clean up inconsistent state on errors await emulated_roku_stop(None) else: self._unsub_stop_listener = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, - emulated_roku_stop) + EVENT_HOMEASSISTANT_STOP, emulated_roku_stop + ) # start immediately if already running if self.hass.state == CoreState.running: await emulated_roku_start(None) else: self._unsub_start_listener = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, - emulated_roku_start) + EVENT_HOMEASSISTANT_START, emulated_roku_start + ) return True diff --git a/homeassistant/components/emulated_roku/config_flow.py b/homeassistant/components/emulated_roku/config_flow.py index d08ad09f1c0..0a6d54693ef 100644 --- a/homeassistant/components/emulated_roku/config_flow.py +++ b/homeassistant/components/emulated_roku/config_flow.py @@ -11,8 +11,9 @@ from .const import CONF_LISTEN_PORT, DEFAULT_NAME, DEFAULT_PORT, DOMAIN @callback def configured_servers(hass): """Return a set of the configured servers.""" - return set(entry.data[CONF_NAME] for entry - in hass.config_entries.async_entries(DOMAIN)) + return set( + entry.data[CONF_NAME] for entry in hass.config_entries.async_entries(DOMAIN) + ) @config_entries.HANDLERS.register(DOMAIN) @@ -30,12 +31,9 @@ class EmulatedRokuFlowHandler(config_entries.ConfigFlow): name = user_input[CONF_NAME] if name in configured_servers(self.hass): - return self.async_abort(reason='name_exists') + return self.async_abort(reason="name_exists") - return self.async_create_entry( - title=name, - data=user_input - ) + return self.async_create_entry(title=name, data=user_input) servers_num = len(configured_servers(self.hass)) @@ -47,14 +45,16 @@ class EmulatedRokuFlowHandler(config_entries.ConfigFlow): default_port = DEFAULT_PORT return self.async_show_form( - step_id='user', - data_schema=vol.Schema({ - vol.Required(CONF_NAME, - default=default_name): str, - vol.Required(CONF_LISTEN_PORT, - default=default_port): vol.Coerce(int) - }), - errors=errors + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME, default=default_name): str, + vol.Required(CONF_LISTEN_PORT, default=default_port): vol.Coerce( + int + ), + } + ), + errors=errors, ) async def async_step_import(self, import_config): diff --git a/homeassistant/components/emulated_roku/const.py b/homeassistant/components/emulated_roku/const.py index 25ea3adaa84..6c780367188 100644 --- a/homeassistant/components/emulated_roku/const.py +++ b/homeassistant/components/emulated_roku/const.py @@ -1,12 +1,12 @@ """Constants for the emulated_roku component.""" -DOMAIN = 'emulated_roku' +DOMAIN = "emulated_roku" -CONF_SERVERS = 'servers' -CONF_LISTEN_PORT = 'listen_port' -CONF_HOST_IP = 'host_ip' -CONF_ADVERTISE_IP = 'advertise_ip' -CONF_ADVERTISE_PORT = 'advertise_port' -CONF_UPNP_BIND_MULTICAST = 'upnp_bind_multicast' +CONF_SERVERS = "servers" +CONF_LISTEN_PORT = "listen_port" +CONF_HOST_IP = "host_ip" +CONF_ADVERTISE_IP = "advertise_ip" +CONF_ADVERTISE_PORT = "advertise_port" +CONF_UPNP_BIND_MULTICAST = "upnp_bind_multicast" DEFAULT_NAME = "Home Assistant" DEFAULT_PORT = 8060 diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 4662c707637..5b0e705f392 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -4,57 +4,84 @@ import logging import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice -from homeassistant.helpers.config_validation import (PLATFORM_SCHEMA) +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, - SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_STEP, MEDIA_TYPE_TVSHOW) + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_ON, + SUPPORT_TURN_OFF, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_STOP, + SUPPORT_SELECT_SOURCE, + SUPPORT_VOLUME_STEP, + MEDIA_TYPE_TVSHOW, +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_SSL, - STATE_OFF, STATE_ON, STATE_PLAYING, CONF_PORT) + CONF_HOST, + CONF_NAME, + CONF_USERNAME, + CONF_PASSWORD, + CONF_SSL, + STATE_OFF, + STATE_ON, + STATE_PLAYING, + CONF_PORT, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_MEDIA_CURRENTLY_RECORDING = 'media_currently_recording' -ATTR_MEDIA_DESCRIPTION = 'media_description' -ATTR_MEDIA_END_TIME = 'media_end_time' -ATTR_MEDIA_START_TIME = 'media_start_time' +ATTR_MEDIA_CURRENTLY_RECORDING = "media_currently_recording" +ATTR_MEDIA_DESCRIPTION = "media_description" +ATTR_MEDIA_END_TIME = "media_end_time" +ATTR_MEDIA_START_TIME = "media_start_time" CONF_USE_CHANNEL_ICON = "use_channel_icon" CONF_DEEP_STANDBY = "deep_standby" CONF_MAC_ADDRESS = "mac_address" CONF_SOURCE_BOUQUET = "source_bouquet" -DEFAULT_NAME = 'Enigma2 Media Player' +DEFAULT_NAME = "Enigma2 Media Player" DEFAULT_PORT = 80 DEFAULT_SSL = False DEFAULT_USE_CHANNEL_ICON = False -DEFAULT_USERNAME = 'root' -DEFAULT_PASSWORD = 'dreambox' +DEFAULT_USERNAME = "root" +DEFAULT_PASSWORD = "dreambox" DEFAULT_DEEP_STANDBY = False -DEFAULT_MAC_ADDRESS = '' -DEFAULT_SOURCE_BOUQUET = '' +DEFAULT_MAC_ADDRESS = "" +DEFAULT_SOURCE_BOUQUET = "" -SUPPORTED_ENIGMA2 = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_OFF | SUPPORT_NEXT_TRACK | SUPPORT_STOP | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_VOLUME_STEP | \ - SUPPORT_TURN_ON | SUPPORT_PAUSE | SUPPORT_SELECT_SOURCE +SUPPORTED_ENIGMA2 = ( + SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + | SUPPORT_TURN_OFF + | SUPPORT_NEXT_TRACK + | SUPPORT_STOP + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_VOLUME_STEP + | SUPPORT_TURN_ON + | SUPPORT_PAUSE + | SUPPORT_SELECT_SOURCE +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_USE_CHANNEL_ICON, - default=DEFAULT_USE_CHANNEL_ICON): cv.boolean, - vol.Optional(CONF_DEEP_STANDBY, default=DEFAULT_DEEP_STANDBY): cv.boolean, - vol.Optional(CONF_MAC_ADDRESS, default=DEFAULT_MAC_ADDRESS): cv.string, - vol.Optional(CONF_SOURCE_BOUQUET, - default=DEFAULT_SOURCE_BOUQUET): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional( + CONF_USE_CHANNEL_ICON, default=DEFAULT_USE_CHANNEL_ICON + ): cv.boolean, + vol.Optional(CONF_DEEP_STANDBY, default=DEFAULT_DEEP_STANDBY): cv.boolean, + vol.Optional(CONF_MAC_ADDRESS, default=DEFAULT_MAC_ADDRESS): cv.string, + vol.Optional(CONF_SOURCE_BOUQUET, default=DEFAULT_SOURCE_BOUQUET): cv.string, + } +) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -64,8 +91,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # which is not useful as OpenWebif never runs on that port. # So use the default port instead. config[CONF_PORT] = DEFAULT_PORT - config[CONF_NAME] = discovery_info['hostname'] - config[CONF_HOST] = discovery_info['host'] + config[CONF_NAME] = discovery_info["hostname"] + config[CONF_HOST] = discovery_info["host"] config[CONF_USERNAME] = DEFAULT_USERNAME config[CONF_PASSWORD] = DEFAULT_PASSWORD config[CONF_SSL] = DEFAULT_SSL @@ -75,16 +102,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config[CONF_SOURCE_BOUQUET] = DEFAULT_SOURCE_BOUQUET from openwebif.api import CreateDevice - device = \ - CreateDevice(host=config[CONF_HOST], - port=config.get(CONF_PORT), - username=config.get(CONF_USERNAME), - password=config.get(CONF_PASSWORD), - is_https=config.get(CONF_SSL), - prefer_picon=config.get(CONF_USE_CHANNEL_ICON), - mac_address=config.get(CONF_MAC_ADDRESS), - turn_off_to_deep=config.get(CONF_DEEP_STANDBY), - source_bouquet=config.get(CONF_SOURCE_BOUQUET)) + + device = CreateDevice( + host=config[CONF_HOST], + port=config.get(CONF_PORT), + username=config.get(CONF_USERNAME), + password=config.get(CONF_PASSWORD), + is_https=config.get(CONF_SSL), + prefer_picon=config.get(CONF_USE_CHANNEL_ICON), + mac_address=config.get(CONF_MAC_ADDRESS), + turn_off_to_deep=config.get(CONF_DEEP_STANDBY), + source_bouquet=config.get(CONF_SOURCE_BOUQUET), + ) add_devices([Enigma2Device(config[CONF_NAME], device)], True) @@ -227,13 +256,15 @@ class Enigma2Device(MediaPlayerDevice): """ attributes = {} if not self.e2_box.in_standby: - attributes[ATTR_MEDIA_CURRENTLY_RECORDING] = \ - self.e2_box.status_info['isRecording'] - attributes[ATTR_MEDIA_DESCRIPTION] = \ - self.e2_box.status_info['currservice_fulldescription'] - attributes[ATTR_MEDIA_START_TIME] = \ - self.e2_box.status_info['currservice_begin'] - attributes[ATTR_MEDIA_END_TIME] = \ - self.e2_box.status_info['currservice_end'] + attributes[ATTR_MEDIA_CURRENTLY_RECORDING] = self.e2_box.status_info[ + "isRecording" + ] + attributes[ATTR_MEDIA_DESCRIPTION] = self.e2_box.status_info[ + "currservice_fulldescription" + ] + attributes[ATTR_MEDIA_START_TIME] = self.e2_box.status_info[ + "currservice_begin" + ] + attributes[ATTR_MEDIA_END_TIME] = self.e2_box.status_info["currservice_end"] return attributes diff --git a/homeassistant/components/enocean/__init__.py b/homeassistant/components/enocean/__init__.py index 9d51821082a..b75c8f001c0 100644 --- a/homeassistant/components/enocean/__init__.py +++ b/homeassistant/components/enocean/__init__.py @@ -9,17 +9,15 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DOMAIN = 'enocean' -DATA_ENOCEAN = 'enocean' +DOMAIN = "enocean" +DATA_ENOCEAN = "enocean" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICE): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_DEVICE): cv.string})}, extra=vol.ALLOW_EXTRA +) -SIGNAL_RECEIVE_MESSAGE = 'enocean.receive_message' -SIGNAL_SEND_MESSAGE = 'enocean.send_message' +SIGNAL_RECEIVE_MESSAGE = "enocean.receive_message" +SIGNAL_SEND_MESSAGE = "enocean.send_message" def setup(hass, config): @@ -37,12 +35,13 @@ class EnOceanDongle: def __init__(self, hass, ser): """Initialize the EnOcean dongle.""" from enocean.communicators.serialcommunicator import SerialCommunicator - self.__communicator = SerialCommunicator( - port=ser, callback=self.callback) + + self.__communicator = SerialCommunicator(port=ser, callback=self.callback) self.__communicator.start() self.hass = hass self.hass.helpers.dispatcher.dispatcher_connect( - SIGNAL_SEND_MESSAGE, self._send_message_callback) + SIGNAL_SEND_MESSAGE, self._send_message_callback + ) def _send_message_callback(self, command): """Send a command through the EnOcean dongle.""" @@ -55,10 +54,10 @@ class EnOceanDongle: is an incoming packet. """ from enocean.protocol.packet import RadioPacket + if isinstance(packet, RadioPacket): _LOGGER.debug("Received radio packet: %s", packet) - self.hass.helpers.dispatcher.dispatcher_send( - SIGNAL_RECEIVE_MESSAGE, packet) + self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_RECEIVE_MESSAGE, packet) class EnOceanDevice(Entity): @@ -72,11 +71,13 @@ class EnOceanDevice(Entity): async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_RECEIVE_MESSAGE, self._message_received_callback) + SIGNAL_RECEIVE_MESSAGE, self._message_received_callback + ) def _message_received_callback(self, packet): """Handle incoming packets.""" from enocean.utils import combine_hex + if packet.sender_int == combine_hex(self.dev_id): self.value_changed(packet) @@ -87,6 +88,6 @@ class EnOceanDevice(Entity): def send_command(self, data, optional, packet_type): """Send a command via the EnOcean dongle.""" from enocean.protocol.packet import Packet + packet = Packet(packet_type, data=data, optional=optional) - self.hass.helpers.dispatcher.dispatcher_send( - SIGNAL_SEND_MESSAGE, packet) + self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_SEND_MESSAGE, packet) diff --git a/homeassistant/components/enocean/binary_sensor.py b/homeassistant/components/enocean/binary_sensor.py index 5e0a3b31817..4ff1b461129 100644 --- a/homeassistant/components/enocean/binary_sensor.py +++ b/homeassistant/components/enocean/binary_sensor.py @@ -5,21 +5,26 @@ import voluptuous as vol from homeassistant.components import enocean from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) + DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorDevice, +) from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'EnOcean binary sensor' -DEPENDENCIES = ['enocean'] -EVENT_BUTTON_PRESSED = 'button_pressed' +DEFAULT_NAME = "EnOcean binary sensor" +DEPENDENCIES = ["enocean"] +EVENT_BUTTON_PRESSED = "button_pressed" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -97,8 +102,12 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice): elif action == 0x15: self.which = 10 self.onoff = 1 - self.hass.bus.fire(EVENT_BUTTON_PRESSED, - {'id': self.dev_id, - 'pushed': pushed, - 'which': self.which, - 'onoff': self.onoff}) + self.hass.bus.fire( + EVENT_BUTTON_PRESSED, + { + "id": self.dev_id, + "pushed": pushed, + "which": self.which, + "onoff": self.onoff, + }, + ) diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index d40b2c01df6..a1d2b22cdb4 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -6,23 +6,28 @@ import voluptuous as vol from homeassistant.components import enocean from homeassistant.components.light import ( - ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light) + ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + Light, +) from homeassistant.const import CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_SENDER_ID = 'sender_id' +CONF_SENDER_ID = "sender_id" -DEFAULT_NAME = 'EnOcean Light' +DEFAULT_NAME = "EnOcean Light" SUPPORT_ENOCEAN = SUPPORT_BRIGHTNESS -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ID, default=[]): - vol.All(cv.ensure_list, [vol.Coerce(int)]), - vol.Required(CONF_SENDER_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_ID, default=[]): vol.All(cv.ensure_list, [vol.Coerce(int)]), + vol.Required(CONF_SENDER_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -77,7 +82,7 @@ class EnOceanLight(enocean.EnOceanDevice, Light): bval = math.floor(self._brightness / 256.0 * 100.0) if bval == 0: bval = 1 - command = [0xa5, 0x02, bval, 0x01, 0x09] + command = [0xA5, 0x02, bval, 0x01, 0x09] command.extend(self._sender_id) command.extend([0x00]) self.send_command(command, [], 0x01) @@ -85,7 +90,7 @@ class EnOceanLight(enocean.EnOceanDevice, Light): def turn_off(self, **kwargs): """Turn the light source off.""" - command = [0xa5, 0x02, 0x00, 0x01, 0x09] + command = [0xA5, 0x02, 0x00, 0x01, 0x09] command.extend(self._sender_id) command.extend([0x00]) self.send_command(command, [], 0x01) @@ -97,7 +102,7 @@ class EnOceanLight(enocean.EnOceanDevice, Light): Dimmer devices like Eltako FUD61 send telegram in different RORGs. We only care about the 4BS (0xA5). """ - if packet.data[0] == 0xa5 and packet.data[1] == 0x02: + if packet.data[0] == 0xA5 and packet.data[1] == 0x02: val = packet.data[2] self._brightness = math.floor(val / 100.0 * 256.0) self._on_state = bool(val != 0) diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 62d0277946f..2e6b5bdb986 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -6,51 +6,59 @@ import voluptuous as vol from homeassistant.components import enocean from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_DEVICE_CLASS, CONF_ID, CONF_NAME, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, POWER_WATT) + CONF_DEVICE_CLASS, + CONF_ID, + CONF_NAME, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, + POWER_WATT, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_MAX_TEMP = 'max_temp' -CONF_MIN_TEMP = 'min_temp' -CONF_RANGE_FROM = 'range_from' -CONF_RANGE_TO = 'range_to' +CONF_MAX_TEMP = "max_temp" +CONF_MIN_TEMP = "min_temp" +CONF_RANGE_FROM = "range_from" +CONF_RANGE_TO = "range_to" -DEFAULT_NAME = 'EnOcean sensor' +DEFAULT_NAME = "EnOcean sensor" -DEVICE_CLASS_POWER = 'powersensor' +DEVICE_CLASS_POWER = "powersensor" SENSOR_TYPES = { DEVICE_CLASS_HUMIDITY: { - 'name': 'Humidity', - 'unit': '%', - 'icon': 'mdi:water-percent', - 'class': DEVICE_CLASS_HUMIDITY, + "name": "Humidity", + "unit": "%", + "icon": "mdi:water-percent", + "class": DEVICE_CLASS_HUMIDITY, }, DEVICE_CLASS_POWER: { - 'name': 'Power', - 'unit': POWER_WATT, - 'icon': 'mdi:power-plug', - 'class': DEVICE_CLASS_POWER, + "name": "Power", + "unit": POWER_WATT, + "icon": "mdi:power-plug", + "class": DEVICE_CLASS_POWER, }, DEVICE_CLASS_TEMPERATURE: { - 'name': 'Temperature', - 'unit': TEMP_CELSIUS, - 'icon': 'mdi:thermometer', - 'class': DEVICE_CLASS_TEMPERATURE, + "name": "Temperature", + "unit": TEMP_CELSIUS, + "icon": "mdi:thermometer", + "class": DEVICE_CLASS_TEMPERATURE, }, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_POWER): cv.string, - vol.Optional(CONF_MAX_TEMP, default=40): vol.Coerce(int), - vol.Optional(CONF_MIN_TEMP, default=0): vol.Coerce(int), - vol.Optional(CONF_RANGE_FROM, default=255): cv.positive_int, - vol.Optional(CONF_RANGE_TO, default=0): cv.positive_int, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_POWER): cv.string, + vol.Optional(CONF_MAX_TEMP, default=40): vol.Coerce(int), + vol.Optional(CONF_MIN_TEMP, default=0): vol.Coerce(int), + vol.Optional(CONF_RANGE_FROM, default=255): cv.positive_int, + vol.Optional(CONF_RANGE_TO, default=0): cv.positive_int, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -64,8 +72,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): temp_max = config.get(CONF_MAX_TEMP) range_from = config.get(CONF_RANGE_FROM) range_to = config.get(CONF_RANGE_TO) - add_entities([EnOceanTemperatureSensor( - dev_id, dev_name, temp_min, temp_max, range_from, range_to)]) + add_entities( + [ + EnOceanTemperatureSensor( + dev_id, dev_name, temp_min, temp_max, range_from, range_to + ) + ] + ) elif dev_class == DEVICE_CLASS_HUMIDITY: add_entities([EnOceanHumiditySensor(dev_id, dev_name)]) @@ -81,11 +94,12 @@ class EnOceanSensor(enocean.EnOceanDevice): """Initialize the EnOcean sensor device.""" super().__init__(dev_id, dev_name) self._sensor_type = sensor_type - self._device_class = SENSOR_TYPES[self._sensor_type]['class'] - self._dev_name = '{} {}'.format( - SENSOR_TYPES[self._sensor_type]['name'], dev_name) - self._unit_of_measurement = SENSOR_TYPES[self._sensor_type]['unit'] - self._icon = SENSOR_TYPES[self._sensor_type]['icon'] + self._device_class = SENSOR_TYPES[self._sensor_type]["class"] + self._dev_name = "{} {}".format( + SENSOR_TYPES[self._sensor_type]["name"], dev_name + ) + self._unit_of_measurement = SENSOR_TYPES[self._sensor_type]["unit"] + self._icon = SENSOR_TYPES[self._sensor_type]["icon"] self._state = None @property @@ -133,10 +147,10 @@ class EnOceanPowerSensor(EnOceanSensor): if packet.rorg != 0xA5: return packet.parse_eep(0x12, 0x01) - if packet.parsed['DT']['raw_value'] == 1: + if packet.parsed["DT"]["raw_value"] == 1: # this packet reports the current value - raw_val = packet.parsed['MR']['raw_value'] - divisor = packet.parsed['DIV']['raw_value'] + raw_val = packet.parsed["MR"]["raw_value"] + divisor = packet.parsed["DIV"]["raw_value"] self._state = raw_val / (10 ** divisor) self.schedule_update_ha_state() @@ -159,8 +173,7 @@ class EnOceanTemperatureSensor(EnOceanSensor): - A5-10-10 to A5-10-14 """ - def __init__(self, dev_id, dev_name, scale_min, scale_max, - range_from, range_to): + def __init__(self, dev_id, dev_name, scale_min, scale_max, range_from, range_to): """Initialize the EnOcean temperature sensor device.""" super().__init__(dev_id, dev_name, DEVICE_CLASS_TEMPERATURE) self._scale_min = scale_min @@ -170,7 +183,7 @@ class EnOceanTemperatureSensor(EnOceanSensor): def value_changed(self, packet): """Update the internal state of the sensor.""" - if packet.data[0] != 0xa5: + if packet.data[0] != 0xA5: return temp_scale = self._scale_max - self._scale_min temp_range = self.range_to - self.range_from diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index 48d53949a47..92642e329d9 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -11,14 +11,16 @@ from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) -CONF_CHANNEL = 'channel' -DEFAULT_NAME = 'EnOcean Switch' +CONF_CHANNEL = "channel" +DEFAULT_NAME = "EnOcean Switch" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_CHANNEL, default=0): cv.positive_int, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CHANNEL, default=0): cv.positive_int, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -53,42 +55,46 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity): def turn_on(self, **kwargs): """Turn on the switch.""" - optional = [0x03, ] + optional = [0x03] optional.extend(self.dev_id) - optional.extend([0xff, 0x00]) - self.send_command(data=[0xD2, 0x01, self.channel & 0xFF, 0x64, 0x00, - 0x00, 0x00, 0x00, 0x00], optional=optional, - packet_type=0x01) + optional.extend([0xFF, 0x00]) + self.send_command( + data=[0xD2, 0x01, self.channel & 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00], + optional=optional, + packet_type=0x01, + ) self._on_state = True def turn_off(self, **kwargs): """Turn off the switch.""" - optional = [0x03, ] + optional = [0x03] optional.extend(self.dev_id) - optional.extend([0xff, 0x00]) - self.send_command(data=[0xD2, 0x01, self.channel & 0xFF, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00], optional=optional, - packet_type=0x01) + optional.extend([0xFF, 0x00]) + self.send_command( + data=[0xD2, 0x01, self.channel & 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + optional=optional, + packet_type=0x01, + ) self._on_state = False def value_changed(self, packet): """Update the internal state of the switch.""" - if packet.data[0] == 0xa5: + if packet.data[0] == 0xA5: # power meter telegram, turn on if > 10 watts packet.parse_eep(0x12, 0x01) - if packet.parsed['DT']['raw_value'] == 1: - raw_val = packet.parsed['MR']['raw_value'] - divisor = packet.parsed['DIV']['raw_value'] + if packet.parsed["DT"]["raw_value"] == 1: + raw_val = packet.parsed["MR"]["raw_value"] + divisor = packet.parsed["DIV"]["raw_value"] watts = raw_val / (10 ** divisor) if watts > 1: self._on_state = True self.schedule_update_ha_state() - elif packet.data[0] == 0xd2: + elif packet.data[0] == 0xD2: # actuator status telegram packet.parse_eep(0x01, 0x01) - if packet.parsed['CMD']['raw_value'] == 4: - channel = packet.parsed['IO']['raw_value'] - output = packet.parsed['OV']['raw_value'] + if packet.parsed["CMD"]["raw_value"] == 4: + channel = packet.parsed["IO"]["raw_value"] + output = packet.parsed["OV"]["raw_value"] if channel == self.channel: self._on_state = output > 0 self.schedule_update_ha_state() diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index b859313a41e..2cc46632dda 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -7,7 +7,11 @@ from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, POWER_WATT, ENERGY_WATT_HOUR) + CONF_IP_ADDRESS, + CONF_MONITORED_CONDITIONS, + POWER_WATT, + ENERGY_WATT_HOUR, +) _LOGGER = logging.getLogger(__name__) @@ -15,32 +19,36 @@ _LOGGER = logging.getLogger(__name__) SENSORS = { "production": ("Envoy Current Energy Production", POWER_WATT), "daily_production": ("Envoy Today's Energy Production", ENERGY_WATT_HOUR), - "seven_days_production": ("Envoy Last Seven Days Energy Production", - ENERGY_WATT_HOUR), - "lifetime_production": ("Envoy Lifetime Energy Production", - ENERGY_WATT_HOUR), + "seven_days_production": ( + "Envoy Last Seven Days Energy Production", + ENERGY_WATT_HOUR, + ), + "lifetime_production": ("Envoy Lifetime Energy Production", ENERGY_WATT_HOUR), "consumption": ("Envoy Current Energy Consumption", POWER_WATT), - "daily_consumption": ("Envoy Today's Energy Consumption", - ENERGY_WATT_HOUR), - "seven_days_consumption": ("Envoy Last Seven Days Energy Consumption", - ENERGY_WATT_HOUR), - "lifetime_consumption": ("Envoy Lifetime Energy Consumption", - ENERGY_WATT_HOUR), - "inverters": ("Envoy Inverter", POWER_WATT) - } + "daily_consumption": ("Envoy Today's Energy Consumption", ENERGY_WATT_HOUR), + "seven_days_consumption": ( + "Envoy Last Seven Days Energy Consumption", + ENERGY_WATT_HOUR, + ), + "lifetime_consumption": ("Envoy Lifetime Energy Consumption", ENERGY_WATT_HOUR), + "inverters": ("Envoy Inverter", POWER_WATT), +} -ICON = 'mdi:flash' +ICON = "mdi:flash" CONST_DEFAULT_HOST = "envoy" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_IP_ADDRESS, default=CONST_DEFAULT_HOST): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): - vol.All(cv.ensure_list, [vol.In(list(SENSORS))])}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_IP_ADDRESS, default=CONST_DEFAULT_HOST): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( + cv.ensure_list, [vol.In(list(SENSORS))] + ), + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Enphase Envoy sensor.""" from envoy_reader.envoy_reader import EnvoyReader @@ -54,13 +62,20 @@ async def async_setup_platform(hass, config, async_add_entities, inverters = await EnvoyReader(ip_address).inverters_production() if isinstance(inverters, dict): for inverter in inverters: - entities.append(Envoy(ip_address, condition, - "{} {}".format(SENSORS[condition][0], - inverter), - SENSORS[condition][1])) + entities.append( + Envoy( + ip_address, + condition, + "{} {}".format(SENSORS[condition][0], inverter), + SENSORS[condition][1], + ) + ) else: - entities.append(Envoy(ip_address, condition, SENSORS[condition][0], - SENSORS[condition][1])) + entities.append( + Envoy( + ip_address, condition, SENSORS[condition][0], SENSORS[condition][1] + ) + ) async_add_entities(entities) @@ -108,8 +123,7 @@ class Envoy(Entity): self._state = None elif self._type == "inverters": - inverters = await (EnvoyReader(self._ip_address) - .inverters_production()) + inverters = await (EnvoyReader(self._ip_address).inverters_production()) if isinstance(inverters, dict): serial_number = self._name.split(" ")[2] self._state = inverters[serial_number] diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 61b183b9408..46ba62ba3fa 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -6,8 +6,12 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - CONF_SHOW_ON_MAP) + ATTR_ATTRIBUTION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_SHOW_ON_MAP, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -16,58 +20,61 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -API_CLIENT_NAME = 'homeassistant-homeassistant' +API_CLIENT_NAME = "homeassistant-homeassistant" ATTRIBUTION = "Data provided by entur.org under NLOD" -CONF_STOP_IDS = 'stop_ids' -CONF_EXPAND_PLATFORMS = 'expand_platforms' -CONF_WHITELIST_LINES = 'line_whitelist' -CONF_OMIT_NON_BOARDING = 'omit_non_boarding' -CONF_NUMBER_OF_DEPARTURES = 'number_of_departures' +CONF_STOP_IDS = "stop_ids" +CONF_EXPAND_PLATFORMS = "expand_platforms" +CONF_WHITELIST_LINES = "line_whitelist" +CONF_OMIT_NON_BOARDING = "omit_non_boarding" +CONF_NUMBER_OF_DEPARTURES = "number_of_departures" -DEFAULT_NAME = 'Entur' -DEFAULT_ICON_KEY = 'bus' +DEFAULT_NAME = "Entur" +DEFAULT_ICON_KEY = "bus" ICONS = { - 'air': 'mdi:airplane', - 'bus': 'mdi:bus', - 'metro': 'mdi:subway', - 'rail': 'mdi:train', - 'tram': 'mdi:tram', - 'water': 'mdi:ferry', + "air": "mdi:airplane", + "bus": "mdi:bus", + "metro": "mdi:subway", + "rail": "mdi:train", + "tram": "mdi:tram", + "water": "mdi:ferry", } SCAN_INTERVAL = timedelta(seconds=45) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_STOP_IDS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EXPAND_PLATFORMS, default=True): cv.boolean, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, - vol.Optional(CONF_WHITELIST_LINES, default=[]): cv.ensure_list, - vol.Optional(CONF_OMIT_NON_BOARDING, default=True): cv.boolean, - vol.Optional(CONF_NUMBER_OF_DEPARTURES, default=2): - vol.All(cv.positive_int, vol.Range(min=2, max=10)) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STOP_IDS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXPAND_PLATFORMS, default=True): cv.boolean, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, + vol.Optional(CONF_WHITELIST_LINES, default=[]): cv.ensure_list, + vol.Optional(CONF_OMIT_NON_BOARDING, default=True): cv.boolean, + vol.Optional(CONF_NUMBER_OF_DEPARTURES, default=2): vol.All( + cv.positive_int, vol.Range(min=2, max=10) + ), + } +) -ATTR_STOP_ID = 'stop_id' +ATTR_STOP_ID = "stop_id" -ATTR_ROUTE = 'route' -ATTR_ROUTE_ID = 'route_id' -ATTR_EXPECTED_AT = 'due_at' -ATTR_DELAY = 'delay' -ATTR_REALTIME = 'real_time' +ATTR_ROUTE = "route" +ATTR_ROUTE_ID = "route_id" +ATTR_EXPECTED_AT = "due_at" +ATTR_DELAY = "delay" +ATTR_REALTIME = "real_time" -ATTR_NEXT_UP_IN = 'next_due_in' -ATTR_NEXT_UP_ROUTE = 'next_route' -ATTR_NEXT_UP_ROUTE_ID = 'next_route_id' -ATTR_NEXT_UP_AT = 'next_due_at' -ATTR_NEXT_UP_DELAY = 'next_delay' -ATTR_NEXT_UP_REALTIME = 'next_real_time' +ATTR_NEXT_UP_IN = "next_due_in" +ATTR_NEXT_UP_ROUTE = "next_route" +ATTR_NEXT_UP_ROUTE_ID = "next_route_id" +ATTR_NEXT_UP_AT = "next_due_at" +ATTR_NEXT_UP_DELAY = "next_delay" +ATTR_NEXT_UP_REALTIME = "next_real_time" -ATTR_TRANSPORT_MODE = 'transport_mode' +ATTR_TRANSPORT_MODE = "transport_mode" def due_in_minutes(timestamp: datetime) -> int: @@ -78,8 +85,7 @@ def due_in_minutes(timestamp: datetime) -> int: return int(diff.total_seconds() / 60) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Entur public transport sensor.""" from enturclient import EnturPublicTransportData @@ -94,13 +100,15 @@ async def async_setup_platform( stops = [s for s in stop_ids if "StopPlace" in s] quays = [s for s in stop_ids if "Quay" in s] - data = EnturPublicTransportData(API_CLIENT_NAME, - stops=stops, - quays=quays, - line_whitelist=line_whitelist, - omit_non_boarding=omit_non_boarding, - number_of_departures=number_of_departures, - web_session=async_get_clientsession(hass)) + data = EnturPublicTransportData( + API_CLIENT_NAME, + stops=stops, + quays=quays, + line_whitelist=line_whitelist, + omit_non_boarding=omit_non_boarding, + number_of_departures=number_of_departures, + web_session=async_get_clientsession(hass), + ) if expand: await data.expand_all_quays() @@ -111,13 +119,13 @@ async def async_setup_platform( entities = [] for place in data.all_stop_places_quays(): try: - given_name = "{} {}".format( - name, data.get_stop_info(place).name) + given_name = "{} {}".format(name, data.get_stop_info(place).name) except KeyError: given_name = "{} {}".format(name, place) entities.append( - EnturPublicTransportSensor(proxy, given_name, place, show_on_map)) + EnturPublicTransportSensor(proxy, given_name, place, show_on_map) + ) async_add_entities(entities, True) @@ -145,8 +153,7 @@ class EnturProxy: class EnturPublicTransportSensor(Entity): """Implementation of a Entur public transport sensor.""" - def __init__( - self, api: EnturProxy, name: str, stop: str, show_on_map: bool): + def __init__(self, api: EnturProxy, name: str, stop: str, show_on_map: bool): """Initialize the sensor.""" self.api = api self._stop = stop @@ -176,7 +183,7 @@ class EnturPublicTransportSensor(Entity): @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return 'min' + return "min" @property def icon(self) -> str: @@ -204,13 +211,13 @@ class EnturPublicTransportSensor(Entity): return self._state = due_in_minutes(calls[0].expected_departure_time) - self._icon = ICONS.get( - calls[0].transport_mode, ICONS[DEFAULT_ICON_KEY]) + self._icon = ICONS.get(calls[0].transport_mode, ICONS[DEFAULT_ICON_KEY]) self._attributes[ATTR_ROUTE] = calls[0].front_display self._attributes[ATTR_ROUTE_ID] = calls[0].line_id - self._attributes[ATTR_EXPECTED_AT] = calls[0]\ - .expected_departure_time.strftime("%H:%M") + self._attributes[ATTR_EXPECTED_AT] = calls[0].expected_departure_time.strftime( + "%H:%M" + ) self._attributes[ATTR_REALTIME] = calls[0].is_realtime self._attributes[ATTR_DELAY] = calls[0].delay_in_min @@ -220,10 +227,12 @@ class EnturPublicTransportSensor(Entity): self._attributes[ATTR_NEXT_UP_ROUTE] = calls[1].front_display self._attributes[ATTR_NEXT_UP_ROUTE_ID] = calls[1].line_id - self._attributes[ATTR_NEXT_UP_AT] = calls[1]\ - .expected_departure_time.strftime("%H:%M") - self._attributes[ATTR_NEXT_UP_IN] = "{} min"\ - .format(due_in_minutes(calls[1].expected_departure_time)) + self._attributes[ATTR_NEXT_UP_AT] = calls[1].expected_departure_time.strftime( + "%H:%M" + ) + self._attributes[ATTR_NEXT_UP_IN] = "{} min".format( + due_in_minutes(calls[1].expected_departure_time) + ) self._attributes[ATTR_NEXT_UP_REALTIME] = calls[1].is_realtime self._attributes[ATTR_NEXT_UP_DELAY] = calls[1].delay_in_min @@ -235,4 +244,5 @@ class EnturPublicTransportSensor(Entity): self._attributes[key_name] = "{}{} {}".format( "" if bool(call.is_realtime) else "ca. ", call.expected_departure_time.strftime("%H:%M"), - call.front_display) + call.front_display, + ) diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 7a42c770841..709e4251fbf 100755 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -9,33 +9,38 @@ import logging import voluptuous as vol -from homeassistant.components.camera import ( - PLATFORM_SCHEMA, Camera) +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import ( - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, ATTR_ATTRIBUTION) + CONF_NAME, + CONF_LATITUDE, + CONF_LONGITUDE, + ATTR_ATTRIBUTION, +) from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_STATION = 'station' -ATTR_LOCATION = 'location' +ATTR_STATION = "station" +ATTR_LOCATION = "location" CONF_ATTRIBUTION = "Data provided by Environment Canada" -CONF_STATION = 'station' -CONF_LOOP = 'loop' -CONF_PRECIP_TYPE = 'precip_type' +CONF_STATION = "station" +CONF_LOOP = "loop" +CONF_PRECIP_TYPE = "precip_type" MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_LOOP, default=True): cv.boolean, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_STATION): cv.string, - vol.Inclusive(CONF_LATITUDE, 'latlon'): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, 'latlon'): cv.longitude, - vol.Optional(CONF_PRECIP_TYPE): ['RAIN', 'SNOW'], -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_LOOP, default=True): cv.boolean, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_STATION): cv.string, + vol.Inclusive(CONF_LATITUDE, "latlon"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "latlon"): cv.longitude, + vol.Optional(CONF_PRECIP_TYPE): ["RAIN", "SNOW"], + } +) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -43,8 +48,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from env_canada import ECRadar if config.get(CONF_STATION): - radar_object = ECRadar(station_id=config[CONF_STATION], - precip_type=config.get(CONF_PRECIP_TYPE)) + radar_object = ECRadar( + station_id=config[CONF_STATION], precip_type=config.get(CONF_PRECIP_TYPE) + ) else: lat = config.get(CONF_LATITUDE, hass.config.latitude) lon = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -62,7 +68,7 @@ class ECCamera(Camera): self.radar_object = radar_object self.camera_name = camera_name - self.content_type = 'image/gif' + self.content_type = "image/gif" self.image = None def camera_image(self): @@ -75,7 +81,7 @@ class ECCamera(Camera): """Return the name of the camera.""" if self.camera_name is not None: return self.camera_name - return ' '.join([self.radar_object.station_name, 'Radar']) + return " ".join([self.radar_object.station_name, "Radar"]) @property def device_state_attributes(self): @@ -83,7 +89,7 @@ class ECCamera(Camera): attr = { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_LOCATION: self.radar_object.station_name, - ATTR_STATION: self.radar_object.station_code + ATTR_STATION: self.radar_object.station_code, } return attr diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 42c419ba015..85df0495428 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -3,7 +3,7 @@ "name": "Environment Canada", "documentation": "https://www.home-assistant.io/components/environment_canada", "requirements": [ - "env_canada==0.0.18" + "env_canada==0.0.20" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 9bfb205f4e5..0182e7c67ed 100755 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -12,42 +12,46 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE, ATTR_ATTRIBUTION, - ATTR_LOCATION) + TEMP_CELSIUS, + CONF_LATITUDE, + CONF_LONGITUDE, + ATTR_ATTRIBUTION, + ATTR_LOCATION, +) from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_UPDATED = 'updated' -ATTR_STATION = 'station' -ATTR_DETAIL = 'alert detail' -ATTR_TIME = 'alert time' +SCAN_INTERVAL = timedelta(minutes=10) + +ATTR_UPDATED = "updated" +ATTR_STATION = "station" +ATTR_DETAIL = "alert detail" +ATTR_TIME = "alert time" CONF_ATTRIBUTION = "Data provided by Environment Canada" -CONF_STATION = 'station' -CONF_LANGUAGE = 'language' - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) +CONF_STATION = "station" +CONF_LANGUAGE = "language" def validate_station(station): """Check that the station ID is well-formed.""" if station is None: return - if not re.fullmatch(r'[A-Z]{2}/s0000\d{3}', station): + if not re.fullmatch(r"[A-Z]{2}/s0000\d{3}", station): raise vol.error.Invalid('Station ID must be of the form "XX/s0000###"') return station -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_LANGUAGE, default='english'): - vol.In(['english', 'french']), - vol.Optional(CONF_STATION): validate_station, - vol.Inclusive(CONF_LATITUDE, 'latlon'): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, 'latlon'): cv.longitude, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_LANGUAGE, default="english"): vol.In(["english", "french"]), + vol.Optional(CONF_STATION): validate_station, + vol.Inclusive(CONF_LATITUDE, "latlon"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "latlon"): cv.longitude, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -55,20 +59,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): from env_canada import ECData if config.get(CONF_STATION): - ec_data = ECData(station_id=config[CONF_STATION], - language=config.get(CONF_LANGUAGE)) + ec_data = ECData( + station_id=config[CONF_STATION], language=config.get(CONF_LANGUAGE) + ) else: lat = config.get(CONF_LATITUDE, hass.config.latitude) lon = config.get(CONF_LONGITUDE, hass.config.longitude) - ec_data = ECData(coordinates=(lat, lon), - language=config.get(CONF_LANGUAGE)) + ec_data = ECData(coordinates=(lat, lon), language=config.get(CONF_LANGUAGE)) sensor_list = list(ec_data.conditions.keys()) + list(ec_data.alerts.keys()) - sensor_list.remove('icon_code') - add_entities([ECSensor(sensor_type, - ec_data) - for sensor_type in sensor_list], - True) + sensor_list.remove("icon_code") + add_entities([ECSensor(sensor_type, ec_data) for sensor_type in sensor_list], True) class ECSensor(Entity): @@ -110,7 +111,6 @@ class ECSensor(Entity): """Return the units of measurement.""" return self._unit - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update current conditions.""" self.ec_data.update() @@ -120,39 +120,38 @@ class ECSensor(Entity): metadata = self.ec_data.metadata sensor_data = conditions.get(self.sensor_type) - self._unique_id = '{}-{}'.format(metadata['location'], - self.sensor_type) + self._unique_id = "{}-{}".format(metadata["location"], self.sensor_type) self._attr = {} - self._name = sensor_data.get('label') - value = sensor_data.get('value') + self._name = sensor_data.get("label") + value = sensor_data.get("value") if isinstance(value, list): - self._state = ' | '.join([str(s.get('title')) - for s in value]) - self._attr.update({ - ATTR_DETAIL: ' | '.join([str(s.get('detail')) - for s in value]), - ATTR_TIME: ' | '.join([str(s.get('date')) - for s in value]) - }) + self._state = " | ".join([str(s.get("title")) for s in value]) + self._attr.update( + { + ATTR_DETAIL: " | ".join([str(s.get("detail")) for s in value]), + ATTR_TIME: " | ".join([str(s.get("date")) for s in value]), + } + ) else: self._state = value - if sensor_data.get('unit') == 'C': + if sensor_data.get("unit") == "C": self._unit = TEMP_CELSIUS else: - self._unit = sensor_data.get('unit') + self._unit = sensor_data.get("unit") - timestamp = metadata.get('timestamp') + timestamp = metadata.get("timestamp") if timestamp: - updated_utc = datetime.strptime(timestamp, - '%Y%m%d%H%M%S').isoformat() + updated_utc = datetime.strptime(timestamp, "%Y%m%d%H%M%S").isoformat() else: updated_utc = None - self._attr.update({ - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_UPDATED: updated_utc, - ATTR_LOCATION: metadata.get('location'), - ATTR_STATION: metadata.get('station'), - }) + self._attr.update( + { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_UPDATED: updated_utc, + ATTR_LOCATION: metadata.get("location"), + ATTR_STATION: metadata.get("station"), + } + ) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 0be659138fb..ebb6b0cd51f 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -12,19 +12,23 @@ from env_canada import ECData import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) -from homeassistant.const import ( - CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + PLATFORM_SCHEMA, + WeatherEntity, +) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.util import Throttle import homeassistant.util.dt as dt import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_FORECAST = 'forecast' +CONF_FORECAST = "forecast" CONF_ATTRIBUTION = "Data provided by Environment Canada" -CONF_STATION = 'station' +CONF_STATION = "station" MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=10) @@ -33,34 +37,37 @@ def validate_station(station): """Check that the station ID is well-formed.""" if station is None: return - if not re.fullmatch(r'[A-Z]{2}/s0000\d{3}', station): + if not re.fullmatch(r"[A-Z]{2}/s0000\d{3}", station): raise vol.error.Invalid('Station ID must be of the form "XX/s0000###"') return station -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_STATION): validate_station, - vol.Inclusive(CONF_LATITUDE, 'latlon'): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, 'latlon'): cv.longitude, - vol.Optional(CONF_FORECAST, default='daily'): - vol.In(['daily', 'hourly']), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_STATION): validate_station, + vol.Inclusive(CONF_LATITUDE, "latlon"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "latlon"): cv.longitude, + vol.Optional(CONF_FORECAST, default="daily"): vol.In(["daily", "hourly"]), + } +) # Icon codes from http://dd.weatheroffice.ec.gc.ca/citypage_weather/ # docs/current_conditions_icon_code_descriptions_e.csv -ICON_CONDITION_MAP = {'sunny': [0, 1], - 'clear-night': [30, 31], - 'partlycloudy': [2, 3, 4, 5, 22, 32, 33, 34, 35], - 'cloudy': [10], - 'rainy': [6, 9, 11, 12, 28, 36], - 'lightning-rainy': [19, 39, 46, 47], - 'pouring': [13], - 'snowy-rainy': [7, 14, 15, 27, 37], - 'snowy': [8, 16, 17, 18, 25, 26, 38, 40], - 'windy': [43], - 'fog': [20, 21, 23, 24, 44], - 'hail': [26, 27]} +ICON_CONDITION_MAP = { + "sunny": [0, 1], + "clear-night": [30, 31], + "partlycloudy": [2, 3, 4, 5, 22, 32, 33, 34, 35], + "cloudy": [10], + "rainy": [6, 9, 11, 12, 28, 36], + "lightning-rainy": [19, 39, 46, 47], + "pouring": [13], + "snowy-rainy": [7, 14, 15, 27, 37], + "snowy": [8, 16, 17, 18, 25, 26, 38, 40], + "windy": [43], + "fog": [20, 21, 23, 24, 44], + "hail": [26, 27], +} def setup_platform(hass, config, add_devices, discovery_info=None): @@ -94,15 +101,15 @@ class ECWeather(WeatherEntity): """Return the name of the weather entity.""" if self.platform_name: return self.platform_name - return self.ec_data.metadata.get('location') + return self.ec_data.metadata.get("location") @property def temperature(self): """Return the temperature.""" - if self.ec_data.conditions.get('temperature').get('value'): - return float(self.ec_data.conditions['temperature']['value']) - if self.ec_data.hourly_forecasts[0].get('temperature'): - return float(self.ec_data.hourly_forecasts[0]['temperature']) + if self.ec_data.conditions.get("temperature").get("value"): + return float(self.ec_data.conditions["temperature"]["value"]) + if self.ec_data.hourly_forecasts[0].get("temperature"): + return float(self.ec_data.hourly_forecasts[0]["temperature"]) return None @property @@ -113,36 +120,36 @@ class ECWeather(WeatherEntity): @property def humidity(self): """Return the humidity.""" - if self.ec_data.conditions.get('humidity').get('value'): - return float(self.ec_data.conditions['humidity']['value']) + if self.ec_data.conditions.get("humidity").get("value"): + return float(self.ec_data.conditions["humidity"]["value"]) return None @property def wind_speed(self): """Return the wind speed.""" - if self.ec_data.conditions.get('wind_speed').get('value'): - return float(self.ec_data.conditions['wind_speed']['value']) + if self.ec_data.conditions.get("wind_speed").get("value"): + return float(self.ec_data.conditions["wind_speed"]["value"]) return None @property def wind_bearing(self): """Return the wind bearing.""" - if self.ec_data.conditions.get('wind_bearing').get('value'): - return float(self.ec_data.conditions['wind_bearing']['value']) + if self.ec_data.conditions.get("wind_bearing").get("value"): + return float(self.ec_data.conditions["wind_bearing"]["value"]) return None @property def pressure(self): """Return the pressure.""" - if self.ec_data.conditions.get('pressure').get('value'): - return 10 * float(self.ec_data.conditions['pressure']['value']) + if self.ec_data.conditions.get("pressure").get("value"): + return 10 * float(self.ec_data.conditions["pressure"]["value"]) return None @property def visibility(self): """Return the visibility.""" - if self.ec_data.conditions.get('visibility').get('value'): - return float(self.ec_data.conditions['visibility']['value']) + if self.ec_data.conditions.get("visibility").get("value"): + return float(self.ec_data.conditions["visibility"]["value"]) return None @property @@ -150,14 +157,14 @@ class ECWeather(WeatherEntity): """Return the weather condition.""" icon_code = None - if self.ec_data.conditions.get('icon_code').get('value'): - icon_code = self.ec_data.conditions['icon_code']['value'] - elif self.ec_data.hourly_forecasts[0].get('icon_code'): - icon_code = self.ec_data.hourly_forecasts[0]['icon_code'] + if self.ec_data.conditions.get("icon_code").get("value"): + icon_code = self.ec_data.conditions["icon_code"]["value"] + elif self.ec_data.hourly_forecasts[0].get("icon_code"): + icon_code = self.ec_data.hourly_forecasts[0]["icon_code"] if icon_code: return icon_code_to_condition(int(icon_code)) - return '' + return "" @property def forecast(self): @@ -174,42 +181,51 @@ def get_forecast(ec_data, forecast_type): """Build the forecast array.""" forecast_array = [] - if forecast_type == 'daily': + if forecast_type == "daily": half_days = ec_data.daily_forecasts - if half_days[0]['temperature_class'] == 'high': - forecast_array.append({ - ATTR_FORECAST_TIME: dt.now().isoformat(), - ATTR_FORECAST_TEMP: int(half_days[0]['temperature']), - ATTR_FORECAST_TEMP_LOW: int(half_days[1]['temperature']), - ATTR_FORECAST_CONDITION: icon_code_to_condition( - int(half_days[0]['icon_code'])) - }) + if half_days[0]["temperature_class"] == "high": + forecast_array.append( + { + ATTR_FORECAST_TIME: dt.now().isoformat(), + ATTR_FORECAST_TEMP: int(half_days[0]["temperature"]), + ATTR_FORECAST_TEMP_LOW: int(half_days[1]["temperature"]), + ATTR_FORECAST_CONDITION: icon_code_to_condition( + int(half_days[0]["icon_code"]) + ), + } + ) half_days = half_days[2:] else: half_days = half_days[1:] - for day, high, low in zip(range(1, 6), - range(0, 9, 2), - range(1, 10, 2)): - forecast_array.append({ - ATTR_FORECAST_TIME: - (dt.now() + datetime.timedelta(days=day)).isoformat(), - ATTR_FORECAST_TEMP: int(half_days[high]['temperature']), - ATTR_FORECAST_TEMP_LOW: int(half_days[low]['temperature']), - ATTR_FORECAST_CONDITION: icon_code_to_condition( - int(half_days[high]['icon_code'])) - }) + for day, high, low in zip(range(1, 6), range(0, 9, 2), range(1, 10, 2)): + forecast_array.append( + { + ATTR_FORECAST_TIME: ( + dt.now() + datetime.timedelta(days=day) + ).isoformat(), + ATTR_FORECAST_TEMP: int(half_days[high]["temperature"]), + ATTR_FORECAST_TEMP_LOW: int(half_days[low]["temperature"]), + ATTR_FORECAST_CONDITION: icon_code_to_condition( + int(half_days[high]["icon_code"]) + ), + } + ) - elif forecast_type == 'hourly': + elif forecast_type == "hourly": hours = ec_data.hourly_forecasts for hour in range(0, 24): - forecast_array.append({ - ATTR_FORECAST_TIME: dt.as_local(datetime.datetime.strptime( - hours[hour]['period'], '%Y%m%d%H%M')).isoformat(), - ATTR_FORECAST_TEMP: int(hours[hour]['temperature']), - ATTR_FORECAST_CONDITION: icon_code_to_condition( - int(hours[hour]['icon_code'])) - }) + forecast_array.append( + { + ATTR_FORECAST_TIME: dt.as_local( + datetime.datetime.strptime(hours[hour]["period"], "%Y%m%d%H%M") + ).isoformat(), + ATTR_FORECAST_TEMP: int(hours[hour]["temperature"]), + ATTR_FORECAST_CONDITION: icon_code_to_condition( + int(hours[hour]["icon_code"]) + ), + } + ) return forecast_array diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 6d792df2421..459d21ab698 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -6,49 +6,52 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (TEMP_CELSIUS, CONF_DISPLAY_OPTIONS, CONF_NAME) +from homeassistant.const import TEMP_CELSIUS, CONF_DISPLAY_OPTIONS, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'envirophat' -CONF_USE_LEDS = 'use_leds' +DEFAULT_NAME = "envirophat" +CONF_USE_LEDS = "use_leds" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SENSOR_TYPES = { - 'light': ['light', ' ', 'mdi:weather-sunny'], - 'light_red': ['light_red', ' ', 'mdi:invert-colors'], - 'light_green': ['light_green', ' ', 'mdi:invert-colors'], - 'light_blue': ['light_blue', ' ', 'mdi:invert-colors'], - 'accelerometer_x': ['accelerometer_x', 'G', 'mdi:earth'], - 'accelerometer_y': ['accelerometer_y', 'G', 'mdi:earth'], - 'accelerometer_z': ['accelerometer_z', 'G', 'mdi:earth'], - 'magnetometer_x': ['magnetometer_x', ' ', 'mdi:magnet'], - 'magnetometer_y': ['magnetometer_y', ' ', 'mdi:magnet'], - 'magnetometer_z': ['magnetometer_z', ' ', 'mdi:magnet'], - 'temperature': ['temperature', TEMP_CELSIUS, 'mdi:thermometer'], - 'pressure': ['pressure', 'hPa', 'mdi:gauge'], - 'voltage_0': ['voltage_0', 'V', 'mdi:flash'], - 'voltage_1': ['voltage_1', 'V', 'mdi:flash'], - 'voltage_2': ['voltage_2', 'V', 'mdi:flash'], - 'voltage_3': ['voltage_3', 'V', 'mdi:flash'], + "light": ["light", " ", "mdi:weather-sunny"], + "light_red": ["light_red", " ", "mdi:invert-colors"], + "light_green": ["light_green", " ", "mdi:invert-colors"], + "light_blue": ["light_blue", " ", "mdi:invert-colors"], + "accelerometer_x": ["accelerometer_x", "G", "mdi:earth"], + "accelerometer_y": ["accelerometer_y", "G", "mdi:earth"], + "accelerometer_z": ["accelerometer_z", "G", "mdi:earth"], + "magnetometer_x": ["magnetometer_x", " ", "mdi:magnet"], + "magnetometer_y": ["magnetometer_y", " ", "mdi:magnet"], + "magnetometer_z": ["magnetometer_z", " ", "mdi:magnet"], + "temperature": ["temperature", TEMP_CELSIUS, "mdi:thermometer"], + "pressure": ["pressure", "hPa", "mdi:gauge"], + "voltage_0": ["voltage_0", "V", "mdi:flash"], + "voltage_1": ["voltage_1", "V", "mdi:flash"], + "voltage_2": ["voltage_2", "V", "mdi:flash"], + "voltage_3": ["voltage_3", "V", "mdi:flash"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DISPLAY_OPTIONS, default=list(SENSOR_TYPES)): - [vol.In(SENSOR_TYPES)], - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_USE_LEDS, default=False): cv.boolean -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_DISPLAY_OPTIONS, default=list(SENSOR_TYPES)): [ + vol.In(SENSOR_TYPES) + ], + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_USE_LEDS, default=False): cv.boolean, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Sense HAT sensor platform.""" try: - envirophat = importlib.import_module('envirophat') + envirophat = importlib.import_module("envirophat") except OSError: _LOGGER.error("No Enviro pHAT was found.") return False @@ -97,37 +100,37 @@ class EnvirophatSensor(Entity): """Get the latest data and updates the states.""" self.data.update() - if self.type == 'light': + if self.type == "light": self._state = self.data.light - if self.type == 'light_red': + if self.type == "light_red": self._state = self.data.light_red - if self.type == 'light_green': + if self.type == "light_green": self._state = self.data.light_green - if self.type == 'light_blue': + if self.type == "light_blue": self._state = self.data.light_blue - if self.type == 'accelerometer_x': + if self.type == "accelerometer_x": self._state = self.data.accelerometer_x - if self.type == 'accelerometer_y': + if self.type == "accelerometer_y": self._state = self.data.accelerometer_y - if self.type == 'accelerometer_z': + if self.type == "accelerometer_z": self._state = self.data.accelerometer_z - if self.type == 'magnetometer_x': + if self.type == "magnetometer_x": self._state = self.data.magnetometer_x - if self.type == 'magnetometer_y': + if self.type == "magnetometer_y": self._state = self.data.magnetometer_y - if self.type == 'magnetometer_z': + if self.type == "magnetometer_z": self._state = self.data.magnetometer_z - if self.type == 'temperature': + if self.type == "temperature": self._state = self.data.temperature - if self.type == 'pressure': + if self.type == "pressure": self._state = self.data.pressure - if self.type == 'voltage_0': + if self.type == "voltage_0": self._state = self.data.voltage_0 - if self.type == 'voltage_1': + if self.type == "voltage_1": self._state = self.data.voltage_1 - if self.type == 'voltage_2': + if self.type == "voltage_2": self._state = self.data.voltage_2 - if self.type == 'voltage_3': + if self.type == "voltage_3": self._state = self.data.voltage_3 @@ -164,18 +167,19 @@ class EnvirophatData: if self.use_leds: self.envirophat.leds.on() # the three color values scaled against the overall light, 0-255 - self.light_red, self.light_green, self.light_blue = \ - self.envirophat.light.rgb() + self.light_red, self.light_green, self.light_blue = self.envirophat.light.rgb() if self.use_leds: self.envirophat.leds.off() # accelerometer readings in G - self.accelerometer_x, self.accelerometer_y, self.accelerometer_z = \ + self.accelerometer_x, self.accelerometer_y, self.accelerometer_z = ( self.envirophat.motion.accelerometer() + ) # raw magnetometer reading - self.magnetometer_x, self.magnetometer_y, self.magnetometer_z = \ + self.magnetometer_x, self.magnetometer_y, self.magnetometer_z = ( self.envirophat.motion.magnetometer() + ) # temperature resolution of BMP280 sensor: 0.01°C self.temperature = round(self.envirophat.weather.temperature(), 2) @@ -185,5 +189,6 @@ class EnvirophatData: self.pressure = round(self.envirophat.weather.pressure() / 100.0, 3) # Voltage sensor, reading between 0-3.3V - self.voltage_0, self.voltage_1, self.voltage_2, self.voltage_3 = \ + self.voltage_0, self.voltage_1, self.voltage_2, self.voltage_3 = ( self.envirophat.analog.read_all() + ) diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 84b98846c2a..76d6a7e369c 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -6,85 +6,94 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_TIMEOUT, \ - CONF_HOST +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_TIMEOUT, CONF_HOST from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) -DOMAIN = 'envisalink' +DOMAIN = "envisalink" -DATA_EVL = 'envisalink' +DATA_EVL = "envisalink" -CONF_CODE = 'code' -CONF_EVL_KEEPALIVE = 'keepalive_interval' -CONF_EVL_PORT = 'port' -CONF_EVL_VERSION = 'evl_version' -CONF_PANEL_TYPE = 'panel_type' -CONF_PANIC = 'panic_type' -CONF_PARTITIONNAME = 'name' -CONF_PARTITIONS = 'partitions' -CONF_PASS = 'password' -CONF_USERNAME = 'user_name' -CONF_ZONEDUMP_INTERVAL = 'zonedump_interval' -CONF_ZONENAME = 'name' -CONF_ZONES = 'zones' -CONF_ZONETYPE = 'type' +CONF_CODE = "code" +CONF_EVL_KEEPALIVE = "keepalive_interval" +CONF_EVL_PORT = "port" +CONF_EVL_VERSION = "evl_version" +CONF_PANEL_TYPE = "panel_type" +CONF_PANIC = "panic_type" +CONF_PARTITIONNAME = "name" +CONF_PARTITIONS = "partitions" +CONF_PASS = "password" +CONF_USERNAME = "user_name" +CONF_ZONEDUMP_INTERVAL = "zonedump_interval" +CONF_ZONENAME = "name" +CONF_ZONES = "zones" +CONF_ZONETYPE = "type" DEFAULT_PORT = 4025 DEFAULT_EVL_VERSION = 3 DEFAULT_KEEPALIVE = 60 DEFAULT_ZONEDUMP_INTERVAL = 30 -DEFAULT_ZONETYPE = 'opening' -DEFAULT_PANIC = 'Police' +DEFAULT_ZONETYPE = "opening" +DEFAULT_PANIC = "Police" DEFAULT_TIMEOUT = 10 -SIGNAL_ZONE_UPDATE = 'envisalink.zones_updated' -SIGNAL_PARTITION_UPDATE = 'envisalink.partition_updated' -SIGNAL_KEYPAD_UPDATE = 'envisalink.keypad_updated' +SIGNAL_ZONE_UPDATE = "envisalink.zones_updated" +SIGNAL_PARTITION_UPDATE = "envisalink.partition_updated" +SIGNAL_KEYPAD_UPDATE = "envisalink.keypad_updated" -ZONE_SCHEMA = vol.Schema({ - vol.Required(CONF_ZONENAME): cv.string, - vol.Optional(CONF_ZONETYPE, default=DEFAULT_ZONETYPE): cv.string}) +ZONE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ZONENAME): cv.string, + vol.Optional(CONF_ZONETYPE, default=DEFAULT_ZONETYPE): cv.string, + } +) -PARTITION_SCHEMA = vol.Schema({ - vol.Required(CONF_PARTITIONNAME): cv.string}) +PARTITION_SCHEMA = vol.Schema({vol.Required(CONF_PARTITIONNAME): cv.string}) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PANEL_TYPE): - vol.All(cv.string, vol.In(['HONEYWELL', 'DSC'])), - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASS): cv.string, - vol.Optional(CONF_CODE): cv.string, - vol.Optional(CONF_PANIC, default=DEFAULT_PANIC): cv.string, - vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, - vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA}, - vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION): - vol.All(vol.Coerce(int), vol.Range(min=3, max=4)), - vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE): - vol.All(vol.Coerce(int), vol.Range(min=15)), - vol.Optional( - CONF_ZONEDUMP_INTERVAL, - default=DEFAULT_ZONEDUMP_INTERVAL): vol.Coerce(int), - vol.Optional( - CONF_TIMEOUT, - default=DEFAULT_TIMEOUT): vol.Coerce(int), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PANEL_TYPE): vol.All( + cv.string, vol.In(["HONEYWELL", "DSC"]) + ), + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASS): cv.string, + vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_PANIC, default=DEFAULT_PANIC): cv.string, + vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, + vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA}, + vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION): vol.All( + vol.Coerce(int), vol.Range(min=3, max=4) + ), + vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( + vol.Coerce(int), vol.Range(min=15) + ), + vol.Optional( + CONF_ZONEDUMP_INTERVAL, default=DEFAULT_ZONEDUMP_INTERVAL + ): vol.Coerce(int), + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -SERVICE_CUSTOM_FUNCTION = 'invoke_custom_function' -ATTR_CUSTOM_FUNCTION = 'pgm' -ATTR_PARTITION = 'partition' +SERVICE_CUSTOM_FUNCTION = "invoke_custom_function" +ATTR_CUSTOM_FUNCTION = "pgm" +ATTR_PARTITION = "partition" -SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_CUSTOM_FUNCTION): cv.string, - vol.Required(ATTR_PARTITION): cv.string, -}) +SERVICE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CUSTOM_FUNCTION): cv.string, + vol.Required(ATTR_PARTITION): cv.string, + } +) async def async_setup(hass, config): @@ -109,8 +118,17 @@ async def async_setup(hass, config): sync_connect = asyncio.Future() controller = EnvisalinkAlarmPanel( - host, port, panel_type, version, user, password, zone_dump, - keep_alive, hass.loop, connection_timeout) + host, + port, + panel_type, + version, + user, + password, + zone_dump, + keep_alive, + hass.loop, + connection_timeout, + ) hass.data[DATA_EVL] = controller @callback @@ -132,8 +150,7 @@ async def async_setup(hass, config): """Handle a successful connection.""" _LOGGER.info("Established a connection with the Envisalink") if not sync_connect.done(): - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, - stop_envisalink) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink) sync_connect.set_result(True) @callback @@ -183,30 +200,34 @@ async def async_setup(hass, config): # Load sub-components for Envisalink if partitions: - hass.async_create_task(async_load_platform( - hass, 'alarm_control_panel', 'envisalink', { - CONF_PARTITIONS: partitions, - CONF_CODE: code, - CONF_PANIC: panic_type - }, config - )) - hass.async_create_task(async_load_platform( - hass, 'sensor', 'envisalink', { - CONF_PARTITIONS: partitions, - CONF_CODE: code - }, config - )) + hass.async_create_task( + async_load_platform( + hass, + "alarm_control_panel", + "envisalink", + {CONF_PARTITIONS: partitions, CONF_CODE: code, CONF_PANIC: panic_type}, + config, + ) + ) + hass.async_create_task( + async_load_platform( + hass, + "sensor", + "envisalink", + {CONF_PARTITIONS: partitions, CONF_CODE: code}, + config, + ) + ) if zones: - hass.async_create_task(async_load_platform( - hass, 'binary_sensor', 'envisalink', { - CONF_ZONES: zones - }, config - )) + hass.async_create_task( + async_load_platform( + hass, "binary_sensor", "envisalink", {CONF_ZONES: zones}, config + ) + ) - hass.services.async_register(DOMAIN, - SERVICE_CUSTOM_FUNCTION, - handle_custom_function, - schema=SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_CUSTOM_FUNCTION, handle_custom_function, schema=SERVICE_SCHEMA + ) return True diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 91a59d8f842..81e656708c5 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -5,31 +5,44 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, - STATE_UNKNOWN) + ATTR_ENTITY_ID, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, + STATE_UNKNOWN, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( - CONF_CODE, CONF_PANIC, CONF_PARTITIONNAME, DATA_EVL, PARTITION_SCHEMA, - SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE, EnvisalinkDevice) + CONF_CODE, + CONF_PANIC, + CONF_PARTITIONNAME, + DATA_EVL, + PARTITION_SCHEMA, + SIGNAL_KEYPAD_UPDATE, + SIGNAL_PARTITION_UPDATE, + EnvisalinkDevice, +) _LOGGER = logging.getLogger(__name__) -SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress' -ATTR_KEYPRESS = 'keypress' -ALARM_KEYPRESS_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_KEYPRESS): cv.string -}) +SERVICE_ALARM_KEYPRESS = "envisalink_alarm_keypress" +ATTR_KEYPRESS = "keypress" +ALARM_KEYPRESS_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_KEYPRESS): cv.string, + } +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Perform the setup for Envisalink alarm panels.""" - configured_partitions = discovery_info['partitions'] + configured_partitions = discovery_info["partitions"] code = discovery_info[CONF_CODE] panic_type = discovery_info[CONF_PANIC] @@ -37,9 +50,14 @@ async def async_setup_platform( for part_num in configured_partitions: device_config_data = PARTITION_SCHEMA(configured_partitions[part_num]) device = EnvisalinkAlarm( - hass, part_num, device_config_data[CONF_PARTITIONNAME], code, - panic_type, hass.data[DATA_EVL].alarm_state['partition'][part_num], - hass.data[DATA_EVL]) + hass, + part_num, + device_config_data[CONF_PARTITIONNAME], + code, + panic_type, + hass.data[DATA_EVL].alarm_state["partition"][part_num], + hass.data[DATA_EVL], + ) devices.append(device) async_add_entities(devices) @@ -50,15 +68,19 @@ async def async_setup_platform( entity_ids = service.data.get(ATTR_ENTITY_ID) keypress = service.data.get(ATTR_KEYPRESS) - target_devices = [device for device in devices - if device.entity_id in entity_ids] + target_devices = [ + device for device in devices if device.entity_id in entity_ids + ] for device in target_devices: device.async_alarm_keypress(keypress) hass.services.async_register( - alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, - schema=ALARM_KEYPRESS_SCHEMA) + alarm.DOMAIN, + SERVICE_ALARM_KEYPRESS, + alarm_keypress_handler, + schema=ALARM_KEYPRESS_SCHEMA, + ) return True @@ -66,8 +88,9 @@ async def async_setup_platform( class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Representation of an Envisalink-based alarm panel.""" - def __init__(self, hass, partition_number, alarm_name, code, panic_type, - info, controller): + def __init__( + self, hass, partition_number, alarm_name, code, panic_type, info, controller + ): """Initialize the alarm panel.""" self._partition_number = partition_number self._code = code @@ -78,10 +101,10 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): async def async_added_to_hass(self): """Register callbacks.""" + async_dispatcher_connect(self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) async_dispatcher_connect( - self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) - async_dispatcher_connect( - self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback) + self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback + ) @callback def _update_callback(self, partition): @@ -101,46 +124,50 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Return the state of the device.""" state = STATE_UNKNOWN - if self._info['status']['alarm']: + if self._info["status"]["alarm"]: state = STATE_ALARM_TRIGGERED - elif self._info['status']['armed_away']: + elif self._info["status"]["armed_away"]: state = STATE_ALARM_ARMED_AWAY - elif self._info['status']['armed_stay']: + elif self._info["status"]["armed_stay"]: state = STATE_ALARM_ARMED_HOME - elif self._info['status']['exit_delay']: + elif self._info["status"]["exit_delay"]: state = STATE_ALARM_PENDING - elif self._info['status']['entry_delay']: + elif self._info["status"]["entry_delay"]: state = STATE_ALARM_PENDING - elif self._info['status']['alpha']: + elif self._info["status"]["alpha"]: state = STATE_ALARM_DISARMED return state async def async_alarm_disarm(self, code=None): """Send disarm command.""" if code: - self.hass.data[DATA_EVL].disarm_partition( - str(code), self._partition_number) + self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number) else: self.hass.data[DATA_EVL].disarm_partition( - str(self._code), self._partition_number) + str(self._code), self._partition_number + ) async def async_alarm_arm_home(self, code=None): """Send arm home command.""" if code: self.hass.data[DATA_EVL].arm_stay_partition( - str(code), self._partition_number) + str(code), self._partition_number + ) else: self.hass.data[DATA_EVL].arm_stay_partition( - str(self._code), self._partition_number) + str(self._code), self._partition_number + ) async def async_alarm_arm_away(self, code=None): """Send arm away command.""" if code: self.hass.data[DATA_EVL].arm_away_partition( - str(code), self._partition_number) + str(code), self._partition_number + ) else: self.hass.data[DATA_EVL].arm_away_partition( - str(self._code), self._partition_number) + str(self._code), self._partition_number + ) async def async_alarm_trigger(self, code=None): """Alarm trigger command. Will be used to trigger a panic alarm.""" @@ -151,4 +178,5 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Send custom keypress.""" if keypress: self.hass.data[DATA_EVL].keypresses_to_partition( - self._partition_number, keypress) + self._partition_number, keypress + ) diff --git a/homeassistant/components/envisalink/binary_sensor.py b/homeassistant/components/envisalink/binary_sensor.py index bf47749d228..fbe9824d067 100644 --- a/homeassistant/components/envisalink/binary_sensor.py +++ b/homeassistant/components/envisalink/binary_sensor.py @@ -9,16 +9,20 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util from . import ( - CONF_ZONENAME, CONF_ZONETYPE, DATA_EVL, SIGNAL_ZONE_UPDATE, ZONE_SCHEMA, - EnvisalinkDevice) + CONF_ZONENAME, + CONF_ZONETYPE, + DATA_EVL, + SIGNAL_ZONE_UPDATE, + ZONE_SCHEMA, + EnvisalinkDevice, +) _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Envisalink binary sensor devices.""" - configured_zones = discovery_info['zones'] + configured_zones = discovery_info["zones"] devices = [] for zone_num in configured_zones: @@ -28,8 +32,8 @@ async def async_setup_platform(hass, config, async_add_entities, zone_num, device_config_data[CONF_ZONENAME], device_config_data[CONF_ZONETYPE], - hass.data[DATA_EVL].alarm_state['zone'][zone_num], - hass.data[DATA_EVL] + hass.data[DATA_EVL].alarm_state["zone"][zone_num], + hass.data[DATA_EVL], ) devices.append(device) @@ -39,19 +43,17 @@ async def async_setup_platform(hass, config, async_add_entities, class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): """Representation of an Envisalink binary sensor.""" - def __init__(self, hass, zone_number, zone_name, zone_type, info, - controller): + def __init__(self, hass, zone_number, zone_name, zone_type, info, controller): """Initialize the binary_sensor.""" self._zone_type = zone_type self._zone_number = zone_number - _LOGGER.debug('Setting up zone: %s', zone_name) + _LOGGER.debug("Setting up zone: %s", zone_name) super().__init__(zone_name, info, controller) async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_ZONE_UPDATE, self._update_callback) + async_dispatcher_connect(self.hass, SIGNAL_ZONE_UPDATE, self._update_callback) @property def device_state_attributes(self): @@ -67,7 +69,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): # interval, so we subtract it from the current second-accurate time # unless it is already at the maximum value, in which case we set it # to None since we can't determine the actual value. - seconds_ago = self._info['last_fault'] + seconds_ago = self._info["last_fault"] if seconds_ago < 65536 * 5: now = dt_util.now().replace(microsecond=0) delta = datetime.timedelta(seconds=seconds_ago) @@ -81,7 +83,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): @property def is_on(self): """Return true if sensor is on.""" - return self._info['status']['open'] + return self._info["status"]["open"] @property def device_class(self): diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py index 2652a7e2137..05ad0783fac 100644 --- a/homeassistant/components/envisalink/sensor.py +++ b/homeassistant/components/envisalink/sensor.py @@ -6,24 +6,31 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from . import ( - CONF_PARTITIONNAME, DATA_EVL, PARTITION_SCHEMA, SIGNAL_KEYPAD_UPDATE, - SIGNAL_PARTITION_UPDATE, EnvisalinkDevice) + CONF_PARTITIONNAME, + DATA_EVL, + PARTITION_SCHEMA, + SIGNAL_KEYPAD_UPDATE, + SIGNAL_PARTITION_UPDATE, + EnvisalinkDevice, +) _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Perform the setup for Envisalink sensor devices.""" - configured_partitions = discovery_info['partitions'] + configured_partitions = discovery_info["partitions"] devices = [] for part_num in configured_partitions: device_config_data = PARTITION_SCHEMA(configured_partitions[part_num]) device = EnvisalinkSensor( - hass, device_config_data[CONF_PARTITIONNAME], part_num, - hass.data[DATA_EVL].alarm_state['partition'][part_num], - hass.data[DATA_EVL]) + hass, + device_config_data[CONF_PARTITIONNAME], + part_num, + hass.data[DATA_EVL].alarm_state["partition"][part_num], + hass.data[DATA_EVL], + ) devices.append(device) @@ -33,21 +40,20 @@ async def async_setup_platform( class EnvisalinkSensor(EnvisalinkDevice, Entity): """Representation of an Envisalink keypad.""" - def __init__(self, hass, partition_name, partition_number, info, - controller): + def __init__(self, hass, partition_name, partition_number, info, controller): """Initialize the sensor.""" - self._icon = 'mdi:alarm' + self._icon = "mdi:alarm" self._partition_number = partition_number _LOGGER.debug("Setting up sensor for partition: %s", partition_name) - super().__init__(partition_name + ' Keypad', info, controller) + super().__init__(partition_name + " Keypad", info, controller) async def async_added_to_hass(self): """Register callbacks.""" + async_dispatcher_connect(self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) async_dispatcher_connect( - self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) - async_dispatcher_connect( - self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback) + self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback + ) @property def icon(self): @@ -57,12 +63,12 @@ class EnvisalinkSensor(EnvisalinkDevice, Entity): @property def state(self): """Return the overall state.""" - return self._info['status']['alpha'] + return self._info["status"]["alpha"] @property def device_state_attributes(self): """Return the state attributes.""" - return self._info['status'] + return self._info["status"] @callback def _update_callback(self, partition): diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 09b0fc0c5fd..0e35b8bbee7 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -5,11 +5,20 @@ import voluptuous as vol from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, SUPPORT_AUX_HEAT, - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_OFF, CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE) + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + SUPPORT_AUX_HEAT, + SUPPORT_TARGET_TEMPERATURE, + HVAC_MODE_OFF, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, +) from homeassistant.const import ( - ATTR_TEMPERATURE, TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD) + ATTR_TEMPERATURE, + TEMP_CELSIUS, + CONF_USERNAME, + CONF_PASSWORD, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -19,15 +28,14 @@ SCAN_INTERVAL = timedelta(seconds=120) OPERATION_LIST = [HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} +) EPH_TO_HA_STATE = { - 'AUTO': HVAC_MODE_HEAT_COOL, - 'ON': HVAC_MODE_HEAT, - 'OFF': HVAC_MODE_OFF + "AUTO": HVAC_MODE_HEAT_COOL, + "ON": HVAC_MODE_HEAT, + "OFF": HVAC_MODE_OFF, } HA_STATE_TO_EPH = {value: key for key, value in EPH_TO_HA_STATE.items()} @@ -58,9 +66,9 @@ class EphEmberThermostat(ClimateDevice): def __init__(self, ember, zone): """Initialize the thermostat.""" self._ember = ember - self._zone_name = zone['name'] + self._zone_name = zone["name"] self._zone = zone - self._hot_water = zone['isHotWater'] + self._hot_water = zone["isHotWater"] @property def supported_features(self): @@ -68,8 +76,7 @@ class EphEmberThermostat(ClimateDevice): if self._hot_water: return SUPPORT_AUX_HEAT - return (SUPPORT_TARGET_TEMPERATURE | - SUPPORT_AUX_HEAT) + return SUPPORT_TARGET_TEMPERATURE | SUPPORT_AUX_HEAT @property def name(self): @@ -84,12 +91,12 @@ class EphEmberThermostat(ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - return self._zone['currentTemperature'] + return self._zone["currentTemperature"] @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._zone['targetTemperature'] + return self._zone["targetTemperature"] @property def target_temperature_step(self): @@ -102,7 +109,7 @@ class EphEmberThermostat(ClimateDevice): @property def hvac_action(self): """Return current HVAC action.""" - if self._zone['isCurrentlyActive']: + if self._zone["isCurrentlyActive"]: return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE @@ -111,7 +118,8 @@ class EphEmberThermostat(ClimateDevice): def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" from pyephember.pyephember import ZoneMode - mode = ZoneMode(self._zone['mode']) + + mode = ZoneMode(self._zone["mode"]) return self.map_mode_eph_hass(mode) @property @@ -130,12 +138,13 @@ class EphEmberThermostat(ClimateDevice): @property def is_aux_heat(self): """Return true if aux heater.""" - return self._zone['isBoostActive'] + return self._zone["isBoostActive"] def turn_aux_heat_on(self): """Turn auxiliary heater on.""" self._ember.activate_boost_by_name( - self._zone_name, self._zone['targetTemperature']) + self._zone_name, self._zone["targetTemperature"] + ) def turn_aux_heat_off(self): """Turn auxiliary heater off.""" @@ -156,15 +165,14 @@ class EphEmberThermostat(ClimateDevice): if temperature > self.max_temp or temperature < self.min_temp: return - self._ember.set_target_temperture_by_name(self._zone_name, - int(temperature)) + self._ember.set_target_temperture_by_name(self._zone_name, int(temperature)) @property def min_temp(self): """Return the minimum temperature.""" # Hot water temp doesn't support being changed if self._hot_water: - return self._zone['targetTemperature'] + return self._zone["targetTemperature"] return 5 @@ -172,7 +180,7 @@ class EphEmberThermostat(ClimateDevice): def max_temp(self): """Return the maximum temperature.""" if self._hot_water: - return self._zone['targetTemperature'] + return self._zone["targetTemperature"] return 35 @@ -184,6 +192,7 @@ class EphEmberThermostat(ClimateDevice): def map_mode_hass_eph(operation_mode): """Map from home assistant mode to eph mode.""" from pyephember.pyephember import ZoneMode + return getattr(ZoneMode, HA_STATE_TO_EPH.get(operation_mode), None) @staticmethod diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 8273ca9a21a..435ef582da8 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -3,45 +3,65 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_NEXT_TRACK, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP) + DOMAIN, + SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_STEP, +) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, STATE_OFF, - STATE_ON) + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_SSL, + STATE_OFF, + STATE_ON, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_CMODE = 'cmode' +ATTR_CMODE = "cmode" -DATA_EPSON = 'epson' -DEFAULT_NAME = 'EPSON Projector' +DATA_EPSON = "epson" +DEFAULT_NAME = "EPSON Projector" -SERVICE_SELECT_CMODE = 'epson_select_cmode' +SERVICE_SELECT_CMODE = "epson_select_cmode" SUPPORT_CMODE = 33001 -SUPPORT_EPSON = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE |\ - SUPPORT_CMODE | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \ - SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK +SUPPORT_EPSON = ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE + | SUPPORT_CMODE + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_STEP + | SUPPORT_NEXT_TRACK + | SUPPORT_PREVIOUS_TRACK +) + +MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=80): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=80): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, -}) - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Epson media player platform.""" - from epson_projector.const import (CMODE_LIST_SET) + from epson_projector.const import CMODE_LIST_SET if DATA_EPSON not in hass.data: hass.data[DATA_EPSON] = [] @@ -51,8 +71,9 @@ async def async_setup_platform( port = config.get(CONF_PORT) ssl = config.get(CONF_SSL) - epson = EpsonProjector(async_get_clientsession( - hass, verify_ssl=False), name, host, port, ssl) + epson = EpsonProjector( + async_get_clientsession(hass, verify_ssl=False), name, host, port, ssl + ) hass.data[DATA_EPSON].append(epson) async_add_entities([epson], update_before_add=True) @@ -61,8 +82,11 @@ async def async_setup_platform( """Handle for services.""" entity_ids = service.data.get(ATTR_ENTITY_ID) if entity_ids: - devices = [device for device in hass.data[DATA_EPSON] - if device.entity_id in entity_ids] + devices = [ + device + for device in hass.data[DATA_EPSON] + if device.entity_id in entity_ids + ] else: devices = hass.data[DATA_EPSON] for device in devices: @@ -71,12 +95,12 @@ async def async_setup_platform( await device.select_cmode(cmode) device.async_schedule_update_ha_state(True) - epson_schema = MEDIA_PLAYER_SCHEMA.extend({ - vol.Required(ATTR_CMODE): vol.All(cv.string, vol.Any(*CMODE_LIST_SET)) - }) + epson_schema = MEDIA_PLAYER_SCHEMA.extend( + {vol.Required(ATTR_CMODE): vol.All(cv.string, vol.Any(*CMODE_LIST_SET))} + ) hass.services.async_register( - DOMAIN, SERVICE_SELECT_CMODE, async_service_handler, - schema=epson_schema) + DOMAIN, SERVICE_SELECT_CMODE, async_service_handler, schema=epson_schema + ) class EpsonProjector(MediaPlayerDevice): @@ -88,8 +112,7 @@ class EpsonProjector(MediaPlayerDevice): from epson_projector.const import DEFAULT_SOURCES self._name = name - self._projector = epson.Projector( - host, websession=websession, port=port) + self._projector = epson.Projector(host, websession=websession, port=port) self._cmode = None self._source_list = list(DEFAULT_SOURCES.values()) self._source = None @@ -99,8 +122,16 @@ class EpsonProjector(MediaPlayerDevice): async def async_update(self): """Update state of device.""" from epson_projector.const import ( - EPSON_CODES, POWER, CMODE, CMODE_LIST, SOURCE, VOLUME, BUSY, - SOURCE_LIST) + EPSON_CODES, + POWER, + CMODE, + CMODE_LIST, + SOURCE, + VOLUME, + BUSY, + SOURCE_LIST, + ) + is_turned_on = await self._projector.get_property(POWER) _LOGGER.debug("Project turn on/off status: %s", is_turned_on) if is_turned_on and is_turned_on == EPSON_CODES[POWER]: @@ -135,12 +166,14 @@ class EpsonProjector(MediaPlayerDevice): async def async_turn_on(self): """Turn on epson.""" from epson_projector.const import TURN_ON + if self._state == STATE_OFF: await self._projector.send_command(TURN_ON) async def async_turn_off(self): """Turn off epson.""" from epson_projector.const import TURN_OFF + if self._state == STATE_ON: await self._projector.send_command(TURN_OFF) @@ -161,48 +194,57 @@ class EpsonProjector(MediaPlayerDevice): async def select_cmode(self, cmode): """Set color mode in Epson.""" - from epson_projector.const import (CMODE_LIST_SET) + from epson_projector.const import CMODE_LIST_SET + await self._projector.send_command(CMODE_LIST_SET[cmode]) async def async_select_source(self, source): """Select input source.""" from epson_projector.const import INV_SOURCES + selected_source = INV_SOURCES[source] await self._projector.send_command(selected_source) async def async_mute_volume(self, mute): """Mute (true) or unmute (false) sound.""" from epson_projector.const import MUTE + await self._projector.send_command(MUTE) async def async_volume_up(self): """Increase volume.""" from epson_projector.const import VOL_UP + await self._projector.send_command(VOL_UP) async def async_volume_down(self): """Decrease volume.""" from epson_projector.const import VOL_DOWN + await self._projector.send_command(VOL_DOWN) async def async_media_play(self): """Play media via Epson.""" from epson_projector.const import PLAY + await self._projector.send_command(PLAY) async def async_media_pause(self): """Pause media via Epson.""" from epson_projector.const import PAUSE + await self._projector.send_command(PAUSE) async def async_media_next_track(self): """Skip to next.""" from epson_projector.const import FAST + await self._projector.send_command(FAST) async def async_media_previous_track(self): """Skip to previous.""" from epson_projector.const import BACK + await self._projector.send_command(BACK) @property diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index 4f9ea4a1dd0..99e2723bf4a 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -10,22 +10,25 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['epsonprinter==0.0.9'] +REQUIREMENTS = ["epsonprinter==0.0.9"] _LOGGER = logging.getLogger(__name__) MONITORED_CONDITIONS = { - 'black': ['Ink level Black', '%', 'mdi:water'], - 'photoblack': ['Ink level Photoblack', '%', 'mdi:water'], - 'magenta': ['Ink level Magenta', '%', 'mdi:water'], - 'cyan': ['Ink level Cyan', '%', 'mdi:water'], - 'yellow': ['Ink level Yellow', '%', 'mdi:water'], - 'clean': ['Cleaning level', '%', 'mdi:water'], + "black": ["Ink level Black", "%", "mdi:water"], + "photoblack": ["Ink level Photoblack", "%", "mdi:water"], + "magenta": ["Ink level Magenta", "%", "mdi:water"], + "cyan": ["Ink level Cyan", "%", "mdi:water"], + "yellow": ["Ink level Yellow", "%", "mdi:water"], + "clean": ["Cleaning level", "%", "mdi:water"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_MONITORED_CONDITIONS): vol.All( + cv.ensure_list, [vol.In(MONITORED_CONDITIONS)] + ), + } +) SCAN_INTERVAL = timedelta(minutes=60) @@ -34,12 +37,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): host = config.get(CONF_HOST) from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI + api = EpsonPrinterAPI(host) if not api.available: raise PlatformNotReady() - sensors = [EpsonPrinterCartridge(api, condition) - for condition in config[CONF_MONITORED_CONDITIONS]] + sensors = [ + EpsonPrinterCartridge(api, condition) + for condition in config[CONF_MONITORED_CONDITIONS] + ] add_devices(sensors, True) diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 6a9d65b3883..219578c34be 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -6,21 +6,33 @@ import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, - SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE) + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_BOOST, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + PRESET_NONE, +) from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_DEVICES, CONF_MAC, PRECISION_HALVES, TEMP_CELSIUS) + ATTR_TEMPERATURE, + CONF_DEVICES, + CONF_MAC, + PRECISION_HALVES, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -STATE_BOOST = 'boost' +STATE_BOOST = "boost" -ATTR_STATE_WINDOW_OPEN = 'window_open' -ATTR_STATE_VALVE = 'valve' -ATTR_STATE_LOCKED = 'is_locked' -ATTR_STATE_LOW_BAT = 'low_battery' -ATTR_STATE_AWAY_END = 'away_end' +ATTR_STATE_WINDOW_OPEN = "window_open" +ATTR_STATE_VALVE = "valve" +ATTR_STATE_LOCKED = "is_locked" +ATTR_STATE_LOW_BAT = "low_battery" +ATTR_STATE_AWAY_END = "away_end" EQ_TO_HA_HVAC = { eq3.Mode.Open: HVAC_MODE_HEAT, @@ -34,28 +46,19 @@ EQ_TO_HA_HVAC = { HA_TO_EQ_HVAC = { HVAC_MODE_HEAT: eq3.Mode.Manual, HVAC_MODE_OFF: eq3.Mode.Closed, - HVAC_MODE_AUTO: eq3.Mode.Auto + HVAC_MODE_AUTO: eq3.Mode.Auto, } -EQ_TO_HA_PRESET = { - eq3.Mode.Boost: PRESET_BOOST, - eq3.Mode.Away: PRESET_AWAY, -} +EQ_TO_HA_PRESET = {eq3.Mode.Boost: PRESET_BOOST, eq3.Mode.Away: PRESET_AWAY} -HA_TO_EQ_PRESET = { - PRESET_BOOST: eq3.Mode.Boost, - PRESET_AWAY: eq3.Mode.Away, -} +HA_TO_EQ_PRESET = {PRESET_BOOST: eq3.Mode.Boost, PRESET_AWAY: eq3.Mode.Away} -DEVICE_SCHEMA = vol.Schema({ - vol.Required(CONF_MAC): cv.string, -}) +DEVICE_SCHEMA = vol.Schema({vol.Required(CONF_MAC): cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): - vol.Schema({cv.string: DEVICE_SCHEMA}), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_DEVICES): vol.Schema({cv.string: DEVICE_SCHEMA})} +) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -181,7 +184,7 @@ class EQ3BTSmartThermostat(ClimateDevice): def set_preset_mode(self, preset_mode): """Set new preset mode.""" - if not preset_mode: + if preset_mode == PRESET_NONE: self.set_hvac_mode(HVAC_MODE_HEAT) self._thermostat.mode = HA_TO_EQ_PRESET[preset_mode] @@ -189,6 +192,7 @@ class EQ3BTSmartThermostat(ClimateDevice): """Update the data from the thermostat.""" # pylint: disable=import-error,no-name-in-module from bluepy.btle import BTLEException + try: self._thermostat.update() except BTLEException as ex: diff --git a/homeassistant/components/esphome/.translations/bg.json b/homeassistant/components/esphome/.translations/bg.json index 3574965cae6..44a18396873 100644 --- a/homeassistant/components/esphome/.translations/bg.json +++ b/homeassistant/components/esphome/.translations/bg.json @@ -8,6 +8,7 @@ "invalid_password": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430!", "resolve_error": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043e\u0442\u043a\u0440\u0438\u0435 \u0430\u0434\u0440\u0435\u0441\u044a\u0442 \u043d\u0430 ESP. \u0410\u043a\u043e \u0442\u0430\u0437\u0438 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0430\u0432\u0430, \u0437\u0430\u0434\u0430\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/da.json b/homeassistant/components/esphome/.translations/da.json index 76389c45149..ba84ab40301 100644 --- a/homeassistant/components/esphome/.translations/da.json +++ b/homeassistant/components/esphome/.translations/da.json @@ -8,6 +8,7 @@ "invalid_password": "Ugyldig adgangskode!", "resolve_error": "Kan ikke finde adressen p\u00e5 ESP. Hvis denne fejl forts\u00e6tter skal du angive en statisk IP-adresse: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/pt-BR.json b/homeassistant/components/esphome/.translations/pt-BR.json index 77a98a875ba..1c2baa9e028 100644 --- a/homeassistant/components/esphome/.translations/pt-BR.json +++ b/homeassistant/components/esphome/.translations/pt-BR.json @@ -18,10 +18,12 @@ "title": "Digite a senha" }, "discovery_confirm": { + "description": "Voc\u00ea quer adicionar o n\u00f3 ESPHome ` {name} ` ao Home Assistant?", "title": "N\u00f3 ESPHome descoberto" }, "user": { "data": { + "host": "Host", "port": "Porta" }, "description": "Por favor insira as configura\u00e7\u00f5es de conex\u00e3o de seu n\u00f3 de [ESPHome] (https://esphomelib.com/).", diff --git a/homeassistant/components/esphome/.translations/zh-Hant.json b/homeassistant/components/esphome/.translations/zh-Hant.json index 721f4362103..74d0b925fb2 100644 --- a/homeassistant/components/esphome/.translations/zh-Hant.json +++ b/homeassistant/components/esphome/.translations/zh-Hant.json @@ -14,7 +14,7 @@ "data": { "password": "\u5bc6\u78bc" }, - "description": "\u8acb\u8f38\u5165\u8a2d\u5b9a\u5167\u6240\u8a2d\u5b9a\u4e4b\u5bc6\u78bc\u3002", + "description": "\u8acb\u8f38\u5165 {name} \u8a2d\u5b9a\u5167\u6240\u8a2d\u5b9a\u4e4b\u5bc6\u78bc\u3002", "title": "\u8f38\u5165\u5bc6\u78bc" }, "discovery_confirm": { diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index db5aeea2aa1..8780d2b67ae 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -5,14 +5,25 @@ import math from typing import Any, Callable, Dict, List, Optional from aioesphomeapi import ( - APIClient, APIConnectionError, DeviceInfo, EntityInfo, EntityState, - HomeassistantServiceCall, UserService, UserServiceArgType) + APIClient, + APIConnectionError, + DeviceInfo, + EntityInfo, + EntityState, + HomeassistantServiceCall, + UserService, + UserServiceArgType, +) import voluptuous as vol from homeassistant import const from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_PORT, EVENT_HOMEASSISTANT_STOP) + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import Event, State, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import template @@ -29,14 +40,19 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType # Import config flow so that it's added to the registry from .config_flow import EsphomeFlowHandler # noqa from .entry_data import ( - DATA_KEY, DISPATCHER_ON_DEVICE_UPDATE, DISPATCHER_ON_LIST, - DISPATCHER_ON_STATE, DISPATCHER_REMOVE_ENTITY, DISPATCHER_UPDATE_ENTITY, - RuntimeEntryData) + DATA_KEY, + DISPATCHER_ON_DEVICE_UPDATE, + DISPATCHER_ON_LIST, + DISPATCHER_ON_STATE, + DISPATCHER_REMOVE_ENTITY, + DISPATCHER_UPDATE_ENTITY, + RuntimeEntryData, +) -DOMAIN = 'esphome' +DOMAIN = "esphome" _LOGGER = logging.getLogger(__name__) -STORAGE_KEY = 'esphome.{}' +STORAGE_KEY = "esphome.{}" STORAGE_VERSION = 1 # No config schema - only configuration entry @@ -51,8 +67,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, - entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the esphome component.""" hass.data.setdefault(DATA_KEY, {}) @@ -60,16 +75,20 @@ async def async_setup_entry(hass: HomeAssistantType, port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] - cli = APIClient(hass.loop, host, port, password, - client_info="Home Assistant {}".format(const.__version__)) + cli = APIClient( + hass.loop, + host, + port, + password, + client_info="Home Assistant {}".format(const.__version__), + ) # Store client in per-config-entry hass.data - store = Store(hass, STORAGE_VERSION, STORAGE_KEY.format(entry.entry_id), - encoder=JSONEncoder) + store = Store( + hass, STORAGE_VERSION, STORAGE_KEY.format(entry.entry_id), encoder=JSONEncoder + ) entry_data = hass.data[DATA_KEY][entry.entry_id] = RuntimeEntryData( - client=cli, - entry_id=entry.entry_id, - store=store, + client=cli, entry_id=entry.entry_id, store=store ) async def on_stop(event: Event) -> None: @@ -88,34 +107,39 @@ async def async_setup_entry(hass: HomeAssistantType, @callback def async_on_service_call(service: HomeassistantServiceCall) -> None: """Call service when user automation in ESPHome config is triggered.""" - domain, service_name = service.service.split('.', 1) + domain, service_name = service.service.split(".", 1) service_data = service.data if service.data_template: try: - data_template = {key: Template(value) for key, value in - service.data_template.items()} + data_template = { + key: Template(value) for key, value in service.data_template.items() + } template.attach(hass, data_template) - service_data.update(template.render_complex( - data_template, service.variables)) + service_data.update( + template.render_complex(data_template, service.variables) + ) except TemplateError as ex: - _LOGGER.error('Error rendering data template: %s', ex) + _LOGGER.error("Error rendering data template: %s", ex) return if service.is_event: # ESPHome uses servicecall packet for both events and service calls # Ensure the user can only send events of form 'esphome.xyz' - if domain != 'esphome': - _LOGGER.error("Can only generate events under esphome " - "domain!") + if domain != "esphome": + _LOGGER.error("Can only generate events under esphome " "domain!") return hass.bus.async_fire(service.service, service_data) else: - hass.async_create_task(hass.services.async_call( - domain, service_name, service_data, blocking=True)) + hass.async_create_task( + hass.services.async_call( + domain, service_name, service_data, blocking=True + ) + ) - async def send_home_assistant_state(entity_id: str, _, - new_state: Optional[State]) -> None: + async def send_home_assistant_state( + entity_id: str, _, new_state: Optional[State] + ) -> None: """Forward Home Assistant states to ESPHome.""" if new_state is None: return @@ -124,30 +148,27 @@ async def async_setup_entry(hass: HomeAssistantType, @callback def async_on_state_subscription(entity_id: str) -> None: """Subscribe and forward states for requested entities.""" - unsub = async_track_state_change( - hass, entity_id, send_home_assistant_state) + unsub = async_track_state_change(hass, entity_id, send_home_assistant_state) entry_data.disconnect_callbacks.append(unsub) # Send initial state - hass.async_create_task(send_home_assistant_state( - entity_id, None, hass.states.get(entity_id))) + hass.async_create_task( + send_home_assistant_state(entity_id, None, hass.states.get(entity_id)) + ) async def on_login() -> None: """Subscribe to states and list entities on successful API login.""" try: entry_data.device_info = await cli.device_info() entry_data.available = True - await _async_setup_device_registry(hass, entry, - entry_data.device_info) + await _async_setup_device_registry(hass, entry, entry_data.device_info) entry_data.async_update_device_state(hass) entity_infos, services = await cli.list_entities_services() - await entry_data.async_update_static_infos( - hass, entry, entity_infos) + await entry_data.async_update_static_infos(hass, entry, entity_infos) await _setup_services(hass, entry_data, services) await cli.subscribe_states(async_on_state) await cli.subscribe_service_calls(async_on_service_call) - await cli.subscribe_home_assistant_states( - async_on_state_subscription) + await cli.subscribe_home_assistant_states(async_on_state_subscription) hass.async_create_task(entry_data.async_save_to_store()) except APIConnectionError as err: @@ -155,8 +176,7 @@ async def async_setup_entry(hass: HomeAssistantType, # Re-connection logic will trigger after this await cli.disconnect() - try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host, - on_login) + try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host, on_login) async def complete_setup() -> None: """Complete the config entry setup.""" @@ -172,10 +192,11 @@ async def async_setup_entry(hass: HomeAssistantType, return True -async def _setup_auto_reconnect_logic(hass: HomeAssistantType, - cli: APIClient, - entry: ConfigEntry, host: str, on_login): +async def _setup_auto_reconnect_logic( + hass: HomeAssistantType, cli: APIClient, entry: ConfigEntry, host: str, on_login +): """Set up the re-connect logic for the API client.""" + async def try_connect(tries: int = 0, is_disconnect: bool = True) -> None: """Try connecting to the API client. Will retry if not successful.""" if entry.entry_id not in hass.data[DOMAIN]: @@ -207,19 +228,19 @@ async def _setup_auto_reconnect_logic(hass: HomeAssistantType, # notify HA of connectivity directly, but for new we'll use a # really short reconnect interval. tries = min(tries, 10) # prevent OverflowError - wait_time = int(round(min(1.8**tries, 60.0))) + wait_time = int(round(min(1.8 ** tries, 60.0))) _LOGGER.info("Trying to reconnect in %s seconds", wait_time) await asyncio.sleep(wait_time) try: await cli.connect(on_stop=try_connect, login=True) except APIConnectionError as error: - _LOGGER.info("Can't connect to ESPHome API for %s: %s", - host, error) + _LOGGER.info("Can't connect to ESPHome API for %s: %s", host, error) # Schedule re-connect in event loop in order not to delay HA # startup. First connect is scheduled in tracked tasks. data.reconnect_task = hass.loop.create_task( - try_connect(tries + 1, is_disconnect=False)) + try_connect(tries + 1, is_disconnect=False) + ) else: _LOGGER.info("Successfully connected to %s", host) hass.async_create_task(on_login()) @@ -227,30 +248,28 @@ async def _setup_auto_reconnect_logic(hass: HomeAssistantType, return try_connect -async def _async_setup_device_registry(hass: HomeAssistantType, - entry: ConfigEntry, - device_info: DeviceInfo): +async def _async_setup_device_registry( + hass: HomeAssistantType, entry: ConfigEntry, device_info: DeviceInfo +): """Set up device registry feature for a particular config entry.""" sw_version = device_info.esphome_version if device_info.compilation_time: - sw_version += ' ({})'.format(device_info.compilation_time) + sw_version += " ({})".format(device_info.compilation_time) device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={ - (dr.CONNECTION_NETWORK_MAC, device_info.mac_address) - }, + connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, name=device_info.name, - manufacturer='espressif', + manufacturer="espressif", model=device_info.model, sw_version=sw_version, ) -async def _register_service(hass: HomeAssistantType, - entry_data: RuntimeEntryData, - service: UserService): - service_name = '{}_{}'.format(entry_data.device_info.name, service.name) +async def _register_service( + hass: HomeAssistantType, entry_data: RuntimeEntryData, service: UserService +): + service_name = "{}_{}".format(entry_data.device_info.name, service.name) schema = {} for arg in service.args: schema[vol.Required(arg.name)] = { @@ -267,13 +286,14 @@ async def _register_service(hass: HomeAssistantType, async def execute_service(call): await entry_data.client.execute_service(service, call.data) - hass.services.async_register(DOMAIN, service_name, execute_service, - vol.Schema(schema)) + hass.services.async_register( + DOMAIN, service_name, execute_service, vol.Schema(schema) + ) -async def _setup_services(hass: HomeAssistantType, - entry_data: RuntimeEntryData, - services: List[UserService]): +async def _setup_services( + hass: HomeAssistantType, entry_data: RuntimeEntryData, services: List[UserService] +): old_services = entry_data.services.copy() to_unregister = [] to_register = [] @@ -295,16 +315,16 @@ async def _setup_services(hass: HomeAssistantType, entry_data.services = {serv.key: serv for serv in services} for service in to_unregister: - service_name = '{}_{}'.format(entry_data.device_info.name, - service.name) + service_name = "{}_{}".format(entry_data.device_info.name, service.name) hass.services.async_remove(DOMAIN, service_name) for service in to_register: await _register_service(hass, entry_data, service) -async def _cleanup_instance(hass: HomeAssistantType, - entry: ConfigEntry) -> RuntimeEntryData: +async def _cleanup_instance( + hass: HomeAssistantType, entry: ConfigEntry +) -> RuntimeEntryData: """Cleanup the esphome client if it exists.""" data = hass.data[DATA_KEY].pop(entry.entry_id) # type: RuntimeEntryData if data.reconnect_task is not None: @@ -317,28 +337,27 @@ async def _cleanup_instance(hass: HomeAssistantType, return data -async def async_unload_entry(hass: HomeAssistantType, - entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload an esphome config entry.""" entry_data = await _cleanup_instance(hass, entry) tasks = [] for platform in entry_data.loaded_platforms: - tasks.append(hass.config_entries.async_forward_entry_unload( - entry, platform)) + tasks.append(hass.config_entries.async_forward_entry_unload(entry, platform)) if tasks: await asyncio.wait(tasks) return True -async def platform_async_setup_entry(hass: HomeAssistantType, - entry: ConfigEntry, - async_add_entities, - *, - component_key: str, - info_type, - entity_type, - state_type - ) -> None: +async def platform_async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities, + *, + component_key: str, + info_type, + entity_type, + state_type, +) -> None: """Set up an esphome platform. This method is in charge of receiving, distributing and storing @@ -399,6 +418,7 @@ def esphome_state_property(func): This checks if the state object in the entity is set, and prevents writing NAN values to the Home Assistant state machine. """ + @property def _wrapper(self): # pylint: disable=protected-access @@ -410,6 +430,7 @@ def esphome_state_property(func): # (not JSON serializable) return None return val + return _wrapper @@ -452,26 +473,28 @@ class EsphomeEntity(Entity): async def async_added_to_hass(self) -> None: """Register callbacks.""" kwargs = { - 'entry_id': self._entry_id, - 'component_key': self._component_key, - 'key': self._key, + "entry_id": self._entry_id, + "component_key": self._component_key, + "key": self._key, } self._remove_callbacks.append( - async_dispatcher_connect(self.hass, - DISPATCHER_UPDATE_ENTITY.format(**kwargs), - self._on_update) - ) - - self._remove_callbacks.append( - async_dispatcher_connect(self.hass, - DISPATCHER_REMOVE_ENTITY.format(**kwargs), - self.async_remove) + async_dispatcher_connect( + self.hass, DISPATCHER_UPDATE_ENTITY.format(**kwargs), self._on_update + ) ) self._remove_callbacks.append( async_dispatcher_connect( - self.hass, DISPATCHER_ON_DEVICE_UPDATE.format(**kwargs), - self.async_schedule_update_ha_state) + self.hass, DISPATCHER_REMOVE_ENTITY.format(**kwargs), self.async_remove + ) + ) + + self._remove_callbacks.append( + async_dispatcher_connect( + self.hass, + DISPATCHER_ON_DEVICE_UPDATE.format(**kwargs), + self.async_schedule_update_ha_state, + ) ) async def _on_update(self) -> None: @@ -530,8 +553,7 @@ class EsphomeEntity(Entity): def device_info(self) -> Dict[str, Any]: """Return device registry information for this entity.""" return { - 'connections': {(dr.CONNECTION_NETWORK_MAC, - self._device_info.mac_address)} + "connections": {(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)} } @property diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 75a7235c58f..4e684638bb7 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -14,10 +14,13 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): """Set up ESPHome binary sensors based on a config entry.""" await platform_async_setup_entry( - hass, entry, async_add_entities, - component_key='binary_sensor', - info_type=BinarySensorInfo, entity_type=EsphomeBinarySensor, - state_type=BinarySensorState + hass, + entry, + async_add_entities, + component_key="binary_sensor", + info_type=BinarySensorInfo, + entity_type=EsphomeBinarySensor, + state_type=BinarySensorState, ) diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 54f774bc426..cc2e0cede23 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -15,14 +15,18 @@ from . import EsphomeEntity, platform_async_setup_entry _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, - entry: ConfigEntry, async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up esphome cameras based on a config entry.""" await platform_async_setup_entry( - hass, entry, async_add_entities, - component_key='camera', - info_type=CameraInfo, entity_type=EsphomeCamera, - state_type=CameraState + hass, + entry, + async_add_entities, + component_key="camera", + info_type=CameraInfo, + entity_type=EsphomeCamera, + state_type=CameraState, ) @@ -74,5 +78,5 @@ class EsphomeCamera(Camera, EsphomeEntity): async def handle_async_mjpeg_stream(self, request): """Serve an HTTP MJPEG stream from the camera.""" return await camera.async_get_still_stream( - request, self._async_camera_stream_image, - camera.DEFAULT_CONTENT_TYPE, 0.0) + request, self._async_camera_stream_image, camera.DEFAULT_CONTENT_TYPE, 0.0 + ) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 2892342ac59..7337aec4541 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -6,18 +6,32 @@ from aioesphomeapi import ClimateInfo, ClimateMode, ClimateState from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE_RANGE, PRESET_AWAY, - HVAC_MODE_OFF) + ATTR_HVAC_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE_RANGE, + PRESET_AWAY, + HVAC_MODE_OFF, +) from homeassistant.const import ( - ATTR_TEMPERATURE, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, - TEMP_CELSIUS) + ATTR_TEMPERATURE, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, + TEMP_CELSIUS, +) from . import ( - EsphomeEntity, esphome_map_enum, esphome_state_property, - platform_async_setup_entry) + EsphomeEntity, + esphome_map_enum, + esphome_state_property, + platform_async_setup_entry, +) _LOGGER = logging.getLogger(__name__) @@ -25,10 +39,13 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): """Set up ESPHome climate devices based on a config entry.""" await platform_async_setup_entry( - hass, entry, async_add_entities, - component_key='climate', - info_type=ClimateInfo, entity_type=EsphomeClimateDevice, - state_type=ClimateState + hass, + entry, + async_add_entities, + component_key="climate", + info_type=ClimateInfo, + entity_type=EsphomeClimateDevice, + state_type=ClimateState, ) @@ -76,6 +93,11 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): for mode in self._static_info.supported_modes ] + @property + def preset_modes(self): + """Return preset modes.""" + return [PRESET_AWAY] if self._static_info.supports_away else [] + @property def target_temperature_step(self) -> float: """Return the supported step of target temperature.""" @@ -97,7 +119,7 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): """Return the list of supported features.""" features = 0 if self._static_info.supports_two_point_target_temperature: - features |= (SUPPORT_TARGET_TEMPERATURE_RANGE) + features |= SUPPORT_TARGET_TEMPERATURE_RANGE else: features |= SUPPORT_TARGET_TEMPERATURE if self._static_info.supports_away: @@ -109,6 +131,11 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): """Return current operation ie. heat, cool, idle.""" return _climate_modes.from_esphome(self._state.mode) + @esphome_state_property + def preset_mode(self): + """Return current preset mode.""" + return PRESET_AWAY if self._state.away else None + @esphome_state_property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" @@ -131,43 +158,24 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature (and operation mode if set).""" - data = {'key': self._static_info.key} + data = {"key": self._static_info.key} if ATTR_HVAC_MODE in kwargs: - data['mode'] = _climate_modes.from_hass( - kwargs[ATTR_HVAC_MODE]) + data["mode"] = _climate_modes.from_hass(kwargs[ATTR_HVAC_MODE]) if ATTR_TEMPERATURE in kwargs: - data['target_temperature'] = kwargs[ATTR_TEMPERATURE] + data["target_temperature"] = kwargs[ATTR_TEMPERATURE] if ATTR_TARGET_TEMP_LOW in kwargs: - data['target_temperature_low'] = kwargs[ATTR_TARGET_TEMP_LOW] + data["target_temperature_low"] = kwargs[ATTR_TARGET_TEMP_LOW] if ATTR_TARGET_TEMP_HIGH in kwargs: - data['target_temperature_high'] = kwargs[ATTR_TARGET_TEMP_HIGH] + data["target_temperature_high"] = kwargs[ATTR_TARGET_TEMP_HIGH] await self._client.climate_command(**data) - async def async_set_operation_mode(self, operation_mode) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target operation mode.""" await self._client.climate_command( - key=self._static_info.key, - mode=_climate_modes.from_hass(operation_mode), + key=self._static_info.key, mode=_climate_modes.from_hass(hvac_mode) ) - @property - def preset_mode(self): - """Return current preset mode.""" - if self._state and self._state.away: - return PRESET_AWAY - - return None - - @property - def preset_modes(self): - """Return preset modes.""" - if self._static_info.supports_away: - return [PRESET_AWAY] - - return [] - async def async_set_preset_mode(self, preset_mode): """Set preset mode.""" away = preset_mode == PRESET_AWAY - await self._client.climate_command(key=self._static_info.key, - away=away) + await self._client.climate_command(key=self._static_info.key, away=away) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 2ce749d6ae9..35389d055d6 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.helpers import ConfigType from .entry_data import DATA_KEY, RuntimeEntryData -@config_entries.HANDLERS.register('esphome') +@config_entries.HANDLERS.register("esphome") class EsphomeFlowHandler(config_entries.ConfigFlow): """Handle a esphome config flow.""" @@ -23,43 +23,40 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): self._port = None # type: Optional[int] self._password = None # type: Optional[str] - async def async_step_user(self, user_input: Optional[ConfigType] = None, - error: Optional[str] = None): + async def async_step_user( + self, user_input: Optional[ConfigType] = None, error: Optional[str] = None + ): """Handle a flow initialized by the user.""" if user_input is not None: return await self._async_authenticate_or_add(user_input) fields = OrderedDict() - fields[vol.Required('host', default=self._host or vol.UNDEFINED)] = str - fields[vol.Optional('port', default=self._port or 6053)] = int + fields[vol.Required("host", default=self._host or vol.UNDEFINED)] = str + fields[vol.Optional("port", default=self._port or 6053)] = int errors = {} if error is not None: - errors['base'] = error + errors["base"] = error return self.async_show_form( - step_id='user', - data_schema=vol.Schema(fields), - errors=errors + step_id="user", data_schema=vol.Schema(fields), errors=errors ) @property def _name(self): - return self.context.get('name') + return self.context.get("name") @_name.setter def _name(self, value): # pylint: disable=unsupported-assignment-operation - self.context['name'] = value - self.context['title_placeholders'] = { - 'name': self._name - } + self.context["name"] = value + self.context["title_placeholders"] = {"name": self._name} def _set_user_input(self, user_input): if user_input is None: return - self._host = user_input['host'] - self._port = user_input['port'] + self._host = user_input["host"] + self._port = user_input["port"] async def _async_authenticate_or_add(self, user_input): self._set_user_input(user_input) @@ -79,42 +76,42 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): if user_input is not None: return await self._async_authenticate_or_add(None) return self.async_show_form( - step_id='discovery_confirm', - description_placeholders={'name': self._name}, + step_id="discovery_confirm", description_placeholders={"name": self._name} ) async def async_step_zeroconf(self, user_input: ConfigType): """Handle zeroconf discovery.""" # Hostname is format: livingroom.local. - local_name = user_input['hostname'][:-1] - node_name = local_name[:-len('.local')] - address = user_input['properties'].get('address', local_name) + local_name = user_input["hostname"][:-1] + node_name = local_name[: -len(".local")] + address = user_input["properties"].get("address", local_name) # Check if already configured for entry in self._async_current_entries(): already_configured = False - if entry.data['host'] == address: + if entry.data["host"] == address: # Is this address already configured? already_configured = True elif entry.entry_id in self.hass.data.get(DATA_KEY, {}): # Does a config entry with this name already exist? data = self.hass.data[DATA_KEY][ - entry.entry_id] # type: RuntimeEntryData + entry.entry_id + ] # type: RuntimeEntryData # Node names are unique in the network if data.device_info is not None: already_configured = data.device_info.name == node_name if already_configured: - return self.async_abort(reason='already_configured') + return self.async_abort(reason="already_configured") self._host = address - self._port = user_input['port'] + self._port = user_input["port"] self._name = node_name # Check if flow for this device already in progress for flow in self._async_in_progress(): - if flow['context'].get('name') == node_name: - return self.async_abort(reason='already_configured') + if flow["context"].get("name") == node_name: + return self.async_abort(reason="already_configured") return await self.async_step_discovery_confirm() @@ -122,17 +119,17 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): return self.async_create_entry( title=self._name, data={ - 'host': self._host, - 'port': self._port, + "host": self._host, + "port": self._port, # The API uses protobuf, so empty string denotes absence - 'password': self._password or '', - } + "password": self._password or "", + }, ) async def async_step_authenticate(self, user_input=None, error=None): """Handle getting password for authentication.""" if user_input is not None: - self._password = user_input['password'] + self._password = user_input["password"] error = await self.try_login() if error: return await self.async_step_authenticate(error=error) @@ -140,30 +137,28 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): errors = {} if error is not None: - errors['base'] = error + errors["base"] = error return self.async_show_form( - step_id='authenticate', - data_schema=vol.Schema({ - vol.Required('password'): str - }), - description_placeholders={'name': self._name}, - errors=errors + step_id="authenticate", + data_schema=vol.Schema({vol.Required("password"): str}), + description_placeholders={"name": self._name}, + errors=errors, ) async def fetch_device_info(self): """Fetch device info from API and return any errors.""" from aioesphomeapi import APIClient, APIConnectionError - cli = APIClient(self.hass.loop, self._host, self._port, '') + cli = APIClient(self.hass.loop, self._host, self._port, "") try: await cli.connect() device_info = await cli.device_info() except APIConnectionError as err: - if 'resolving' in str(err): - return 'resolve_error', None - return 'connection_error', None + if "resolving" in str(err): + return "resolve_error", None + return "connection_error", None finally: await cli.disconnect(force=True) @@ -179,6 +174,6 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): await cli.connect(login=True) except APIConnectionError: await cli.disconnect(force=True) - return 'invalid_password' + return "invalid_password" return None diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index b69b62075db..7da2fcee380 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -5,9 +5,17 @@ from typing import Optional from aioesphomeapi import CoverInfo, CoverState from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, - SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, - SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, CoverDevice) + ATTR_POSITION, + ATTR_TILT_POSITION, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, + CoverDevice, +) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -16,14 +24,18 @@ from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, - entry: ConfigEntry, async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up ESPHome covers based on a config entry.""" await platform_async_setup_entry( - hass, entry, async_add_entities, - component_key='cover', - info_type=CoverInfo, entity_type=EsphomeCover, - state_type=CoverState + hass, + entry, + async_add_entities, + component_key="cover", + info_type=CoverInfo, + entity_type=EsphomeCover, + state_type=CoverState, ) @@ -41,8 +53,7 @@ class EsphomeCover(EsphomeEntity, CoverDevice): if self._static_info.supports_position: flags |= SUPPORT_SET_POSITION if self._static_info.supports_tilt: - flags |= (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | - SUPPORT_SET_TILT_POSITION) + flags |= SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_SET_TILT_POSITION return flags @property @@ -69,12 +80,14 @@ class EsphomeCover(EsphomeEntity, CoverDevice): def is_opening(self) -> bool: """Return if the cover is opening or not.""" from aioesphomeapi import CoverOperation + return self._state.current_operation == CoverOperation.IS_OPENING @esphome_state_property def is_closing(self) -> bool: """Return if the cover is closing or not.""" from aioesphomeapi import CoverOperation + return self._state.current_operation == CoverOperation.IS_CLOSING @esphome_state_property @@ -93,13 +106,11 @@ class EsphomeCover(EsphomeEntity, CoverDevice): async def async_open_cover(self, **kwargs) -> None: """Open the cover.""" - await self._client.cover_command(key=self._static_info.key, - position=1.0) + await self._client.cover_command(key=self._static_info.key, position=1.0) async def async_close_cover(self, **kwargs) -> None: """Close cover.""" - await self._client.cover_command(key=self._static_info.key, - position=0.0) + await self._client.cover_command(key=self._static_info.key, position=0.0) async def async_stop_cover(self, **kwargs) -> None: """Stop the cover.""" @@ -107,8 +118,9 @@ class EsphomeCover(EsphomeEntity, CoverDevice): async def async_set_cover_position(self, **kwargs) -> None: """Move the cover to a specific position.""" - await self._client.cover_command(key=self._static_info.key, - position=kwargs[ATTR_POSITION] / 100) + await self._client.cover_command( + key=self._static_info.key, position=kwargs[ATTR_POSITION] / 100 + ) async def async_open_cover_tilt(self, **kwargs) -> None: """Open the cover tilt.""" @@ -120,5 +132,6 @@ class EsphomeCover(EsphomeEntity, CoverDevice): async def async_set_cover_tilt_position(self, **kwargs) -> None: """Move the cover tilt to a specific position.""" - await self._client.cover_command(key=self._static_info.key, - tilt=kwargs[ATTR_TILT_POSITION] / 100) + await self._client.cover_command( + key=self._static_info.key, tilt=kwargs[ATTR_TILT_POSITION] / 100 + ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 4e78718b760..b7f9ad9b347 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -3,10 +3,21 @@ import asyncio from typing import Any, Callable, Dict, List, Optional, Tuple, Set from aioesphomeapi import ( - COMPONENT_TYPE_TO_INFO, DeviceInfo, EntityInfo, EntityState, UserService, + COMPONENT_TYPE_TO_INFO, + DeviceInfo, + EntityInfo, + EntityState, + UserService, BinarySensorInfo, - CameraInfo, ClimateInfo, CoverInfo, FanInfo, LightInfo, SensorInfo, - SwitchInfo, TextSensorInfo) + CameraInfo, + ClimateInfo, + CoverInfo, + FanInfo, + LightInfo, + SensorInfo, + SwitchInfo, + TextSensorInfo, +) import attr from homeassistant.config_entries import ConfigEntry @@ -14,24 +25,24 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import HomeAssistantType -DATA_KEY = 'esphome' -DISPATCHER_UPDATE_ENTITY = 'esphome_{entry_id}_update_{component_key}_{key}' -DISPATCHER_REMOVE_ENTITY = 'esphome_{entry_id}_remove_{component_key}_{key}' -DISPATCHER_ON_LIST = 'esphome_{entry_id}_on_list' -DISPATCHER_ON_DEVICE_UPDATE = 'esphome_{entry_id}_on_device_update' -DISPATCHER_ON_STATE = 'esphome_{entry_id}_on_state' +DATA_KEY = "esphome" +DISPATCHER_UPDATE_ENTITY = "esphome_{entry_id}_update_{component_key}_{key}" +DISPATCHER_REMOVE_ENTITY = "esphome_{entry_id}_remove_{component_key}_{key}" +DISPATCHER_ON_LIST = "esphome_{entry_id}_on_list" +DISPATCHER_ON_DEVICE_UPDATE = "esphome_{entry_id}_on_device_update" +DISPATCHER_ON_STATE = "esphome_{entry_id}_on_state" # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM = { - BinarySensorInfo: 'binary_sensor', - CameraInfo: 'camera', - ClimateInfo: 'climate', - CoverInfo: 'cover', - FanInfo: 'fan', - LightInfo: 'light', - SensorInfo: 'sensor', - SwitchInfo: 'switch', - TextSensorInfo: 'sensor', + BinarySensorInfo: "binary_sensor", + CameraInfo: "camera", + ClimateInfo: "climate", + CoverInfo: "cover", + FanInfo: "fan", + LightInfo: "light", + SensorInfo: "sensor", + SwitchInfo: "switch", + TextSensorInfo: "sensor", } @@ -40,12 +51,12 @@ class RuntimeEntryData: """Store runtime data for esphome config entries.""" entry_id = attr.ib(type=str) - client = attr.ib(type='APIClient') + client = attr.ib(type="APIClient") store = attr.ib(type=Store) reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None) state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) - services = attr.ib(type=Dict[int, 'UserService'], factory=dict) + services = attr.ib(type=Dict[int, "UserService"], factory=dict) available = attr.ib(type=bool, default=False) device_info = attr.ib(type=DeviceInfo, default=None) cleanup_callbacks = attr.ib(type=List[Callable[[], None]], factory=list) @@ -53,36 +64,41 @@ class RuntimeEntryData: loaded_platforms = attr.ib(type=Set[str], factory=set) platform_load_lock = attr.ib(type=asyncio.Lock, factory=asyncio.Lock) - def async_update_entity(self, hass: HomeAssistantType, component_key: str, - key: int) -> None: + def async_update_entity( + self, hass: HomeAssistantType, component_key: str, key: int + ) -> None: """Schedule the update of an entity.""" signal = DISPATCHER_UPDATE_ENTITY.format( - entry_id=self.entry_id, component_key=component_key, key=key) + entry_id=self.entry_id, component_key=component_key, key=key + ) async_dispatcher_send(hass, signal) - def async_remove_entity(self, hass: HomeAssistantType, component_key: str, - key: int) -> None: + def async_remove_entity( + self, hass: HomeAssistantType, component_key: str, key: int + ) -> None: """Schedule the removal of an entity.""" signal = DISPATCHER_REMOVE_ENTITY.format( - entry_id=self.entry_id, component_key=component_key, key=key) + entry_id=self.entry_id, component_key=component_key, key=key + ) async_dispatcher_send(hass, signal) - async def _ensure_platforms_loaded(self, hass: HomeAssistantType, - entry: ConfigEntry, - platforms: Set[str]): + async def _ensure_platforms_loaded( + self, hass: HomeAssistantType, entry: ConfigEntry, platforms: Set[str] + ): async with self.platform_load_lock: needed = platforms - self.loaded_platforms tasks = [] for platform in needed: - tasks.append(hass.config_entries.async_forward_entry_setup( - entry, platform)) + tasks.append( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) if tasks: await asyncio.wait(tasks) self.loaded_platforms |= needed async def async_update_static_infos( - self, hass: HomeAssistantType, entry: ConfigEntry, - infos: List[EntityInfo]) -> None: + self, hass: HomeAssistantType, entry: ConfigEntry, infos: List[EntityInfo] + ) -> None: """Distribute an update of static infos to all platforms.""" # First, load all platforms needed_platforms = set() @@ -97,8 +113,7 @@ class RuntimeEntryData: signal = DISPATCHER_ON_LIST.format(entry_id=self.entry_id) async_dispatcher_send(hass, signal, infos) - def async_update_state(self, hass: HomeAssistantType, - state: EntityState) -> None: + def async_update_state(self, hass: HomeAssistantType, state: EntityState) -> None: """Distribute an update of state information to all platforms.""" signal = DISPATCHER_ON_STATE.format(entry_id=self.entry_id) async_dispatcher_send(hass, signal, state) @@ -108,15 +123,15 @@ class RuntimeEntryData: signal = DISPATCHER_ON_DEVICE_UPDATE.format(entry_id=self.entry_id) async_dispatcher_send(hass, signal) - async def async_load_from_store(self) -> Tuple[List[EntityInfo], - List[UserService]]: + async def async_load_from_store(self) -> Tuple[List[EntityInfo], List[UserService]]: """Load the retained data from store and return de-serialized data.""" restored = await self.store.async_load() if restored is None: return [], [] - self.device_info = _attr_obj_from_dict(DeviceInfo, - **restored.pop('device_info')) + self.device_info = _attr_obj_from_dict( + DeviceInfo, **restored.pop("device_info") + ) infos = [] for comp_type, restored_infos in restored.items(): if comp_type not in COMPONENT_TYPE_TO_INFO: @@ -125,26 +140,21 @@ class RuntimeEntryData: cls = COMPONENT_TYPE_TO_INFO[comp_type] infos.append(_attr_obj_from_dict(cls, **info)) services = [] - for service in restored.get('services', []): + for service in restored.get("services", []): services.append(UserService.from_dict(service)) return infos, services async def async_save_to_store(self) -> None: """Generate dynamic data to store and save it to the filesystem.""" - store_data = { - 'device_info': attr.asdict(self.device_info), - 'services': [] - } + store_data = {"device_info": attr.asdict(self.device_info), "services": []} for comp_type, infos in self.info.items(): - store_data[comp_type] = [attr.asdict(info) - for info in infos.values()] + store_data[comp_type] = [attr.asdict(info) for info in infos.values()] for service in self.services.values(): - store_data['services'].append(service.to_dict()) + store_data["services"].append(service.to_dict()) await self.store.async_save(store_data) def _attr_obj_from_dict(cls, **kwargs): - return cls(**{key: kwargs[key] for key in attr.fields_dict(cls) - if key in kwargs}) + return cls(**{key: kwargs[key] for key in attr.fields_dict(cls) if key in kwargs}) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 255bdaa8cb1..44059673f15 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -5,26 +5,39 @@ from typing import List, Optional from aioesphomeapi import FanInfo, FanSpeed, FanState from homeassistant.components.fan import ( - SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_OSCILLATE, - SUPPORT_SET_SPEED, FanEntity) + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, + FanEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from . import ( - EsphomeEntity, esphome_map_enum, esphome_state_property, - platform_async_setup_entry) + EsphomeEntity, + esphome_map_enum, + esphome_state_property, + platform_async_setup_entry, +) _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, - entry: ConfigEntry, async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up ESPHome fans based on a config entry.""" await platform_async_setup_entry( - hass, entry, async_add_entities, - component_key='fan', - info_type=FanInfo, entity_type=EsphomeFan, - state_type=FanState + hass, + entry, + async_add_entities, + component_key="fan", + info_type=FanInfo, + entity_type=EsphomeFan, + state_type=FanState, ) @@ -55,17 +68,17 @@ class EsphomeFan(EsphomeEntity, FanEntity): return await self._client.fan_command( - self._static_info.key, speed=_fan_speeds.from_hass(speed)) + self._static_info.key, speed=_fan_speeds.from_hass(speed) + ) - async def async_turn_on(self, speed: Optional[str] = None, - **kwargs) -> None: + async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None: """Turn on the fan.""" if speed == SPEED_OFF: await self.async_turn_off() return - data = {'key': self._static_info.key, 'state': True} + data = {"key": self._static_info.key, "state": True} if speed is not None: - data['speed'] = _fan_speeds.from_hass(speed) + data["speed"] = _fan_speeds.from_hass(speed) await self._client.fan_command(**data) # pylint: disable=arguments-differ @@ -75,8 +88,9 @@ class EsphomeFan(EsphomeEntity, FanEntity): async def async_oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" - await self._client.fan_command(key=self._static_info.key, - oscillating=oscillating) + await self._client.fan_command( + key=self._static_info.key, oscillating=oscillating + ) @esphome_state_property def is_on(self) -> Optional[bool]: diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index f94229d61cc..e455d5581d1 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -5,10 +5,24 @@ from typing import List, Optional, Tuple from aioesphomeapi import LightInfo, LightState from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, - ATTR_TRANSITION, ATTR_WHITE_VALUE, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, Light) + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_FLASH, + ATTR_HS_COLOR, + ATTR_TRANSITION, + ATTR_WHITE_VALUE, + FLASH_LONG, + FLASH_SHORT, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_FLASH, + SUPPORT_TRANSITION, + SUPPORT_WHITE_VALUE, + Light, +) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util @@ -18,20 +32,21 @@ from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry _LOGGER = logging.getLogger(__name__) -FLASH_LENGTHS = { - FLASH_SHORT: 2, - FLASH_LONG: 10, -} +FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} -async def async_setup_entry(hass: HomeAssistantType, - entry: ConfigEntry, async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up ESPHome lights based on a config entry.""" await platform_async_setup_entry( - hass, entry, async_add_entities, - component_key='light', - info_type=LightInfo, entity_type=EsphomeLight, - state_type=LightState + hass, + entry, + async_add_entities, + component_key="light", + info_type=LightInfo, + entity_type=EsphomeLight, + state_type=LightState, ) @@ -53,32 +68,32 @@ class EsphomeLight(EsphomeEntity, Light): async def async_turn_on(self, **kwargs) -> None: """Turn the entity on.""" - data = {'key': self._static_info.key, 'state': True} + data = {"key": self._static_info.key, "state": True} if ATTR_HS_COLOR in kwargs: hue, sat = kwargs[ATTR_HS_COLOR] red, green, blue = color_util.color_hsv_to_RGB(hue, sat, 100) - data['rgb'] = (red / 255, green / 255, blue / 255) + data["rgb"] = (red / 255, green / 255, blue / 255) if ATTR_FLASH in kwargs: - data['flash'] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] + data["flash"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] if ATTR_TRANSITION in kwargs: - data['transition_length'] = kwargs[ATTR_TRANSITION] + data["transition_length"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs: - data['brightness'] = kwargs[ATTR_BRIGHTNESS] / 255 + data["brightness"] = kwargs[ATTR_BRIGHTNESS] / 255 if ATTR_COLOR_TEMP in kwargs: - data['color_temperature'] = kwargs[ATTR_COLOR_TEMP] + data["color_temperature"] = kwargs[ATTR_COLOR_TEMP] if ATTR_EFFECT in kwargs: - data['effect'] = kwargs[ATTR_EFFECT] + data["effect"] = kwargs[ATTR_EFFECT] if ATTR_WHITE_VALUE in kwargs: - data['white'] = kwargs[ATTR_WHITE_VALUE] / 255 + data["white"] = kwargs[ATTR_WHITE_VALUE] / 255 await self._client.light_command(**data) async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" - data = {'key': self._static_info.key, 'state': False} + data = {"key": self._static_info.key, "state": False} if ATTR_FLASH in kwargs: - data['flash'] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] + data["flash"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] if ATTR_TRANSITION in kwargs: - data['transition_length'] = kwargs[ATTR_TRANSITION] + data["transition_length"] = kwargs[ATTR_TRANSITION] await self._client.light_command(**data) @esphome_state_property @@ -90,9 +105,8 @@ class EsphomeLight(EsphomeEntity, Light): def hs_color(self) -> Optional[Tuple[float, float]]: """Return the hue and saturation color value [float, float].""" return color_util.color_RGB_to_hs( - self._state.red * 255, - self._state.green * 255, - self._state.blue * 255) + self._state.red * 255, self._state.green * 255, self._state.blue * 255 + ) @esphome_state_property def color_temp(self) -> Optional[float]: diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index a5a530b49f1..3168bae7ec8 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -3,8 +3,7 @@ import logging import math from typing import Optional -from aioesphomeapi import ( - SensorInfo, SensorState, TextSensorInfo, TextSensorState) +from aioesphomeapi import SensorInfo, SensorState, TextSensorInfo, TextSensorState from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -14,20 +13,27 @@ from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, - entry: ConfigEntry, async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up esphome sensors based on a config entry.""" await platform_async_setup_entry( - hass, entry, async_add_entities, - component_key='sensor', - info_type=SensorInfo, entity_type=EsphomeSensor, - state_type=SensorState + hass, + entry, + async_add_entities, + component_key="sensor", + info_type=SensorInfo, + entity_type=EsphomeSensor, + state_type=SensorState, ) await platform_async_setup_entry( - hass, entry, async_add_entities, - component_key='text_sensor', - info_type=TextSensorInfo, entity_type=EsphomeTextSensor, - state_type=TextSensorState + hass, + entry, + async_add_entities, + component_key="text_sensor", + info_type=TextSensorInfo, + entity_type=EsphomeTextSensor, + state_type=TextSensorState, ) @@ -52,8 +58,9 @@ class EsphomeSensor(EsphomeEntity): """Return the state of the entity.""" if math.isnan(self._state.state): return None - return '{:.{prec}f}'.format( - self._state.state, prec=self._static_info.accuracy_decimals) + return "{:.{prec}f}".format( + self._state.state, prec=self._static_info.accuracy_decimals + ) @property def unit_of_measurement(self) -> str: @@ -65,11 +72,11 @@ class EsphomeTextSensor(EsphomeEntity): """A text sensor implementation for ESPHome.""" @property - def _static_info(self) -> 'TextSensorInfo': + def _static_info(self) -> "TextSensorInfo": return super()._static_info @property - def _state(self) -> Optional['TextSensorState']: + def _state(self) -> Optional["TextSensorState"]: return super()._state @property diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index d209df8cd83..f66bfaa39f3 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -13,14 +13,18 @@ from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, - entry: ConfigEntry, async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up ESPHome switches based on a config entry.""" await platform_async_setup_entry( - hass, entry, async_add_entities, - component_key='switch', - info_type=SwitchInfo, entity_type=EsphomeSwitch, - state_type=SwitchState + hass, + entry, + async_add_entities, + component_key="switch", + info_type=SwitchInfo, + entity_type=EsphomeSwitch, + state_type=SwitchState, ) diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py index e77b256abb7..83d3164e3ff 100644 --- a/homeassistant/components/essent/sensor.py +++ b/homeassistant/components/essent/sensor.py @@ -5,18 +5,16 @@ from pyessent import PyEssent import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, ENERGY_KILO_WATT_HOUR) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, ENERGY_KILO_WATT_HOUR import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle SCAN_INTERVAL = timedelta(hours=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} +) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -28,26 +26,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None): meters = [] for meter in essent.retrieve_meters(): data = essent.retrieve_meter_data(meter) - for tariff in data['values']['LVR'].keys(): - meters.append(EssentMeter( - essent, - meter, - data['type'], - tariff, - data['values']['LVR'][tariff]['unit'])) + for tariff in data["values"]["LVR"].keys(): + meters.append( + EssentMeter( + essent, + meter, + data["type"], + tariff, + data["values"]["LVR"][tariff]["unit"], + ) + ) if not meters: hass.components.persistent_notification.create( - 'Couldn\'t find any meter readings. ' - 'Please ensure Verbruiks Manager is enabled in Mijn Essent ' - 'and at least one reading has been logged to Meterstanden.', - title='Essent', notification_id='essent_notification') + "Couldn't find any meter readings. " + "Please ensure Verbruiks Manager is enabled in Mijn Essent " + "and at least one reading has been logged to Meterstanden.", + title="Essent", + notification_id="essent_notification", + ) return add_devices(meters, True) -class EssentBase(): +class EssentBase: """Essent Base.""" def __init__(self, username, password): @@ -72,8 +75,7 @@ class EssentBase(): essent = PyEssent(self._username, self._password) eans = essent.get_EANs() for possible_meter in eans: - meter_data = essent.read_meter( - possible_meter, only_last_meter_reading=True) + meter_data = essent.read_meter(possible_meter, only_last_meter_reading=True) if meter_data: self._meter_data[possible_meter] = meter_data @@ -103,7 +105,7 @@ class EssentMeter(Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - if self._unit.lower() == 'kwh': + if self._unit.lower() == "kwh": return ENERGY_KILO_WATT_HOUR return self._unit @@ -118,4 +120,5 @@ class EssentMeter(Entity): # Set our value self._state = next( - iter(data['values']['LVR'][self._tariff]['records'].values())) + iter(data["values"]["LVR"][self._tariff]["records"].values()) + ) diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 83805ec4d20..9cabb2762b0 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -4,23 +4,24 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME, CONF_TOKEN) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity ATTRIBUTION = "Data provided by etherscan.io" -CONF_TOKEN_ADDRESS = 'token_address' +CONF_TOKEN_ADDRESS = "token_address" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_TOKEN): cv.string, - vol.Optional(CONF_TOKEN_ADDRESS): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_TOKEN): cv.string, + vol.Optional(CONF_TOKEN_ADDRESS): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -75,6 +76,7 @@ class EtherscanSensor(Entity): def update(self): """Get the latest state of the sensor.""" from pyetherscan import get_balance + if self._token_address: self._state = get_balance(self._address, self._token_address) elif self._token: diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index 8425780b76b..df6aed3582f 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -4,39 +4,53 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_ACCESS_TOKEN, CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PASSWORD, - CONF_TYPE, CONF_USERNAME) + CONF_ACCESS_TOKEN, + CONF_ADDRESS, + CONF_DEVICES, + CONF_NAME, + CONF_PASSWORD, + CONF_TYPE, + CONF_USERNAME, +) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DOMAIN = 'eufy' +DOMAIN = "eufy" -DEVICE_SCHEMA = vol.Schema({ - vol.Required(CONF_ADDRESS): cv.string, - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Required(CONF_TYPE): cv.string, - vol.Optional(CONF_NAME): cv.string -}) +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_TYPE): cv.string, + vol.Optional(CONF_NAME): cv.string, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_DEVICES, default=[]): - vol.All(cv.ensure_list, [DEVICE_SCHEMA]), - vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, - vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_DEVICES, default=[]): vol.All( + cv.ensure_list, [DEVICE_SCHEMA] + ), + vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, + vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) EUFY_DISPATCH = { - 'T1011': 'light', - 'T1012': 'light', - 'T1013': 'light', - 'T1201': 'switch', - 'T1202': 'switch', - 'T1203': 'switch', - 'T1211': 'switch' + "T1011": "light", + "T1012": "light", + "T1013": "light", + "T1201": "switch", + "T1202": "switch", + "T1203": "switch", + "T1211": "switch", } @@ -45,25 +59,24 @@ def setup(hass, config): import lakeside if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]: - data = lakeside.get_devices(config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD]) + data = lakeside.get_devices( + config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD] + ) for device in data: - kind = device['type'] + kind = device["type"] if kind not in EUFY_DISPATCH: continue - discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, - config) + discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, config) for device_info in config[DOMAIN][CONF_DEVICES]: - kind = device_info['type'] + kind = device_info["type"] if kind not in EUFY_DISPATCH: continue device = {} - device['address'] = device_info['address'] - device['code'] = device_info['access_token'] - device['type'] = device_info['type'] - device['name'] = device_info['name'] - discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, - config) + device["address"] = device_info["address"] + device["code"] = device_info["access_token"] + device["type"] = device_info["type"] + device["name"] = device_info["name"] + discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, config) return True diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index 1d08e42fff7..f5359e6f2f6 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -2,14 +2,21 @@ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, + SUPPORT_COLOR, + Light, +) import homeassistant.util.color as color_util from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, - color_temperature_kelvin_to_mired as kelvin_to_mired) + color_temperature_kelvin_to_mired as kelvin_to_mired, +) _LOGGER = logging.getLogger(__name__) @@ -35,10 +42,10 @@ class EufyLight(Light): self._brightness = None self._hs = None self._state = None - self._name = device['name'] - self._address = device['address'] - self._code = device['code'] - self._type = device['type'] + self._name = device["name"] + self._address = device["address"] + self._code = device["code"] + self._type = device["type"] self._bulb = lakeside.bulb(self._address, self._code, self._type) self._colormode = False if self._type == "T1011": @@ -46,8 +53,7 @@ class EufyLight(Light): elif self._type == "T1012": self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP elif self._type == "T1013": - self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | \ - SUPPORT_COLOR + self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR self._bulb.connect() def update(self): @@ -96,9 +102,9 @@ class EufyLight(Light): @property def color_temp(self): """Return the color temperature of this light.""" - temp_in_k = int(EUFY_MIN_KELVIN + (self._temp * - (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN) - / 100)) + temp_in_k = int( + EUFY_MIN_KELVIN + (self._temp * (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN) / 100) + ) return kelvin_to_mired(temp_in_k) @property @@ -131,28 +137,29 @@ class EufyLight(Light): self._colormode = False temp_in_k = mired_to_kelvin(colortemp) relative_temp = temp_in_k - EUFY_MIN_KELVIN - temp = int(relative_temp * 100 / - (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN)) + temp = int(relative_temp * 100 / (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN)) else: temp = None if hs is not None: - rgb = color_util.color_hsv_to_RGB( - hs[0], hs[1], brightness / 255 * 100) + rgb = color_util.color_hsv_to_RGB(hs[0], hs[1], brightness / 255 * 100) self._colormode = True elif self._colormode: rgb = color_util.color_hsv_to_RGB( - self._hs[0], self._hs[1], brightness / 255 * 100) + self._hs[0], self._hs[1], brightness / 255 * 100 + ) else: rgb = None try: - self._bulb.set_state(power=True, brightness=brightness, - temperature=temp, colors=rgb) + self._bulb.set_state( + power=True, brightness=brightness, temperature=temp, colors=rgb + ) except BrokenPipeError: self._bulb.connect() - self._bulb.set_state(power=True, brightness=brightness, - temperature=temp, colors=rgb) + self._bulb.set_state( + power=True, brightness=brightness, temperature=temp, colors=rgb + ) def turn_off(self, **kwargs): """Turn the specified light off.""" diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index 3216bfed69e..3d05ef5d351 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -21,10 +21,10 @@ class EufySwitch(SwitchDevice): import lakeside self._state = None - self._name = device['name'] - self._address = device['address'] - self._code = device['code'] - self._type = device['type'] + self._name = device["name"] + self._address = device["address"] + self._code = device["code"] + self._type = device["type"] self._switch = lakeside.switch(self._address, self._code, self._type) self._switch.connect() diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index c5fb025370d..21629360ac7 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -7,9 +7,15 @@ import voluptuous as vol from homeassistant.const import CONF_HOSTS from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, - SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_COLOR, - Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_EFFECT, + SUPPORT_BRIGHTNESS, + SUPPORT_EFFECT, + SUPPORT_COLOR, + Light, + PLATFORM_SCHEMA, +) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -17,36 +23,35 @@ from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) -SUPPORT_EVERLIGHTS = (SUPPORT_EFFECT | SUPPORT_BRIGHTNESS | SUPPORT_COLOR) +SUPPORT_EVERLIGHTS = SUPPORT_EFFECT | SUPPORT_BRIGHTNESS | SUPPORT_COLOR SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string])} +) NAME_FORMAT = "EverLights {} Zone {}" def color_rgb_to_int(red: int, green: int, blue: int) -> int: """Return a RGB color as an integer.""" - return red*256*256+green*256+blue + return red * 256 * 256 + green * 256 + blue def color_int_to_rgb(value: int) -> Tuple[int, int, int]: """Return an RGB tuple from an integer.""" - return (value >> 16, (value >> 8) & 0xff, value & 0xff) + return (value >> 16, (value >> 8) & 0xFF, value & 0xFF) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the EverLights lights from configuration.yaml.""" import pyeverlights + lights = [] for ipaddr in config[CONF_HOSTS]: - api = pyeverlights.EverLights(ipaddr, - async_get_clientsession(hass)) + api = pyeverlights.EverLights(ipaddr, async_get_clientsession(hass)) try: status = await api.get_status() @@ -57,10 +62,8 @@ async def async_setup_platform(hass, config, async_add_entities, raise PlatformNotReady else: - lights.append(EverLightsLight(api, pyeverlights.ZONE_1, - status, effects)) - lights.append(EverLightsLight(api, pyeverlights.ZONE_2, - status, effects)) + lights.append(EverLightsLight(api, pyeverlights.ZONE_1, status, effects)) + lights.append(EverLightsLight(api, pyeverlights.ZONE_2, status, effects)) async_add_entities(lights) @@ -74,7 +77,7 @@ class EverLightsLight(Light): self._channel = channel self._status = status self._effects = effects - self._mac = status['mac'] + self._mac = status["mac"] self._error_reported = False self._hs_color = [255, 255] self._brightness = 255 @@ -84,7 +87,7 @@ class EverLightsLight(Light): @property def unique_id(self) -> str: """Return a unique ID.""" - return '{}-{}'.format(self._mac, self._channel) + return "{}-{}".format(self._mac, self._channel) @property def available(self) -> bool: @@ -99,7 +102,7 @@ class EverLightsLight(Light): @property def is_on(self): """Return true if device is on.""" - return self._status['ch{}Active'.format(self._channel)] == 1 + return self._status["ch{}Active".format(self._channel)] == 1 @property def brightness(self): @@ -141,7 +144,7 @@ class EverLightsLight(Light): brightness = hsv[2] / 100 * 255 else: - rgb = color_util.color_hsv_to_RGB(*hs_color, brightness/255*100) + rgb = color_util.color_hsv_to_RGB(*hs_color, brightness / 255 * 100) colors = [color_rgb_to_int(*rgb)] await self._api.set_pattern(self._channel, colors) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 49ddbdde156..f0e7a26e1f5 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -12,38 +12,55 @@ import voluptuous as vol import evohomeclient2 from homeassistant.const import ( - CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, - HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS, TEMP_CELSIUS) + CONF_ACCESS_TOKEN, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, + HTTP_SERVICE_UNAVAILABLE, + HTTP_TOO_MANY_REQUESTS, + TEMP_CELSIUS, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, async_dispatcher_send) + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( - async_track_point_in_utc_time, track_time_interval) + async_track_point_in_utc_time, + track_time_interval, +) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime, utcnow from .const import DOMAIN, STORAGE_VERSION, STORAGE_KEY, GWS, TCS _LOGGER = logging.getLogger(__name__) -CONF_ACCESS_TOKEN_EXPIRES = 'access_token_expires' -CONF_REFRESH_TOKEN = 'refresh_token' +CONF_ACCESS_TOKEN_EXPIRES = "access_token_expires" +CONF_REFRESH_TOKEN = "refresh_token" -CONF_LOCATION_IDX = 'location_idx' +CONF_LOCATION_IDX = "location_idx" SCAN_INTERVAL_DEFAULT = timedelta(seconds=300) SCAN_INTERVAL_MINIMUM = timedelta(seconds=60) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT): - vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int, + vol.Optional( + CONF_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT + ): vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def _local_dt_to_utc(dt_naive: datetime) -> datetime: @@ -69,7 +86,7 @@ def _handle_exception(err) -> bool: "Failed to (re)authenticate with the vendor's server. " "Check that your username and password are correct. " "Message is: %s", - err + err, ) return False @@ -79,7 +96,7 @@ def _handle_exception(err) -> bool: "Unable to connect with the vendor's server. " "Check your network and the vendor's status page." "Message is: %s", - err + err, ) return False @@ -94,26 +111,25 @@ def _handle_exception(err) -> bool: if err.response.status_code == HTTP_TOO_MANY_REQUESTS: _LOGGER.warning( "The vendor's API rate limit has been exceeded. " - "Consider increasing the %s.", CONF_SCAN_INTERVAL + "Consider increasing the %s.", + CONF_SCAN_INTERVAL, ) return False raise # we don't expect/handle any other HTTPErrors -def setup(hass, hass_config) -> bool: +def setup(hass: HomeAssistantType, hass_config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell evohome system.""" broker = EvoBroker(hass, hass_config[DOMAIN]) if not broker.init_client(): return False - load_platform(hass, 'climate', DOMAIN, {}, hass_config) + load_platform(hass, "climate", DOMAIN, {}, hass_config) if broker.tcs.hotwater: - load_platform(hass, 'water_heater', DOMAIN, {}, hass_config) + load_platform(hass, "water_heater", DOMAIN, {}, hass_config) - track_time_interval( - hass, broker.update, hass_config[DOMAIN][CONF_SCAN_INTERVAL] - ) + track_time_interval(hass, broker.update, hass_config[DOMAIN][CONF_SCAN_INTERVAL]) return True @@ -132,16 +148,16 @@ class EvoBroker: self._app_storage = {} hass.data[DOMAIN] = {} - hass.data[DOMAIN]['broker'] = self + hass.data[DOMAIN]["broker"] = self def init_client(self) -> bool: """Initialse the evohome data broker. Return True if this is successful, otherwise return False. """ - refresh_token, access_token, access_token_expires = \ - asyncio.run_coroutine_threadsafe( - self._load_auth_tokens(), self.hass.loop).result() + refresh_token, access_token, access_token_expires = asyncio.run_coroutine_threadsafe( + self._load_auth_tokens(), self.hass.loop + ).result() # evohomeclient2 uses naive/local datetimes if access_token_expires is not None: @@ -153,16 +169,18 @@ class EvoBroker: self.params[CONF_PASSWORD], refresh_token=refresh_token, access_token=access_token, - access_token_expires=access_token_expires + access_token_expires=access_token_expires, ) - except (requests.exceptions.RequestException, - evohomeclient2.AuthenticationError) as err: + except ( + requests.exceptions.RequestException, + evohomeclient2.AuthenticationError, + ) as err: if not _handle_exception(err): return False finally: - self.params[CONF_PASSWORD] = 'REDACTED' + self.params[CONF_PASSWORD] = "REDACTED" self.hass.add_job(self._save_auth_tokens()) @@ -175,23 +193,30 @@ class EvoBroker: "Config error: '%s' = %s, but its valid range is 0-%s. " "Unable to continue. " "Fix any configuration errors and restart HA.", - CONF_LOCATION_IDX, loc_idx, len(client.installation_info) - 1 + CONF_LOCATION_IDX, + loc_idx, + len(client.installation_info) - 1, ) return False - self.tcs = client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa: E501; pylint: disable=protected-access + self.tcs = ( + client.locations[loc_idx] # noqa: E501; pylint: disable=protected-access + ._gateways[0] + ._control_systems[0] + ) _LOGGER.debug("Config = %s", self.config) if _LOGGER.isEnabledFor(logging.DEBUG): # don't do an I/O unless required _LOGGER.debug( - "Status = %s", - client.locations[loc_idx].status()[GWS][0][TCS][0]) + "Status = %s", client.locations[loc_idx].status()[GWS][0][TCS][0] + ) return True - async def _load_auth_tokens(self) -> Tuple[ - Optional[str], Optional[str], Optional[datetime]]: + async def _load_auth_tokens( + self + ) -> Tuple[Optional[str], Optional[str], Optional[datetime]]: store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) app_storage = self._app_storage = await store.async_load() @@ -213,14 +238,12 @@ class EvoBroker: async def _save_auth_tokens(self, *args) -> None: # evohomeclient2 uses naive/local datetimes - access_token_expires = _local_dt_to_utc( - self.client.access_token_expires) + access_token_expires = _local_dt_to_utc(self.client.access_token_expires) self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME] self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token self._app_storage[CONF_ACCESS_TOKEN] = self.client.access_token - self._app_storage[CONF_ACCESS_TOKEN_EXPIRES] = \ - access_token_expires.isoformat() + self._app_storage[CONF_ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat() store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) await store.async_save(self._app_storage) @@ -228,7 +251,7 @@ class EvoBroker: async_track_point_in_utc_time( self.hass, self._save_auth_tokens, - access_token_expires + self.params[CONF_SCAN_INTERVAL] + access_token_expires + self.params[CONF_SCAN_INTERVAL], ) def update(self, *args, **kwargs) -> None: @@ -242,16 +265,18 @@ class EvoBroker: try: status = self.client.locations[loc_idx].status()[GWS][0][TCS][0] - except (requests.exceptions.RequestException, - evohomeclient2.AuthenticationError) as err: + except ( + requests.exceptions.RequestException, + evohomeclient2.AuthenticationError, + ) as err: _handle_exception(err) else: - self.timers['statusUpdated'] = utcnow() + self.timers["statusUpdated"] = utcnow() _LOGGER.debug("Status = %s", status) # inform the evohome devices that state data has been updated - async_dispatcher_send(self.hass, DOMAIN, {'signal': 'refresh'}) + async_dispatcher_send(self.hass, DOMAIN, {"signal": "refresh"}) class EvoDevice(Entity): @@ -270,59 +295,60 @@ class EvoDevice(Entity): self._state_attributes = [] self._supported_features = None - self._setpoints = None + self._schedule = {} @callback def _refresh(self, packet): - if packet['signal'] == 'refresh': + if packet["signal"] == "refresh": self.async_schedule_update_ha_state(force_refresh=True) - def get_setpoints(self) -> Optional[Dict[str, Any]]: - """Return the current/next scheduled switchpoints. + @property + def setpoints(self) -> Dict[str, Any]: + """Return the current/next setpoints from the schedule. - Only Zones & DHW controllers (but not the TCS) have schedules. + Only Zones & DHW controllers (but not the TCS) can have schedules. """ - switchpoints = {} - schedule = self._evo_device.schedule() + if not self._schedule["DailySchedules"]: + return {} - if not schedule['DailySchedules']: - return None + switchpoints = {} day_time = datetime.now() - day_of_week = int(day_time.strftime('%w')) # 0 is Sunday + day_of_week = int(day_time.strftime("%w")) # 0 is Sunday # Iterate today's switchpoints until past the current time of day... - day = schedule['DailySchedules'][day_of_week] + day = self._schedule["DailySchedules"][day_of_week] sp_idx = -1 # last switchpoint of the day before - for i, tmp in enumerate(day['Switchpoints']): - if day_time.strftime('%H:%M:%S') > tmp['TimeOfDay']: + for i, tmp in enumerate(day["Switchpoints"]): + if day_time.strftime("%H:%M:%S") > tmp["TimeOfDay"]: sp_idx = i # current setpoint else: break # Did the current SP start yesterday? Does the next start SP tomorrow? current_sp_day = -1 if sp_idx == -1 else 0 - next_sp_day = 1 if sp_idx + 1 == len(day['Switchpoints']) else 0 + next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 for key, offset, idx in [ - ('current', current_sp_day, sp_idx), - ('next', next_sp_day, (sp_idx + 1) * (1 - next_sp_day))]: + ("current", current_sp_day, sp_idx), + ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), + ]: spt = switchpoints[key] = {} - sp_date = (day_time + timedelta(days=offset)).strftime('%Y-%m-%d') - day = schedule['DailySchedules'][(day_of_week + offset) % 7] - switchpoint = day['Switchpoints'][idx] + sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") + day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] + switchpoint = day["Switchpoints"][idx] dt_naive = datetime.strptime( - '{}T{}'.format(sp_date, switchpoint['TimeOfDay']), - '%Y-%m-%dT%H:%M:%S') + "{}T{}".format(sp_date, switchpoint["TimeOfDay"]), "%Y-%m-%dT%H:%M:%S" + ) - spt['from'] = _local_dt_to_utc(dt_naive).isoformat() + spt["from"] = _local_dt_to_utc(dt_naive).isoformat() try: - spt['temperature'] = switchpoint['heatSetpoint'] + spt["temperature"] = switchpoint["heatSetpoint"] except KeyError: - spt['state'] = switchpoint['DhwState'] + spt["state"] = switchpoint["DhwState"] return switchpoints @@ -341,13 +367,13 @@ class EvoDevice(Entity): """Return the Evohome-specific state attributes.""" status = {} for attr in self._state_attributes: - if attr != 'setpoints': + if attr != "setpoints": status[attr] = getattr(self._evo_device, attr) - if 'setpoints' in self._state_attributes: - status['setpoints'] = self._setpoints + if "setpoints" in self._state_attributes: + status["setpoints"] = self.setpoints - return {'status': status} + return {"status": status} @property def icon(self) -> str: @@ -373,6 +399,14 @@ class EvoDevice(Entity): """Return the temperature unit to use in the frontend UI.""" return TEMP_CELSIUS + def _update_schedule(self) -> None: + """Get the latest state data.""" + if ( + not self._schedule.get("DailySchedules") + or parse_datetime(self.setpoints["next"]["from"]) < utcnow() + ): + self._schedule = self._evo_device.schedule() + def update(self) -> None: """Get the latest state data.""" - self._setpoints = self.get_setpoints() + self._update_schedule() diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 540675d7ef4..d1b9d5f54c7 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -8,63 +8,88 @@ import evohomeclient2 from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF, - CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, - PRESET_AWAY, PRESET_ECO, PRESET_HOME, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE) + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_ECO, + PRESET_HOME, + PRESET_NONE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_PRESET_MODE, +) from homeassistant.const import PRECISION_TENTHS +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime from . import CONF_LOCATION_IDX, _handle_exception, EvoDevice from .const import ( - DOMAIN, EVO_RESET, EVO_AUTO, EVO_AUTOECO, EVO_AWAY, EVO_DAYOFF, EVO_CUSTOM, - EVO_HEATOFF, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER) + DOMAIN, + EVO_RESET, + EVO_AUTO, + EVO_AUTOECO, + EVO_AWAY, + EVO_CUSTOM, + EVO_DAYOFF, + EVO_HEATOFF, + EVO_FOLLOW, + EVO_TEMPOVER, + EVO_PERMOVER, +) _LOGGER = logging.getLogger(__name__) -PRESET_RESET = 'Reset' # reset all child zones to EVO_FOLLOW -PRESET_CUSTOM = 'Custom' +PRESET_RESET = "Reset" # reset all child zones to EVO_FOLLOW +PRESET_CUSTOM = "Custom" -HA_HVAC_TO_TCS = { - HVAC_MODE_OFF: EVO_HEATOFF, - HVAC_MODE_HEAT: EVO_AUTO, -} +HA_HVAC_TO_TCS = {HVAC_MODE_OFF: EVO_HEATOFF, HVAC_MODE_HEAT: EVO_AUTO} -HA_PRESET_TO_TCS = { - PRESET_AWAY: EVO_AWAY, - PRESET_CUSTOM: EVO_CUSTOM, - PRESET_ECO: EVO_AUTOECO, - PRESET_HOME: EVO_DAYOFF, - PRESET_RESET: EVO_RESET, -} -TCS_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_TCS.items()} +TCS_PRESET_TO_HA = { + EVO_AWAY: PRESET_AWAY, + EVO_CUSTOM: PRESET_CUSTOM, + EVO_AUTOECO: PRESET_ECO, + EVO_DAYOFF: PRESET_HOME, + EVO_RESET: PRESET_RESET, +} # EVO_AUTO: None, + +HA_PRESET_TO_TCS = {v: k for k, v in TCS_PRESET_TO_HA.items()} EVO_PRESET_TO_HA = { - EVO_FOLLOW: None, - EVO_TEMPOVER: 'temporary', - EVO_PERMOVER: 'permanent', + EVO_FOLLOW: PRESET_NONE, + EVO_TEMPOVER: "temporary", + EVO_PERMOVER: "permanent", } -HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items() - if v is not None} +HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items()} -def setup_platform(hass, hass_config, add_entities, - discovery_info=None) -> None: +def setup_platform( + hass: HomeAssistantType, hass_config: ConfigType, add_entities, discovery_info=None +) -> None: """Create the evohome Controller, and its Zones, if any.""" - broker = hass.data[DOMAIN]['broker'] + broker = hass.data[DOMAIN]["broker"] loc_idx = broker.params[CONF_LOCATION_IDX] _LOGGER.debug( "Found Location/Controller, id=%s [%s], name=%s (location_idx=%s)", - broker.tcs.systemId, broker.tcs.modelType, broker.tcs.location.name, - loc_idx) + broker.tcs.systemId, + broker.tcs.modelType, + broker.tcs.location.name, + loc_idx, + ) # special case of RoundThermostat (is single zone) - if broker.config['zones'][0]['modelType'] == 'RoundModulation': + if broker.config["zones"][0]["modelType"] == "RoundModulation": zone = list(broker.tcs.zones.values())[0] _LOGGER.debug( "Found %s, id=%s [%s], name=%s", - zone.zoneType, zone.zoneId, zone.modelType, zone.name) + zone.zoneType, + zone.zoneId, + zone.modelType, + zone.name, + ) add_entities([EvoThermostat(broker, zone)], update_before_add=True) return @@ -75,7 +100,11 @@ def setup_platform(hass, hass_config, add_entities, for zone in broker.tcs.zones.values(): _LOGGER.debug( "Found %s, id=%s [%s], name=%s", - zone.zoneType, zone.zoneId, zone.modelType, zone.name) + zone.zoneType, + zone.zoneId, + zone.modelType, + zone.name, + ) zones.append(EvoZone(broker, zone)) add_entities([controller] + zones, update_before_add=True) @@ -90,65 +119,74 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): self._preset_modes = None - def _set_temperature(self, temperature: float, - until: Optional[datetime] = None) -> None: + def _set_temperature( + self, temperature: float, until: Optional[datetime] = None + ) -> None: """Set a new target temperature for the Zone. until == None means indefinitely (i.e. PermanentOverride) """ try: self._evo_device.set_temperature(temperature, until) - except (requests.exceptions.RequestException, - evohomeclient2.AuthenticationError) as err: + except ( + requests.exceptions.RequestException, + evohomeclient2.AuthenticationError, + ) as err: _handle_exception(err) def _set_zone_mode(self, op_mode: str) -> None: - """Set the Zone to one of its native EVO_* operating modes. + """Set a Zone to one of its native EVO_* operating modes. - NB: evohome Zones 'inherit' their operating mode from the Controller. + Zones inherit their _effective_ operating mode from the Controller. Usually, Zones are in 'FollowSchedule' mode, where their setpoints are - a function of their schedule, and the Controller's operating_mode, e.g. - Economy mode is their scheduled setpoint less (usually) 3C. + a function of their own schedule and the Controller's operating mode, + e.g. 'AutoWithEco' mode means their setpoint is (by default) 3C less + than scheduled. - However, Zones can override these setpoints, either for a specified - period of time, 'TemporaryOverride', after which they will revert back - to 'FollowSchedule' mode, or indefinitely, 'PermanentOverride'. + However, Zones can _override_ these setpoints, either indefinitely, + 'PermanentOverride' mode, or for a period of time, 'TemporaryOverride', + after which they will revert back to 'FollowSchedule'. - Some of the Controller's operating_mode are 'forced' upon the Zone, - regardless of its override state, e.g. 'HeatingOff' (Zones to min_temp) - and 'Away' (Zones to 12C). + Finally, some of the Controller's operating modes are _forced_ upon the + Zones, regardless of any override mode, e.g. 'HeatingOff', Zones to + (by default) 5C, and 'Away', Zones to (by default) 12C. """ if op_mode == EVO_FOLLOW: try: self._evo_device.cancel_temp_override() - except (requests.exceptions.RequestException, - evohomeclient2.AuthenticationError) as err: + except ( + requests.exceptions.RequestException, + evohomeclient2.AuthenticationError, + ) as err: _handle_exception(err) return - temperature = self._evo_device.setpointStatus['targetHeatTemperature'] + temperature = self._evo_device.setpointStatus["targetHeatTemperature"] until = None # EVO_PERMOVER - if op_mode == EVO_TEMPOVER: - self._setpoints = self.get_setpoints() - if self._setpoints: - until = parse_datetime(self._setpoints['next']['from']) + if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: + self._update_schedule() + if self._schedule["DailySchedules"]: + until = parse_datetime(self.setpoints["next"]["from"]) self._set_temperature(temperature, until=until) def _set_tcs_mode(self, op_mode: str) -> None: """Set the Controller to any of its native EVO_* operating modes.""" try: - self._evo_tcs._set_status(op_mode) # noqa: E501; pylint: disable=protected-access - except (requests.exceptions.RequestException, - evohomeclient2.AuthenticationError) as err: + # noqa: E501; pylint: disable=protected-access + self._evo_tcs._set_status(op_mode) + except ( + requests.exceptions.RequestException, + evohomeclient2.AuthenticationError, + ) as err: _handle_exception(err) @property def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes.""" - return [HVAC_MODE_OFF, HVAC_MODE_HEAT] + return list(HA_HVAC_TO_TCS) @property def preset_modes(self) -> Optional[List[str]]: @@ -164,22 +202,24 @@ class EvoZone(EvoClimateDevice): super().__init__(evo_broker, evo_device) self._name = evo_device.name - self._icon = 'mdi:radiator' + self._icon = "mdi:radiator" - self._precision = \ - self._evo_device.setpointCapabilities['valueResolution'] + self._precision = self._evo_device.setpointCapabilities["valueResolution"] self._state_attributes = [ - 'zoneId', 'activeFaults', 'setpointStatus', 'temperatureStatus', - 'setpoints'] + "zoneId", + "activeFaults", + "setpointStatus", + "temperatureStatus", + "setpoints", + ] - self._supported_features = SUPPORT_PRESET_MODE | \ - SUPPORT_TARGET_TEMPERATURE + self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE self._preset_modes = list(HA_PRESET_TO_EVO) @property def hvac_mode(self) -> str: """Return the current operating mode of the evohome Zone.""" - if self._evo_tcs.systemModeStatus['mode'] in [EVO_AWAY, EVO_HEATOFF]: + if self._evo_tcs.systemModeStatus["mode"] in [EVO_AWAY, EVO_HEATOFF]: return HVAC_MODE_AUTO is_off = self.target_temperature <= self.min_temp return HVAC_MODE_OFF if is_off else HVAC_MODE_HEAT @@ -187,34 +227,38 @@ class EvoZone(EvoClimateDevice): @property def hvac_action(self) -> Optional[str]: """Return the current running hvac operation if supported.""" - if self._evo_tcs.systemModeStatus['mode'] == EVO_HEATOFF: + if self._evo_tcs.systemModeStatus["mode"] == EVO_HEATOFF: return CURRENT_HVAC_OFF if self.target_temperature <= self.min_temp: return CURRENT_HVAC_OFF - if self.target_temperature <= self.current_temperature: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE + if self.target_temperature < self.current_temperature: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_HEAT @property def current_temperature(self) -> Optional[float]: """Return the current temperature of the evohome Zone.""" - return (self._evo_device.temperatureStatus['temperature'] - if self._evo_device.temperatureStatus['isAvailable'] else None) + return ( + self._evo_device.temperatureStatus["temperature"] + if self._evo_device.temperatureStatus["isAvailable"] + else None + ) @property def target_temperature(self) -> float: """Return the target temperature of the evohome Zone.""" - if self._evo_tcs.systemModeStatus['mode'] == EVO_HEATOFF: - return self._evo_device.setpointCapabilities['minHeatSetpoint'] - return self._evo_device.setpointStatus['targetHeatTemperature'] + if self._evo_tcs.systemModeStatus["mode"] == EVO_HEATOFF: + return self._evo_device.setpointCapabilities["minHeatSetpoint"] + return self._evo_device.setpointStatus["targetHeatTemperature"] @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - if self._evo_tcs.systemModeStatus['mode'] in [EVO_AWAY, EVO_HEATOFF]: - return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus['mode']) + if self._evo_tcs.systemModeStatus["mode"] in [EVO_AWAY, EVO_HEATOFF]: + return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) return EVO_PRESET_TO_HA.get( - self._evo_device.setpointStatus['setpointMode'], 'follow') + self._evo_device.setpointStatus["setpointMode"], "follow" + ) @property def min_temp(self) -> float: @@ -222,7 +266,7 @@ class EvoZone(EvoClimateDevice): The default is 5, but is user-configurable within 5-35 (in Celsius). """ - return self._evo_device.setpointCapabilities['minHeatSetpoint'] + return self._evo_device.setpointCapabilities["minHeatSetpoint"] @property def max_temp(self) -> float: @@ -230,15 +274,15 @@ class EvoZone(EvoClimateDevice): The default is 35, but is user-configurable within 5-35 (in Celsius). """ - return self._evo_device.setpointCapabilities['maxHeatSetpoint'] + return self._evo_device.setpointCapabilities["maxHeatSetpoint"] def set_temperature(self, **kwargs) -> None: """Set a new target temperature.""" - until = kwargs.get('until') + until = kwargs.get("until") if until: until = parse_datetime(until) - self._set_temperature(kwargs['temperature'], until) + self._set_temperature(kwargs["temperature"], until) def set_hvac_mode(self, hvac_mode: str) -> None: """Set an operating mode for the Zone.""" @@ -268,11 +312,10 @@ class EvoController(EvoClimateDevice): super().__init__(evo_broker, evo_device) self._name = evo_device.location.name - self._icon = 'mdi:thermostat' + self._icon = "mdi:thermostat" self._precision = PRECISION_TENTHS - self._state_attributes = [ - 'systemId', 'activeFaults', 'systemModeStatus'] + self._state_attributes = ["systemId", "activeFaults", "systemModeStatus"] self._supported_features = SUPPORT_PRESET_MODE self._preset_modes = list(HA_PRESET_TO_TCS) @@ -280,7 +323,7 @@ class EvoController(EvoClimateDevice): @property def hvac_mode(self) -> str: """Return the current operating mode of the evohome Controller.""" - tcs_mode = self._evo_tcs.systemModeStatus['mode'] + tcs_mode = self._evo_tcs.systemModeStatus["mode"] return HVAC_MODE_OFF if tcs_mode == EVO_HEATOFF else HVAC_MODE_HEAT @property @@ -289,18 +332,23 @@ class EvoController(EvoClimateDevice): Controllers do not have a current temp, but one is expected by HA. """ - temps = [z.temperatureStatus['temperature'] - for z in self._evo_tcs.zones.values() - if z.temperatureStatus['isAvailable']] + temps = [ + z.temperatureStatus["temperature"] + for z in self._evo_tcs.zones.values() + if z.temperatureStatus["isAvailable"] + ] return round(sum(temps) / len(temps), 1) if temps else None @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus['mode']) + return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) def set_temperature(self, **kwargs) -> None: - """The evohome Controller doesn't have a targert temperature.""" + """Do nothing. + + The evohome Controller doesn't have a target temperature. + """ return def set_hvac_mode(self, hvac_mode: str) -> None: @@ -335,17 +383,17 @@ class EvoThermostat(EvoZone): @property def device_state_attributes(self) -> Dict[str, Any]: """Return the device-specific state attributes.""" - status = super().device_state_attributes['status'] + status = super().device_state_attributes["status"] - status['systemModeStatus'] = self._evo_tcs.systemModeStatus - status['activeFaults'] += self._evo_tcs.activeFaults + status["systemModeStatus"] = self._evo_tcs.systemModeStatus + status["activeFaults"] += self._evo_tcs.activeFaults - return {'status': status} + return {"status": status} @property def hvac_mode(self) -> str: """Return the current operating mode.""" - if self._evo_tcs.systemModeStatus['mode'] == EVO_HEATOFF: + if self._evo_tcs.systemModeStatus["mode"] == EVO_HEATOFF: return HVAC_MODE_OFF return super().hvac_mode @@ -353,8 +401,10 @@ class EvoThermostat(EvoZone): @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - if self._evo_tcs.systemModeStatus['mode'] == EVO_AUTOECO and \ - self._evo_device.setpointStatus['setpointMode'] == EVO_FOLLOW: + if ( + self._evo_tcs.systemModeStatus["mode"] == EVO_AUTOECO + and self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW + ): return PRESET_ECO return super().preset_mode diff --git a/homeassistant/components/evohome/const.py b/homeassistant/components/evohome/const.py index d1a22a844f6..a0480d62a10 100644 --- a/homeassistant/components/evohome/const.py +++ b/homeassistant/components/evohome/const.py @@ -1,25 +1,25 @@ """Support for (EMEA/EU-based) Honeywell TCC climate systems.""" -DOMAIN = 'evohome' +DOMAIN = "evohome" STORAGE_VERSION = 1 STORAGE_KEY = DOMAIN # The Parent's (i.e. TCS, Controller's) operating mode is one of: -EVO_RESET = 'AutoWithReset' -EVO_AUTO = 'Auto' -EVO_AUTOECO = 'AutoWithEco' -EVO_AWAY = 'Away' -EVO_DAYOFF = 'DayOff' -EVO_CUSTOM = 'Custom' -EVO_HEATOFF = 'HeatingOff' +EVO_RESET = "AutoWithReset" +EVO_AUTO = "Auto" +EVO_AUTOECO = "AutoWithEco" +EVO_AWAY = "Away" +EVO_DAYOFF = "DayOff" +EVO_CUSTOM = "Custom" +EVO_HEATOFF = "HeatingOff" # The Childs' operating mode is one of: -EVO_FOLLOW = 'FollowSchedule' # the operating mode is 'inherited' from the TCS -EVO_TEMPOVER = 'TemporaryOverride' -EVO_PERMOVER = 'PermanentOverride' +EVO_FOLLOW = "FollowSchedule" # the operating mode is 'inherited' from the TCS +EVO_TEMPOVER = "TemporaryOverride" +EVO_PERMOVER = "PermanentOverride" # These are used only to help prevent E501 (line too long) violations -GWS = 'gateways' -TCS = 'temperatureControlSystems' +GWS = "gateways" +TCS = "temperatureControlSystems" -EVO_STRFTIME = '%Y-%m-%dT%H:%M:%SZ' +EVO_STRFTIME = "%Y-%m-%dT%H:%M:%SZ" diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 4706269e1cf..6309f07a000 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -6,7 +6,9 @@ import requests.exceptions import evohomeclient2 from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, WaterHeaterDevice) + SUPPORT_OPERATION_MODE, + WaterHeaterDevice, +) from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON from homeassistant.util.dt import parse_datetime @@ -15,20 +17,19 @@ from .const import DOMAIN, EVO_STRFTIME, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER _LOGGER = logging.getLogger(__name__) -HA_STATE_TO_EVO = {STATE_ON: 'On', STATE_OFF: 'Off'} +HA_STATE_TO_EVO = {STATE_ON: "On", STATE_OFF: "Off"} EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items()} HA_OPMODE_TO_DHW = {STATE_ON: EVO_FOLLOW, STATE_OFF: EVO_PERMOVER} -def setup_platform(hass, hass_config, add_entities, - discovery_info=None) -> None: +def setup_platform(hass, hass_config, add_entities, discovery_info=None) -> None: """Create the DHW controller.""" - broker = hass.data[DOMAIN]['broker'] + broker = hass.data[DOMAIN]["broker"] _LOGGER.debug( - "Found %s, id: %s", - broker.tcs.hotwater.zone_type, broker.tcs.hotwater.zoneId) + "Found %s, id: %s", broker.tcs.hotwater.zone_type, broker.tcs.hotwater.zoneId + ) evo_dhw = EvoDHW(broker, broker.tcs.hotwater) @@ -42,13 +43,17 @@ class EvoDHW(EvoDevice, WaterHeaterDevice): """Initialize the evohome DHW controller.""" super().__init__(evo_broker, evo_device) - self._name = 'DHW controller' - self._icon = 'mdi:thermometer-lines' + self._name = "DHW controller" + self._icon = "mdi:thermometer-lines" self._precision = PRECISION_WHOLE self._state_attributes = [ - 'dhwId', 'activeFaults', 'stateStatus', 'temperatureStatus', - 'setpoints'] + "dhwId", + "activeFaults", + "stateStatus", + "temperatureStatus", + "setpoints", + ] self._supported_features = SUPPORT_OPERATION_MODE self._operation_list = list(HA_OPMODE_TO_DHW) @@ -56,7 +61,7 @@ class EvoDHW(EvoDevice, WaterHeaterDevice): @property def current_operation(self) -> str: """Return the current operating mode (On, or Off).""" - return EVO_STATE_TO_HA[self._evo_device.stateStatus['state']] + return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] @property def operation_list(self) -> List[str]: @@ -66,25 +71,27 @@ class EvoDHW(EvoDevice, WaterHeaterDevice): @property def current_temperature(self) -> float: """Return the current temperature.""" - return self._evo_device.temperatureStatus['temperature'] + return self._evo_device.temperatureStatus["temperature"] def set_operation_mode(self, operation_mode: str) -> None: """Set new operation mode for a DHW controller.""" op_mode = HA_OPMODE_TO_DHW[operation_mode] - state = '' if op_mode == EVO_FOLLOW else HA_STATE_TO_EVO[STATE_OFF] + state = "" if op_mode == EVO_FOLLOW else HA_STATE_TO_EVO[STATE_OFF] until = None # EVO_FOLLOW, EVO_PERMOVER - if op_mode == EVO_TEMPOVER: - self._setpoints = self.get_setpoints() - if self._setpoints: - until = parse_datetime(self._setpoints['next']['from']) + if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: + self._update_schedule() + if self._schedule["DailySchedules"]: + until = parse_datetime(self.setpoints["next"]["from"]) until = until.strftime(EVO_STRFTIME) - data = {'Mode': op_mode, 'State': state, 'UntilTime': until} + data = {"Mode": op_mode, "State": state, "UntilTime": until} try: self._evo_device._set_dhw(data) # pylint: disable=protected-access - except (requests.exceptions.RequestException, - evohomeclient2.AuthenticationError) as err: + except ( + requests.exceptions.RequestException, + evohomeclient2.AuthenticationError, + ) as err: _handle_exception(err) diff --git a/homeassistant/components/facebook/notify.py b/homeassistant/components/facebook/notify.py index 625b922927c..452b81c0f16 100644 --- a/homeassistant/components/facebook/notify.py +++ b/homeassistant/components/facebook/notify.py @@ -9,20 +9,23 @@ import voluptuous as vol from homeassistant.const import CONTENT_TYPE_JSON import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (ATTR_DATA, ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + PLATFORM_SCHEMA, + BaseNotificationService, +) _LOGGER = logging.getLogger(__name__) -CONF_PAGE_ACCESS_TOKEN = 'page_access_token' -BASE_URL = 'https://graph.facebook.com/v2.6/me/messages' -CREATE_BROADCAST_URL = 'https://graph.facebook.com/v2.11/me/message_creatives' -SEND_BROADCAST_URL = 'https://graph.facebook.com/v2.11/me/broadcast_messages' +CONF_PAGE_ACCESS_TOKEN = "page_access_token" +BASE_URL = "https://graph.facebook.com/v2.6/me/messages" +CREATE_BROADCAST_URL = "https://graph.facebook.com/v2.11/me/message_creatives" +SEND_BROADCAST_URL = "https://graph.facebook.com/v2.11/me/broadcast_messages" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PAGE_ACCESS_TOKEN): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_PAGE_ACCESS_TOKEN): cv.string} +) def get_service(hass, config, discovery_info=None): @@ -39,7 +42,7 @@ class FacebookNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send some message.""" - payload = {'access_token': self.page_access_token} + payload = {"access_token": self.page_access_token} targets = kwargs.get(ATTR_TARGET) data = kwargs.get(ATTR_DATA) @@ -48,36 +51,40 @@ class FacebookNotificationService(BaseNotificationService): if data is not None: body_message.update(data) # Only one of text or attachment can be specified - if 'attachment' in body_message: - body_message.pop('text') + if "attachment" in body_message: + body_message.pop("text") if not targets: _LOGGER.error("At least 1 target is required") return # broadcast message - if targets[0].lower() == 'broadcast': + if targets[0].lower() == "broadcast": broadcast_create_body = {"messages": [body_message]} _LOGGER.debug("Broadcast body %s : ", broadcast_create_body) - resp = requests.post(CREATE_BROADCAST_URL, - data=json.dumps(broadcast_create_body), - params=payload, - headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, - timeout=10) + resp = requests.post( + CREATE_BROADCAST_URL, + data=json.dumps(broadcast_create_body), + params=payload, + headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, + timeout=10, + ) _LOGGER.debug("FB Messager broadcast id %s : ", resp.json()) # at this point we get broadcast id broadcast_body = { - "message_creative_id": resp.json().get('message_creative_id'), + "message_creative_id": resp.json().get("message_creative_id"), "notification_type": "REGULAR", } - resp = requests.post(SEND_BROADCAST_URL, - data=json.dumps(broadcast_body), - params=payload, - headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, - timeout=10) + resp = requests.post( + SEND_BROADCAST_URL, + data=json.dumps(broadcast_body), + params=payload, + headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, + timeout=10, + ) if resp.status_code != 200: log_error(resp) @@ -86,19 +93,19 @@ class FacebookNotificationService(BaseNotificationService): for target in targets: # If the target starts with a "+", it's a phone number, # otherwise it's a user id. - if target.startswith('+'): + if target.startswith("+"): recipient = {"phone_number": target} else: recipient = {"id": target} - body = { - "recipient": recipient, - "message": body_message - } - resp = requests.post(BASE_URL, data=json.dumps(body), - params=payload, - headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, - timeout=10) + body = {"recipient": recipient, "message": body_message} + resp = requests.post( + BASE_URL, + data=json.dumps(body), + params=payload, + headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, + timeout=10, + ) if resp.status_code != 200: log_error(resp) @@ -106,9 +113,9 @@ class FacebookNotificationService(BaseNotificationService): def log_error(response): """Log error message.""" obj = response.json() - error_message = obj['error']['message'] - error_code = obj['error']['code'] + error_message = obj["error"]["message"] + error_code = obj["error"]["code"] _LOGGER.error( - "Error %s : %s (Code %s)", response.status_code, error_message, - error_code) + "Error %s : %s (Code %s)", response.status_code, error_message, error_code + ) diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index 2b4f184c3fd..a1e686bcbd0 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -5,60 +5,72 @@ import logging import requests import voluptuous as vol -from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_NAME) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_CONFIDENCE, CONF_SOURCE, - CONF_ENTITY_ID, CONF_NAME, DOMAIN) + PLATFORM_SCHEMA, + ImageProcessingFaceEntity, + ATTR_CONFIDENCE, + CONF_SOURCE, + CONF_ENTITY_ID, + CONF_NAME, + DOMAIN, +) from homeassistant.const import ( - CONF_IP_ADDRESS, CONF_PORT, CONF_PASSWORD, CONF_USERNAME, - HTTP_BAD_REQUEST, HTTP_OK, HTTP_UNAUTHORIZED) + CONF_IP_ADDRESS, + CONF_PORT, + CONF_PASSWORD, + CONF_USERNAME, + HTTP_BAD_REQUEST, + HTTP_OK, + HTTP_UNAUTHORIZED, +) _LOGGER = logging.getLogger(__name__) -ATTR_BOUNDING_BOX = 'bounding_box' -ATTR_CLASSIFIER = 'classifier' -ATTR_IMAGE_ID = 'image_id' -ATTR_ID = 'id' -ATTR_MATCHED = 'matched' -FACEBOX_NAME = 'name' -CLASSIFIER = 'facebox' -DATA_FACEBOX = 'facebox_classifiers' -FILE_PATH = 'file_path' -SERVICE_TEACH_FACE = 'facebox_teach_face' +ATTR_BOUNDING_BOX = "bounding_box" +ATTR_CLASSIFIER = "classifier" +ATTR_IMAGE_ID = "image_id" +ATTR_ID = "id" +ATTR_MATCHED = "matched" +FACEBOX_NAME = "name" +CLASSIFIER = "facebox" +DATA_FACEBOX = "facebox_classifiers" +FILE_PATH = "file_path" +SERVICE_TEACH_FACE = "facebox_teach_face" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + } +) -SERVICE_TEACH_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_NAME): cv.string, - vol.Required(FILE_PATH): cv.string, -}) +SERVICE_TEACH_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_NAME): cv.string, + vol.Required(FILE_PATH): cv.string, + } +) def check_box_health(url, username, password): """Check the health of the classifier and return its id if healthy.""" kwargs = {} if username: - kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password) + kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password) try: - response = requests.get( - url, - **kwargs - ) + response = requests.get(url, **kwargs) if response.status_code == HTTP_UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) return None if response.status_code == HTTP_OK: - return response.json()['hostname'] + return response.json()["hostname"] except requests.exceptions.ConnectionError: _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) return None @@ -66,14 +78,15 @@ def check_box_health(url, username, password): def encode_image(image): """base64 encode an image stream.""" - base64_img = base64.b64encode(image).decode('ascii') + base64_img = base64.b64encode(image).decode("ascii") return base64_img def get_matched_faces(faces): """Return the name and rounded confidence of matched faces.""" - return {face['name']: round(face['confidence'], 2) - for face in faces if face['matched']} + return { + face["name"]: round(face["confidence"], 2) for face in faces if face["matched"] + } def parse_faces(api_faces): @@ -81,15 +94,15 @@ def parse_faces(api_faces): known_faces = [] for entry in api_faces: face = {} - if entry['matched']: # This data is only in matched faces. - face[FACEBOX_NAME] = entry['name'] - face[ATTR_IMAGE_ID] = entry['id'] + if entry["matched"]: # This data is only in matched faces. + face[FACEBOX_NAME] = entry["name"] + face[ATTR_IMAGE_ID] = entry["id"] else: # Lets be explicit. face[FACEBOX_NAME] = None face[ATTR_IMAGE_ID] = None - face[ATTR_CONFIDENCE] = round(100.0*entry['confidence'], 2) - face[ATTR_MATCHED] = entry['matched'] - face[ATTR_BOUNDING_BOX] = entry['rect'] + face[ATTR_CONFIDENCE] = round(100.0 * entry["confidence"], 2) + face[ATTR_MATCHED] = entry["matched"] + face[ATTR_BOUNDING_BOX] = entry["rect"] known_faces.append(face) return known_faces @@ -98,13 +111,9 @@ def post_image(url, image, username, password): """Post an image to the classifier.""" kwargs = {} if username: - kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password) + kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password) try: - response = requests.post( - url, - json={"base64": encode_image(image)}, - **kwargs - ) + response = requests.post(url, json={"base64": encode_image(image)}, **kwargs) if response.status_code == HTTP_UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) return None @@ -118,20 +127,24 @@ def teach_file(url, name, file_path, username, password): """Teach the classifier a name associated with a file.""" kwargs = {} if username: - kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password) + kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password) try: - with open(file_path, 'rb') as open_file: + with open(file_path, "rb") as open_file: response = requests.post( url, data={FACEBOX_NAME: name, ATTR_ID: file_path}, - files={'file': open_file}, - **kwargs - ) + files={"file": open_file}, + **kwargs, + ) if response.status_code == HTTP_UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) elif response.status_code == HTTP_BAD_REQUEST: - _LOGGER.error("%s teaching of file %s failed with message:%s", - CLASSIFIER, file_path, response.text) + _LOGGER.error( + "%s teaching of file %s failed with message:%s", + CLASSIFIER, + file_path, + response.text, + ) except requests.exceptions.ConnectionError: _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) @@ -142,8 +155,7 @@ def valid_file_path(file_path): cv.isfile(file_path) return True except vol.Invalid: - _LOGGER.error( - "%s error: Invalid file path: %s", CLASSIFIER, file_path) + _LOGGER.error("%s error: Invalid file path: %s", CLASSIFIER, file_path) return False @@ -164,15 +176,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entities = [] for camera in config[CONF_SOURCE]: facebox = FaceClassifyEntity( - ip_address, port, username, password, hostname, - camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) + ip_address, + port, + username, + password, + hostname, + camera[CONF_ENTITY_ID], + camera.get(CONF_NAME), + ) entities.append(facebox) hass.data[DATA_FACEBOX].append(facebox) add_entities(entities) def service_handle(service): """Handle for services.""" - entity_ids = service.data.get('entity_id') + entity_ids = service.data.get("entity_id") classifiers = hass.data[DATA_FACEBOX] if entity_ids: @@ -184,21 +202,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None): classifier.teach(name, file_path) hass.services.register( - DOMAIN, SERVICE_TEACH_FACE, service_handle, - schema=SERVICE_TEACH_SCHEMA) + DOMAIN, SERVICE_TEACH_FACE, service_handle, schema=SERVICE_TEACH_SCHEMA + ) class FaceClassifyEntity(ImageProcessingFaceEntity): """Perform a face classification.""" - def __init__(self, ip_address, port, username, password, hostname, - camera_entity, name=None): + def __init__( + self, ip_address, port, username, password, hostname, camera_entity, name=None + ): """Init with the API key and model id.""" super().__init__() - self._url_check = "http://{}:{}/{}/check".format( - ip_address, port, CLASSIFIER) - self._url_teach = "http://{}:{}/{}/teach".format( - ip_address, port, CLASSIFIER) + self._url_check = "http://{}:{}/{}/check".format(ip_address, port, CLASSIFIER) + self._url_teach = "http://{}:{}/{}/teach".format(ip_address, port, CLASSIFIER) self._username = username self._password = password self._hostname = hostname @@ -212,13 +229,12 @@ class FaceClassifyEntity(ImageProcessingFaceEntity): def process_image(self, image): """Process an image.""" - response = post_image( - self._url_check, image, self._username, self._password) + response = post_image(self._url_check, image, self._username, self._password) if response: response_json = response.json() - if response_json['success']: - total_faces = response_json['facesCount'] - faces = parse_faces(response_json['faces']) + if response_json["success"]: + total_faces = response_json["facesCount"] + faces = parse_faces(response_json["faces"]) self._matched = get_matched_faces(faces) self.process_faces(faces, total_faces) @@ -229,11 +245,11 @@ class FaceClassifyEntity(ImageProcessingFaceEntity): def teach(self, name, file_path): """Teach classifier a face name.""" - if (not self.hass.config.is_allowed_path(file_path) - or not valid_file_path(file_path)): + if not self.hass.config.is_allowed_path(file_path) or not valid_file_path( + file_path + ): return - teach_file( - self._url_teach, name, file_path, self._username, self._password) + teach_file(self._url_teach, name, file_path, self._username, self._password) @property def camera_entity(self): @@ -249,7 +265,7 @@ class FaceClassifyEntity(ImageProcessingFaceEntity): def device_state_attributes(self): """Return the classifier attributes.""" return { - 'matched_faces': self._matched, - 'total_matched_faces': len(self._matched), - 'hostname': self._hostname - } + "matched_faces": self._matched, + "total_matched_faces": len(self._matched), + "hostname": self._hostname, + } diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 78b11b1942b..d5e3d6064ea 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -15,31 +15,30 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, CONF_FILE_PATH -) +from homeassistant.const import CONF_NAME, CONF_FILE_PATH from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_JAILS = 'jails' +CONF_JAILS = "jails" -DEFAULT_NAME = 'fail2ban' -DEFAULT_LOG = '/var/log/fail2ban.log' +DEFAULT_NAME = "fail2ban" +DEFAULT_LOG = "/var/log/fail2ban.log" -STATE_CURRENT_BANS = 'current_bans' -STATE_ALL_BANS = 'total_bans' +STATE_CURRENT_BANS = "current_bans" +STATE_ALL_BANS = "total_bans" SCAN_INTERVAL = timedelta(seconds=120) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_JAILS): vol.All(cv.ensure_list, vol.Length(min=1)), - vol.Optional(CONF_FILE_PATH): cv.isfile, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_JAILS): vol.All(cv.ensure_list, vol.Length(min=1)), + vol.Optional(CONF_FILE_PATH): cv.isfile, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the fail2ban sensor.""" name = config.get(CONF_NAME) jails = config.get(CONF_JAILS) @@ -58,7 +57,7 @@ class BanSensor(Entity): def __init__(self, name, jail, log_parser): """Initialize the sensor.""" - self._name = '{} {}'.format(name, jail) + self._name = "{} {}".format(name, jail) self.jail = jail self.ban_dict = {STATE_CURRENT_BANS: [], STATE_ALL_BANS: []} self.last_ban = None @@ -91,7 +90,7 @@ class BanSensor(Entity): for entry in self.log_parser.data: _LOGGER.debug(entry) current_ip = entry[1] - if entry[0] == 'Ban': + if entry[0] == "Ban": if current_ip not in self.ban_dict[STATE_CURRENT_BANS]: self.ban_dict[STATE_CURRENT_BANS].append(current_ip) if current_ip not in self.ban_dict[STATE_ALL_BANS]: @@ -99,14 +98,14 @@ class BanSensor(Entity): if len(self.ban_dict[STATE_ALL_BANS]) > 10: self.ban_dict[STATE_ALL_BANS].pop(0) - elif entry[0] == 'Unban': + elif entry[0] == "Unban": if current_ip in self.ban_dict[STATE_CURRENT_BANS]: self.ban_dict[STATE_CURRENT_BANS].remove(current_ip) if self.ban_dict[STATE_CURRENT_BANS]: self.last_ban = self.ban_dict[STATE_CURRENT_BANS][-1] else: - self.last_ban = 'None' + self.last_ban = "None" class BanLogParser: @@ -122,10 +121,8 @@ class BanLogParser: """Read the fail2ban log and find entries for jail.""" self.data = list() try: - with open(self.log_file, 'r', encoding='utf-8') as file_data: + with open(self.log_file, "r", encoding="utf-8") as file_data: self.data = self.ip_regex[jail].findall(file_data.read()) - except (IndexError, FileNotFoundError, IsADirectoryError, - UnboundLocalError): - _LOGGER.warning("File not present: %s", - os.path.basename(self.log_file)) + except (IndexError, FileNotFoundError, IsADirectoryError, UnboundLocalError): + _LOGGER.warning("File not present: %s", os.path.basename(self.log_file)) diff --git a/homeassistant/components/familyhub/camera.py b/homeassistant/components/familyhub/camera.py index e9a8bcd94a6..546d95f24d1 100644 --- a/homeassistant/components/familyhub/camera.py +++ b/homeassistant/components/familyhub/camera.py @@ -10,18 +10,20 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'FamilyHub Camera' +DEFAULT_NAME = "FamilyHub Camera" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Family Hub Camera.""" from pyfamilyhublocal import FamilyHubCam + address = config.get(CONF_IP_ADDRESS) name = config.get(CONF_NAME) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 23015769f28..235e8cf5fad 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -6,81 +6,71 @@ import logging import voluptuous as vol from homeassistant.components import group -from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE, - SERVICE_TURN_OFF, ATTR_ENTITY_ID) +from homeassistant.const import SERVICE_TURN_ON, SERVICE_TOGGLE, SERVICE_TURN_OFF from homeassistant.loader import bind_hass from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import ( # noqa - PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) + ENTITY_SERVICE_SCHEMA, + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DOMAIN = 'fan' +DOMAIN = "fan" SCAN_INTERVAL = timedelta(seconds=30) -GROUP_NAME_ALL_FANS = 'all fans' +GROUP_NAME_ALL_FANS = "all fans" ENTITY_ID_ALL_FANS = group.ENTITY_ID_FORMAT.format(GROUP_NAME_ALL_FANS) -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" # Bitfield of features supported by the fan entity SUPPORT_SET_SPEED = 1 SUPPORT_OSCILLATE = 2 SUPPORT_DIRECTION = 4 -SERVICE_SET_SPEED = 'set_speed' -SERVICE_OSCILLATE = 'oscillate' -SERVICE_SET_DIRECTION = 'set_direction' +SERVICE_SET_SPEED = "set_speed" +SERVICE_OSCILLATE = "oscillate" +SERVICE_SET_DIRECTION = "set_direction" -SPEED_OFF = 'off' -SPEED_LOW = 'low' -SPEED_MEDIUM = 'medium' -SPEED_HIGH = 'high' +SPEED_OFF = "off" +SPEED_LOW = "low" +SPEED_MEDIUM = "medium" +SPEED_HIGH = "high" -DIRECTION_FORWARD = 'forward' -DIRECTION_REVERSE = 'reverse' +DIRECTION_FORWARD = "forward" +DIRECTION_REVERSE = "reverse" -ATTR_SPEED = 'speed' -ATTR_SPEED_LIST = 'speed_list' -ATTR_OSCILLATING = 'oscillating' -ATTR_DIRECTION = 'direction' +ATTR_SPEED = "speed" +ATTR_SPEED_LIST = "speed_list" +ATTR_OSCILLATING = "oscillating" +ATTR_DIRECTION = "direction" PROP_TO_ATTR = { - 'speed': ATTR_SPEED, - 'speed_list': ATTR_SPEED_LIST, - 'oscillating': ATTR_OSCILLATING, - 'direction': ATTR_DIRECTION, + "speed": ATTR_SPEED, + "speed_list": ATTR_SPEED_LIST, + "oscillating": ATTR_OSCILLATING, + "direction": ATTR_DIRECTION, } # type: dict -FAN_SET_SPEED_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_SPEED): cv.string -}) # type: dict +FAN_SET_SPEED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_SPEED): cv.string} +) # type: dict -FAN_TURN_ON_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Optional(ATTR_SPEED): cv.string -}) # type: dict +FAN_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Optional(ATTR_SPEED): cv.string} +) # type: dict -FAN_TURN_OFF_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids -}) # type: dict +FAN_OSCILLATE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_OSCILLATING): cv.boolean} +) # type: dict -FAN_OSCILLATE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_OSCILLATING): cv.boolean -}) # type: dict - -FAN_TOGGLE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids -}) - -FAN_SET_DIRECTION_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Optional(ATTR_DIRECTION): cv.string -}) # type: dict +FAN_SET_DIRECTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Optional(ATTR_DIRECTION): cv.string} +) # type: dict @bind_hass @@ -94,33 +84,28 @@ def is_on(hass, entity_id: str = None) -> bool: async def async_setup(hass, config: dict): """Expose fan control via statemachine and services.""" component = hass.data[DOMAIN] = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_FANS) + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_FANS + ) await component.async_setup(config) component.async_register_entity_service( - SERVICE_TURN_ON, FAN_TURN_ON_SCHEMA, - 'async_turn_on' + SERVICE_TURN_ON, FAN_TURN_ON_SCHEMA, "async_turn_on" ) component.async_register_entity_service( - SERVICE_TURN_OFF, FAN_TURN_OFF_SCHEMA, - 'async_turn_off' + SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" ) component.async_register_entity_service( - SERVICE_TOGGLE, FAN_TOGGLE_SCHEMA, - 'async_toggle' + SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" ) component.async_register_entity_service( - SERVICE_SET_SPEED, FAN_SET_SPEED_SCHEMA, - 'async_set_speed' + SERVICE_SET_SPEED, FAN_SET_SPEED_SCHEMA, "async_set_speed" ) component.async_register_entity_service( - SERVICE_OSCILLATE, FAN_OSCILLATE_SCHEMA, - 'async_oscillate' + SERVICE_OSCILLATE, FAN_OSCILLATE_SCHEMA, "async_oscillate" ) component.async_register_entity_service( - SERVICE_SET_DIRECTION, FAN_SET_DIRECTION_SCHEMA, - 'async_set_direction' + SERVICE_SET_DIRECTION, FAN_SET_DIRECTION_SCHEMA, "async_set_direction" ) return True @@ -176,8 +161,7 @@ class FanEntity(ToggleEntity): """ if speed is SPEED_OFF: return self.async_turn_off() - return self.hass.async_add_job( - ft.partial(self.turn_on, speed, **kwargs)) + return self.hass.async_add_job(ft.partial(self.turn_on, speed, **kwargs)) def oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index 3fe860a81fd..a40c2597222 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -10,22 +10,28 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval -DOMAIN = 'fastdotcom' -DATA_UPDATED = '{}_data_updated'.format(DOMAIN) +DOMAIN = "fastdotcom" +DATA_UPDATED = "{}_data_updated".format(DOMAIN) _LOGGER = logging.getLogger(__name__) -CONF_MANUAL = 'manual' +CONF_MANUAL = "manual" DEFAULT_INTERVAL = timedelta(hours=1) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): - vol.All(cv.time_period, cv.positive_timedelta), - vol.Optional(CONF_MANUAL, default=False): cv.boolean, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): vol.All( + cv.time_period, cv.positive_timedelta + ), + vol.Optional(CONF_MANUAL, default=False): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -34,19 +40,15 @@ async def async_setup(hass, config): data = hass.data[DOMAIN] = SpeedtestData(hass) if not conf[CONF_MANUAL]: - async_track_time_interval( - hass, data.update, conf[CONF_SCAN_INTERVAL] - ) + async_track_time_interval(hass, data.update, conf[CONF_SCAN_INTERVAL]) def update(call=None): """Service call to manually update the data.""" data.update() - hass.services.async_register(DOMAIN, 'speedtest', update) + hass.services.async_register(DOMAIN, "speedtest", update) - hass.async_create_task( - async_load_platform(hass, 'sensor', DOMAIN, {}, config) - ) + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) return True @@ -62,6 +64,7 @@ class SpeedtestData: def update(self, now=None): """Get the latest data from fast.com.""" from fastdotcom import fast_com + _LOGGER.debug("Executing fast.com speedtest") - self.data = {'download': fast_com()} + self.data = {"download": fast_com()} dispatcher_send(self._hass, DATA_UPDATED) diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index c9af8e53ab8..6d9445ce159 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -9,13 +9,12 @@ from . import DATA_UPDATED, DOMAIN as FASTDOTCOM_DOMAIN _LOGGER = logging.getLogger(__name__) -ICON = 'mdi:speedometer' +ICON = "mdi:speedometer" -UNIT_OF_MEASUREMENT = 'Mbit/s' +UNIT_OF_MEASUREMENT = "Mbit/s" -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Fast.com sensor.""" async_add_entities([SpeedtestSensor(hass.data[FASTDOTCOM_DOMAIN])]) @@ -25,7 +24,7 @@ class SpeedtestSensor(RestoreEntity): def __init__(self, speedtest_data): """Initialize the sensor.""" - self._name = 'Fast.com Download' + self._name = "Fast.com Download" self.speedtest_client = speedtest_data self._state = None @@ -71,7 +70,7 @@ class SpeedtestSensor(RestoreEntity): data = self.speedtest_client.data if data is None: return - self._state = data['download'] + self._state = data["download"] @callback def _schedule_immediate_update(self): diff --git a/homeassistant/components/fedex/sensor.py b/homeassistant/components/fedex/sensor.py index aec1cee053c..a18d6a4c651 100644 --- a/homeassistant/components/fedex/sensor.py +++ b/homeassistant/components/fedex/sensor.py @@ -7,8 +7,13 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - ATTR_ATTRIBUTION, CONF_SCAN_INTERVAL) +from homeassistant.const import ( + CONF_NAME, + CONF_USERNAME, + CONF_PASSWORD, + ATTR_ATTRIBUTION, + CONF_SCAN_INTERVAL, +) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util import slugify @@ -16,21 +21,23 @@ from homeassistant.util.dt import now, parse_date _LOGGER = logging.getLogger(__name__) -COOKIE = 'fedexdeliverymanager_cookies.pickle' +COOKIE = "fedexdeliverymanager_cookies.pickle" -DOMAIN = 'fedex' +DOMAIN = "fedex" -ICON = 'mdi:package-variant-closed' +ICON = "mdi:package-variant-closed" -STATUS_DELIVERED = 'delivered' +STATUS_DELIVERED = "delivered" SCAN_INTERVAL = timedelta(seconds=1800) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -43,8 +50,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: cookie = hass.config.path(COOKIE) session = fedexdeliverymanager.get_session( - config.get(CONF_USERNAME), config.get(CONF_PASSWORD), - cookie_path=cookie) + config.get(CONF_USERNAME), config.get(CONF_PASSWORD), cookie_path=cookie + ) except fedexdeliverymanager.FedexError: _LOGGER.exception("Could not connect to Fedex Delivery Manager") return False @@ -76,22 +83,23 @@ class FedexSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return 'packages' + return "packages" def _update(self): """Update device state.""" import fedexdeliverymanager + status_counts = defaultdict(int) for package in fedexdeliverymanager.get_packages(self._session): - status = slugify(package['primary_status']) - skip = status == STATUS_DELIVERED and \ - parse_date(package['delivery_date']) < now().date() + status = slugify(package["primary_status"]) + skip = ( + status == STATUS_DELIVERED + and parse_date(package["delivery_date"]) < now().date() + ) if skip: continue status_counts[status] += 1 - self._attributes = { - ATTR_ATTRIBUTION: fedexdeliverymanager.ATTRIBUTION - } + self._attributes = {ATTR_ATTRIBUTION: fedexdeliverymanager.ATTRIBUTION} self._attributes.update(status_counts) self._state = sum(status_counts.values()) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index d2acb674ec7..cdd76a56e16 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -13,25 +13,30 @@ import homeassistant.helpers.config_validation as cv _LOGGER = getLogger(__name__) -CONF_URLS = 'urls' -CONF_MAX_ENTRIES = 'max_entries' +CONF_URLS = "urls" +CONF_MAX_ENTRIES = "max_entries" DEFAULT_MAX_ENTRIES = 20 DEFAULT_SCAN_INTERVAL = timedelta(hours=1) -DOMAIN = 'feedreader' +DOMAIN = "feedreader" -EVENT_FEEDREADER = 'feedreader' +EVENT_FEEDREADER = "feedreader" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: { - vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): - cv.time_period, - vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES): - cv.positive_int - } -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: { + vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + vol.Optional( + CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES + ): cv.positive_int, + } + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -41,8 +46,9 @@ def setup(hass, config): max_entries = config.get(DOMAIN).get(CONF_MAX_ENTRIES) data_file = hass.config.path("{}.pickle".format(DOMAIN)) storage = StoredData(data_file) - feeds = [FeedManager(url, scan_interval, max_entries, hass, storage) for - url in urls] + feeds = [ + FeedManager(url, scan_interval, max_entries, hass, storage) for url in urls + ] return len(feeds) > 0 @@ -63,8 +69,7 @@ class FeedManager: self._has_published_parsed = False self._event_type = EVENT_FEEDREADER self._feed_id = url - hass.bus.listen_once( - EVENT_HOMEASSISTANT_START, lambda _: self._update()) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: self._update()) self._init_regular_updates(hass) def _log_no_entries(self): @@ -73,8 +78,7 @@ class FeedManager: def _init_regular_updates(self, hass): """Schedule regular updates at the top of the clock.""" - track_time_interval(hass, lambda now: self._update(), - self._scan_interval) + track_time_interval(hass, lambda now: self._update(), self._scan_interval) @property def last_update_successful(self): @@ -84,12 +88,13 @@ class FeedManager: def _update(self): """Update the feed and publish new entries to the event bus.""" import feedparser + _LOGGER.info("Fetching new data from feed %s", self._url) - self._feed = feedparser.parse(self._url, - etag=None if not self._feed - else self._feed.get('etag'), - modified=None if not self._feed - else self._feed.get('modified')) + self._feed = feedparser.parse( + self._url, + etag=None if not self._feed else self._feed.get("etag"), + modified=None if not self._feed else self._feed.get("modified"), + ) if not self._feed: _LOGGER.error("Error fetching feed data from %s", self._url) self._last_update_successful = False @@ -101,18 +106,23 @@ class FeedManager: # If an error is detected here, log error message but continue # processing the feed entries if present. if self._feed.bozo != 0: - _LOGGER.error("Error parsing feed %s: %s", self._url, - self._feed.bozo_exception) + _LOGGER.error( + "Error parsing feed %s: %s", self._url, self._feed.bozo_exception + ) # Using etag and modified, if there's no new data available, # the entries list will be empty if self._feed.entries: - _LOGGER.debug("%s entri(es) available in feed %s", - len(self._feed.entries), self._url) + _LOGGER.debug( + "%s entri(es) available in feed %s", + len(self._feed.entries), + self._url, + ) self._filter_entries() self._publish_new_entries() if self._has_published_parsed: self._storage.put_timestamp( - self._feed_id, self._last_entry_timestamp) + self._feed_id, self._last_entry_timestamp + ) else: self._log_no_entries() self._last_update_successful = True @@ -121,23 +131,26 @@ class FeedManager: def _filter_entries(self): """Filter the entries provided and return the ones to keep.""" if len(self._feed.entries) > self._max_entries: - _LOGGER.debug("Processing only the first %s entries " - "in feed %s", self._max_entries, self._url) - self._feed.entries = self._feed.entries[0:self._max_entries] + _LOGGER.debug( + "Processing only the first %s entries " "in feed %s", + self._max_entries, + self._url, + ) + self._feed.entries = self._feed.entries[0 : self._max_entries] def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" # We are lucky, `published_parsed` data available, let's make use of # it to publish only new available entries since the last run - if 'published_parsed' in entry.keys(): + if "published_parsed" in entry.keys(): self._has_published_parsed = True self._last_entry_timestamp = max( - entry.published_parsed, self._last_entry_timestamp) + entry.published_parsed, self._last_entry_timestamp + ) else: self._has_published_parsed = False - _LOGGER.debug("No published_parsed info available for entry %s", - entry) - entry.update({'feed_url': self._url}) + _LOGGER.debug("No published_parsed info available for entry %s", entry) + entry.update({"feed_url": self._url}) self._hass.bus.fire(self._event_type, entry) def _publish_new_entries(self): @@ -148,12 +161,12 @@ class FeedManager: self._firstrun = False else: # Set last entry timestamp as epoch time if not available - self._last_entry_timestamp = \ - datetime.utcfromtimestamp(0).timetuple() + self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple() for entry in self._feed.entries: if self._firstrun or ( - 'published_parsed' in entry.keys() and - entry.published_parsed > self._last_entry_timestamp): + "published_parsed" in entry.keys() + and entry.published_parsed > self._last_entry_timestamp + ): self._update_and_fire_entry(entry) new_entries = True else: @@ -179,12 +192,13 @@ class StoredData: if self._cache_outdated and exists(self._data_file): try: _LOGGER.debug("Fetching data from file %s", self._data_file) - with self._lock, open(self._data_file, 'rb') as myfile: + with self._lock, open(self._data_file, "rb") as myfile: self._data = pickle.load(myfile) or {} self._cache_outdated = False except: # noqa: E722 pylint: disable=bare-except - _LOGGER.error("Error loading data from pickled file %s", - self._data_file) + _LOGGER.error( + "Error loading data from pickled file %s", self._data_file + ) def get_timestamp(self, feed_id): """Return stored timestamp for given feed id (usually the url).""" @@ -194,13 +208,15 @@ class StoredData: def put_timestamp(self, feed_id, timestamp): """Update timestamp for given feed id (usually the url).""" self._fetch_data() - with self._lock, open(self._data_file, 'wb') as myfile: + with self._lock, open(self._data_file, "wb") as myfile: self._data.update({feed_id: timestamp}) - _LOGGER.debug("Overwriting feed %s timestamp in storage file %s", - feed_id, self._data_file) + _LOGGER.debug( + "Overwriting feed %s timestamp in storage file %s", + feed_id, + self._data_file, + ) try: pickle.dump(self._data, myfile) except: # noqa: E722 pylint: disable=bare-except - _LOGGER.error( - "Error saving pickled data to %s", self._data_file) + _LOGGER.error("Error saving pickled data to %s", self._data_file) self._cache_outdated = True diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 7252e617c5a..51e1cac3859 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -6,53 +6,56 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.const import ( - ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect) + async_dispatcher_send, + async_dispatcher_connect, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -DOMAIN = 'ffmpeg' +DOMAIN = "ffmpeg" _LOGGER = logging.getLogger(__name__) -SERVICE_START = 'start' -SERVICE_STOP = 'stop' -SERVICE_RESTART = 'restart' +SERVICE_START = "start" +SERVICE_STOP = "stop" +SERVICE_RESTART = "restart" -SIGNAL_FFMPEG_START = 'ffmpeg.start' -SIGNAL_FFMPEG_STOP = 'ffmpeg.stop' -SIGNAL_FFMPEG_RESTART = 'ffmpeg.restart' +SIGNAL_FFMPEG_START = "ffmpeg.start" +SIGNAL_FFMPEG_STOP = "ffmpeg.stop" +SIGNAL_FFMPEG_RESTART = "ffmpeg.restart" -DATA_FFMPEG = 'ffmpeg' +DATA_FFMPEG = "ffmpeg" -CONF_INITIAL_STATE = 'initial_state' -CONF_INPUT = 'input' -CONF_FFMPEG_BIN = 'ffmpeg_bin' -CONF_EXTRA_ARGUMENTS = 'extra_arguments' -CONF_OUTPUT = 'output' +CONF_INITIAL_STATE = "initial_state" +CONF_INPUT = "input" +CONF_FFMPEG_BIN = "ffmpeg_bin" +CONF_EXTRA_ARGUMENTS = "extra_arguments" +CONF_OUTPUT = "output" -DEFAULT_BINARY = 'ffmpeg' +DEFAULT_BINARY = "ffmpeg" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_FFMPEG_BIN, default=DEFAULT_BINARY): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Optional(CONF_FFMPEG_BIN, default=DEFAULT_BINARY): cv.string} + ) + }, + extra=vol.ALLOW_EXTRA, +) -SERVICE_FFMPEG_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) +SERVICE_FFMPEG_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) async def async_setup(hass, config): """Set up the FFmpeg component.""" conf = config.get(DOMAIN, {}) - manager = FFmpegManager( - hass, - conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY) - ) + manager = FFmpegManager(hass, conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY)) await manager.async_get_version() @@ -69,16 +72,16 @@ async def async_setup(hass, config): async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids) hass.services.async_register( - DOMAIN, SERVICE_START, async_service_handle, - schema=SERVICE_FFMPEG_SCHEMA) + DOMAIN, SERVICE_START, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA + ) hass.services.async_register( - DOMAIN, SERVICE_STOP, async_service_handle, - schema=SERVICE_FFMPEG_SCHEMA) + DOMAIN, SERVICE_STOP, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA + ) hass.services.async_register( - DOMAIN, SERVICE_RESTART, async_service_handle, - schema=SERVICE_FFMPEG_SCHEMA) + DOMAIN, SERVICE_RESTART, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA + ) hass.data[DATA_FFMPEG] = manager return True @@ -119,9 +122,9 @@ class FFmpegManager: def ffmpeg_stream_content_type(self): """Return HTTP content type for ffmpeg stream.""" if self._major_version is not None and self._major_version > 3: - return 'multipart/x-mixed-replace;boundary=ffmpeg' + return "multipart/x-mixed-replace;boundary=ffmpeg" - return 'multipart/x-mixed-replace;boundary=ffserver' + return "multipart/x-mixed-replace;boundary=ffserver" class FFmpegBase(Entity): @@ -138,11 +141,12 @@ class FFmpegBase(Entity): This method is a coroutine. """ async_dispatcher_connect( - self.hass, SIGNAL_FFMPEG_START, self._async_start_ffmpeg) + self.hass, SIGNAL_FFMPEG_START, self._async_start_ffmpeg + ) + async_dispatcher_connect(self.hass, SIGNAL_FFMPEG_STOP, self._async_stop_ffmpeg) async_dispatcher_connect( - self.hass, SIGNAL_FFMPEG_STOP, self._async_stop_ffmpeg) - async_dispatcher_connect( - self.hass, SIGNAL_FFMPEG_RESTART, self._async_restart_ffmpeg) + self.hass, SIGNAL_FFMPEG_RESTART, self._async_restart_ffmpeg + ) # register start/stop self._async_register_events() @@ -184,12 +188,12 @@ class FFmpegBase(Entity): @callback def _async_register_events(self): """Register a FFmpeg process/device.""" + async def async_shutdown_handle(event): """Stop FFmpeg process.""" await self._async_stop_ffmpeg(None) - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_shutdown_handle) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown_handle) # start on startup if not self.initial_state: @@ -200,5 +204,4 @@ class FFmpegBase(Entity): await self._async_start_ffmpeg(None) self.async_schedule_update_ha_state() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_start_handle) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start_handle) diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 20b4e538085..598ffe36bd4 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -4,8 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components.camera import ( - PLATFORM_SCHEMA, Camera, SUPPORT_STREAM) +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_STREAM from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream import homeassistant.helpers.config_validation as cv @@ -14,18 +13,19 @@ from . import CONF_EXTRA_ARGUMENTS, CONF_INPUT, DATA_FFMPEG _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'FFmpeg' +DEFAULT_NAME = "FFmpeg" DEFAULT_ARGUMENTS = "-pred 1" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_INPUT): cv.string, - vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_INPUT): cv.string, + vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a FFmpeg camera.""" async_add_entities([FFmpegCamera(hass, config)]) @@ -49,16 +49,19 @@ class FFmpegCamera(Camera): async def stream_source(self): """Return the stream source.""" - return self._input.split(' ')[-1] + return self._input.split(" ")[-1] async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg.tools import ImageFrame, IMAGE_JPEG + ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) - image = await asyncio.shield(ffmpeg.get_image( - self._input, output_format=IMAGE_JPEG, - extra_cmd=self._extra_arguments)) + image = await asyncio.shield( + ffmpeg.get_image( + self._input, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments + ) + ) return image async def handle_async_mjpeg_stream(self, request): @@ -66,14 +69,16 @@ class FFmpegCamera(Camera): from haffmpeg.camera import CameraMjpeg stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) - await stream.open_camera( - self._input, extra_cmd=self._extra_arguments) + await stream.open_camera(self._input, extra_cmd=self._extra_arguments) try: stream_reader = await stream.get_reader() return await async_aiohttp_proxy_stream( - self.hass, request, stream_reader, - self._manager.ffmpeg_stream_content_type) + self.hass, + request, + stream_reader, + self._manager.ffmpeg_stream_content_type, + ) finally: await stream.close() diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index 03aacf3aafb..235a9e4b009 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -5,41 +5,49 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import ( - FFmpegBase, DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS, - CONF_INITIAL_STATE) + FFmpegBase, + DATA_FFMPEG, + CONF_INPUT, + CONF_EXTRA_ARGUMENTS, + CONF_INITIAL_STATE, +) from homeassistant.const import CONF_NAME _LOGGER = logging.getLogger(__name__) -CONF_RESET = 'reset' -CONF_CHANGES = 'changes' -CONF_REPEAT = 'repeat' -CONF_REPEAT_TIME = 'repeat_time' +CONF_RESET = "reset" +CONF_CHANGES = "changes" +CONF_REPEAT = "repeat" +CONF_REPEAT_TIME = "repeat_time" -DEFAULT_NAME = 'FFmpeg Motion' +DEFAULT_NAME = "FFmpeg Motion" DEFAULT_INIT_STATE = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_INPUT): cv.string, - vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, - vol.Optional(CONF_RESET, default=10): - vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_CHANGES, default=10): - vol.All(vol.Coerce(float), vol.Range(min=0, max=99)), - vol.Inclusive(CONF_REPEAT, 'repeat'): - vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Inclusive(CONF_REPEAT_TIME, 'repeat'): - vol.All(vol.Coerce(int), vol.Range(min=1)), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_INPUT): cv.string, + vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, + vol.Optional(CONF_RESET, default=10): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Optional(CONF_CHANGES, default=10): vol.All( + vol.Coerce(float), vol.Range(min=0, max=99) + ), + vol.Inclusive(CONF_REPEAT, "repeat"): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Inclusive(CONF_REPEAT_TIME, "repeat"): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the FFmpeg binary motion sensor.""" manager = hass.data[DATA_FFMPEG] entity = FFmpegMotion(hass, manager, config) @@ -82,8 +90,7 @@ class FFmpegMotion(FFmpegBinarySensor): from haffmpeg.sensor import SensorMotion super().__init__(config) - self.ffmpeg = SensorMotion( - manager.binary, hass.loop, self._async_callback) + self.ffmpeg = SensorMotion(manager.binary, hass.loop, self._async_callback) async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. @@ -110,4 +117,4 @@ class FFmpegMotion(FFmpegBinarySensor): @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - return 'motion' + return "motion" diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index 7fbda8ca18b..00e5dbb682f 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -5,38 +5,44 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import PLATFORM_SCHEMA -from homeassistant.components.ffmpeg_motion.binary_sensor import ( - FFmpegBinarySensor) +from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor from homeassistant.components.ffmpeg import ( - DATA_FFMPEG, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS, - CONF_INITIAL_STATE) + DATA_FFMPEG, + CONF_INPUT, + CONF_OUTPUT, + CONF_EXTRA_ARGUMENTS, + CONF_INITIAL_STATE, +) from homeassistant.const import CONF_NAME _LOGGER = logging.getLogger(__name__) -CONF_PEAK = 'peak' -CONF_DURATION = 'duration' -CONF_RESET = 'reset' +CONF_PEAK = "peak" +CONF_DURATION = "duration" +CONF_RESET = "reset" -DEFAULT_NAME = 'FFmpeg Noise' +DEFAULT_NAME = "FFmpeg Noise" DEFAULT_INIT_STATE = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_INPUT): cv.string, - vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, - vol.Optional(CONF_OUTPUT): cv.string, - vol.Optional(CONF_PEAK, default=-30): vol.Coerce(int), - vol.Optional(CONF_DURATION, default=1): - vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_RESET, default=10): - vol.All(vol.Coerce(int), vol.Range(min=1)), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_INPUT): cv.string, + vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, + vol.Optional(CONF_OUTPUT): cv.string, + vol.Optional(CONF_PEAK, default=-30): vol.Coerce(int), + vol.Optional(CONF_DURATION, default=1): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Optional(CONF_RESET, default=10): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the FFmpeg noise binary sensor.""" manager = hass.data[DATA_FFMPEG] entity = FFmpegNoise(hass, manager, config) @@ -51,8 +57,7 @@ class FFmpegNoise(FFmpegBinarySensor): from haffmpeg.sensor import SensorNoise super().__init__(config) - self.ffmpeg = SensorNoise( - manager.binary, hass.loop, self._async_callback) + self.ffmpeg = SensorNoise(manager.binary, hass.loop, self._async_callback) async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. @@ -77,4 +82,4 @@ class FFmpegNoise(FFmpegBinarySensor): @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - return 'sound' + return "sound" diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index f78afbf10e5..d47c2b0c2d2 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -5,9 +5,17 @@ from typing import Optional import voluptuous as vol from homeassistant.const import ( - ATTR_ARMED, ATTR_BATTERY_LEVEL, CONF_DEVICE_CLASS, CONF_EXCLUDE, - CONF_ICON, CONF_PASSWORD, CONF_URL, CONF_USERNAME, - CONF_WHITE_VALUE, EVENT_HOMEASSISTANT_STOP) + ATTR_ARMED, + ATTR_BATTERY_LEVEL, + CONF_DEVICE_CLASS, + CONF_EXCLUDE, + CONF_ICON, + CONF_PASSWORD, + CONF_URL, + CONF_USERNAME, + CONF_WHITE_VALUE, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -15,72 +23,88 @@ from homeassistant.util import convert, slugify _LOGGER = logging.getLogger(__name__) -ATTR_CURRENT_ENERGY_KWH = 'current_energy_kwh' -ATTR_CURRENT_POWER_W = 'current_power_w' +ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh" +ATTR_CURRENT_POWER_W = "current_power_w" -CONF_COLOR = 'color' -CONF_DEVICE_CONFIG = 'device_config' -CONF_DIMMING = 'dimming' -CONF_GATEWAYS = 'gateways' -CONF_PLUGINS = 'plugins' -CONF_RESET_COLOR = 'reset_color' -DOMAIN = 'fibaro' -FIBARO_CONTROLLERS = 'fibaro_controllers' -FIBARO_DEVICES = 'fibaro_devices' -FIBARO_COMPONENTS = ['binary_sensor', 'climate', 'cover', 'light', - 'scene', 'sensor', 'switch'] +CONF_COLOR = "color" +CONF_DEVICE_CONFIG = "device_config" +CONF_DIMMING = "dimming" +CONF_GATEWAYS = "gateways" +CONF_PLUGINS = "plugins" +CONF_RESET_COLOR = "reset_color" +DOMAIN = "fibaro" +FIBARO_CONTROLLERS = "fibaro_controllers" +FIBARO_DEVICES = "fibaro_devices" +FIBARO_COMPONENTS = [ + "binary_sensor", + "climate", + "cover", + "light", + "scene", + "sensor", + "switch", +] FIBARO_TYPEMAP = { - 'com.fibaro.multilevelSensor': "sensor", - 'com.fibaro.binarySwitch': 'switch', - 'com.fibaro.multilevelSwitch': 'switch', - 'com.fibaro.FGD212': 'light', - 'com.fibaro.FGR': 'cover', - 'com.fibaro.doorSensor': 'binary_sensor', - 'com.fibaro.doorWindowSensor': 'binary_sensor', - 'com.fibaro.FGMS001': 'binary_sensor', - 'com.fibaro.heatDetector': 'binary_sensor', - 'com.fibaro.lifeDangerSensor': 'binary_sensor', - 'com.fibaro.smokeSensor': 'binary_sensor', - 'com.fibaro.remoteSwitch': 'switch', - 'com.fibaro.sensor': 'sensor', - 'com.fibaro.colorController': 'light', - 'com.fibaro.securitySensor': 'binary_sensor', - 'com.fibaro.hvac': 'climate', - 'com.fibaro.setpoint': 'climate', - 'com.fibaro.FGT001': 'climate', - 'com.fibaro.thermostatDanfoss': 'climate' + "com.fibaro.multilevelSensor": "sensor", + "com.fibaro.binarySwitch": "switch", + "com.fibaro.multilevelSwitch": "switch", + "com.fibaro.FGD212": "light", + "com.fibaro.FGR": "cover", + "com.fibaro.doorSensor": "binary_sensor", + "com.fibaro.doorWindowSensor": "binary_sensor", + "com.fibaro.FGMS001": "binary_sensor", + "com.fibaro.heatDetector": "binary_sensor", + "com.fibaro.lifeDangerSensor": "binary_sensor", + "com.fibaro.smokeSensor": "binary_sensor", + "com.fibaro.remoteSwitch": "switch", + "com.fibaro.sensor": "sensor", + "com.fibaro.colorController": "light", + "com.fibaro.securitySensor": "binary_sensor", + "com.fibaro.hvac": "climate", + "com.fibaro.setpoint": "climate", + "com.fibaro.FGT001": "climate", + "com.fibaro.thermostatDanfoss": "climate", } -DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ - vol.Optional(CONF_DIMMING): cv.boolean, - vol.Optional(CONF_COLOR): cv.boolean, - vol.Optional(CONF_WHITE_VALUE): cv.boolean, - vol.Optional(CONF_RESET_COLOR): cv.boolean, - vol.Optional(CONF_DEVICE_CLASS): cv.string, - vol.Optional(CONF_ICON): cv.string, -}) +DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema( + { + vol.Optional(CONF_DIMMING): cv.boolean, + vol.Optional(CONF_COLOR): cv.boolean, + vol.Optional(CONF_WHITE_VALUE): cv.boolean, + vol.Optional(CONF_RESET_COLOR): cv.boolean, + vol.Optional(CONF_DEVICE_CLASS): cv.string, + vol.Optional(CONF_ICON): cv.string, + } +) FIBARO_ID_LIST_SCHEMA = vol.Schema([cv.string]) -GATEWAY_CONFIG = vol.Schema({ - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_URL): cv.url, - vol.Optional(CONF_PLUGINS, default=False): cv.boolean, - vol.Optional(CONF_EXCLUDE, default=[]): FIBARO_ID_LIST_SCHEMA, - vol.Optional(CONF_DEVICE_CONFIG, default={}): - vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}) -}, extra=vol.ALLOW_EXTRA) +GATEWAY_CONFIG = vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_URL): cv.url, + vol.Optional(CONF_PLUGINS, default=False): cv.boolean, + vol.Optional(CONF_EXCLUDE, default=[]): FIBARO_ID_LIST_SCHEMA, + vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema( + {cv.string: DEVICE_CONFIG_SCHEMA_ENTRY} + ), + }, + extra=vol.ALLOW_EXTRA, +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_GATEWAYS): vol.All(cv.ensure_list, [GATEWAY_CONFIG]), - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Required(CONF_GATEWAYS): vol.All(cv.ensure_list, [GATEWAY_CONFIG])} + ) + }, + extra=vol.ALLOW_EXTRA, +) -class FibaroController(): +class FibaroController: """Initiate Fibaro Controller Class.""" def __init__(self, config): @@ -88,7 +112,8 @@ class FibaroController(): from fiblary3.client.v4.client import Client as FibaroClient self._client = FibaroClient( - config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD]) + config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] + ) self._scene_map = None # Whether to import devices from plugins self._import_plugins = config[CONF_PLUGINS] @@ -99,7 +124,7 @@ class FibaroController(): self._callbacks = {} # Update value callbacks by deviceId self._state_handler = None # Fiblary's StateHandler object self._excluded_devices = config[CONF_EXCLUDE] - self.hub_serial = None # Unique serial number of the hub + self.hub_serial = None # Unique serial number of the hub def connect(self): """Start the communication with the Fibaro controller.""" @@ -108,12 +133,12 @@ class FibaroController(): info = self._client.info.get() self.hub_serial = slugify(info.serialNumber) except AssertionError: - _LOGGER.error("Can't connect to Fibaro HC. " - "Please check URL.") + _LOGGER.error("Can't connect to Fibaro HC. " "Please check URL.") return False if login is None or login.status is False: - _LOGGER.error("Invalid login for Fibaro HC. " - "Please check username and password") + _LOGGER.error( + "Invalid login for Fibaro HC. " "Please check username and password" + ) return False self._room_map = {room.id: room for room in self._client.rooms.list()} @@ -124,6 +149,7 @@ class FibaroController(): def enable_state_handler(self): """Start StateHandler thread for monitoring updates.""" from fiblary3.client.v4.client import StateHandler + self._state_handler = StateHandler(self._client, self._on_state_change) def disable_state_handler(self): @@ -134,28 +160,26 @@ class FibaroController(): def _on_state_change(self, state): """Handle change report received from the HomeCenter.""" callback_set = set() - for change in state.get('changes', []): + for change in state.get("changes", []): try: - dev_id = change.pop('id') + dev_id = change.pop("id") if dev_id not in self._device_map.keys(): continue device = self._device_map[dev_id] for property_name, value in change.items(): - if property_name == 'log': + if property_name == "log": if value and value != "transfer OK": - _LOGGER.debug("LOG %s: %s", - device.friendly_name, value) + _LOGGER.debug("LOG %s: %s", device.friendly_name, value) continue - if property_name == 'logTemp': + if property_name == "logTemp": continue if property_name in device.properties: - device.properties[property_name] = \ - value - _LOGGER.debug("<- %s.%s = %s", device.ha_id, - property_name, str(value)) + device.properties[property_name] = value + _LOGGER.debug( + "<- %s.%s = %s", device.ha_id, property_name, str(value) + ) else: - _LOGGER.warning("%s.%s not found", device.ha_id, - property_name) + _LOGGER.warning("%s.%s not found", device.ha_id, property_name) if dev_id in self._callbacks: callback_set.add(dev_id) except (ValueError, KeyError): @@ -170,8 +194,10 @@ class FibaroController(): def get_children(self, device_id): """Get a list of child devices.""" return [ - device for device in self._device_map.values() - if device.parentId == device_id] + device + for device in self._device_map.values() + if device.parentId == device_id + ] def get_siblings(self, device_id): """Get the siblings of a device.""" @@ -182,29 +208,31 @@ class FibaroController(): """Map device to HA device type.""" # Use our lookup table to identify device type device_type = None - if 'type' in device: + if "type" in device: device_type = FIBARO_TYPEMAP.get(device.type) - if device_type is None and 'baseType' in device: + if device_type is None and "baseType" in device: device_type = FIBARO_TYPEMAP.get(device.baseType) # We can also identify device type by its capabilities if device_type is None: - if 'setBrightness' in device.actions: - device_type = 'light' - elif 'turnOn' in device.actions: - device_type = 'switch' - elif 'open' in device.actions: - device_type = 'cover' - elif 'value' in device.properties: - if device.properties.value in ('true', 'false'): - device_type = 'binary_sensor' + if "setBrightness" in device.actions: + device_type = "light" + elif "turnOn" in device.actions: + device_type = "switch" + elif "open" in device.actions: + device_type = "cover" + elif "value" in device.properties: + if device.properties.value in ("true", "false"): + device_type = "binary_sensor" else: - device_type = 'sensor' + device_type = "sensor" # Switches that control lights should show up as lights - if device_type == 'switch' and \ - device.properties.get('isLight', 'false') == 'true': - device_type = 'light' + if ( + device_type == "switch" + and device.properties.get("isLight", "false") == "true" + ): + device_type = "light" return device_type def _read_scenes(self): @@ -215,17 +243,17 @@ class FibaroController(): continue device.fibaro_controller = self if device.roomID == 0: - room_name = 'Unknown' + room_name = "Unknown" else: room_name = self._room_map[device.roomID].name device.room_name = room_name - device.friendly_name = '{} {}'.format(room_name, device.name) - device.ha_id = 'scene_{}_{}_{}'.format( - slugify(room_name), slugify(device.name), device.id) - device.unique_id_str = "{}.scene.{}".format( - self.hub_serial, device.id) + device.friendly_name = "{} {}".format(room_name, device.name) + device.ha_id = "scene_{}_{}_{}".format( + slugify(room_name), slugify(device.name), device.id + ) + device.unique_id_str = "{}.scene.{}".format(self.hub_serial, device.id) self._scene_map[device.id] = device - self.fibaro_devices['scene'].append(device) + self.fibaro_devices["scene"].append(device) def _read_devices(self): """Read and process the device list.""" @@ -237,42 +265,48 @@ class FibaroController(): try: device.fibaro_controller = self if device.roomID == 0: - room_name = 'Unknown' + room_name = "Unknown" else: room_name = self._room_map[device.roomID].name device.room_name = room_name - device.friendly_name = room_name + ' ' + device.name - device.ha_id = '{}_{}_{}'.format( - slugify(room_name), slugify(device.name), device.id) - if device.enabled and \ - ('isPlugin' not in device or - (not device.isPlugin or self._import_plugins)) and \ - device.ha_id not in self._excluded_devices: + device.friendly_name = room_name + " " + device.name + device.ha_id = "{}_{}_{}".format( + slugify(room_name), slugify(device.name), device.id + ) + if ( + device.enabled + and ( + "isPlugin" not in device + or (not device.isPlugin or self._import_plugins) + ) + and device.ha_id not in self._excluded_devices + ): device.mapped_type = self._map_device_to_type(device) - device.device_config = \ - self._device_config.get(device.ha_id, {}) + device.device_config = self._device_config.get(device.ha_id, {}) else: device.mapped_type = None dtype = device.mapped_type if dtype: - device.unique_id_str = "{}.{}".format( - self.hub_serial, device.id) + device.unique_id_str = "{}.{}".format(self.hub_serial, device.id) self._device_map[device.id] = device - if dtype != 'climate': + if dtype != "climate": self.fibaro_devices[dtype].append(device) else: # if a sibling of this has been added, skip this one # otherwise add the first visible device in the group # which is a hack, but solves a problem with FGT having # hidden compatibility devices before the real device - if last_climate_parent != device.parentId and \ - device.visible: + if last_climate_parent != device.parentId and device.visible: self.fibaro_devices[dtype].append(device) last_climate_parent = device.parentId - _LOGGER.debug("%s (%s, %s) -> %s %s", - device.ha_id, device.type, - device.baseType, dtype, - str(device)) + _LOGGER.debug( + "%s (%s, %s) -> %s %s", + device.ha_id, + device.type, + device.baseType, + dtype, + str(device), + ) except (KeyError, ValueError): pass @@ -298,12 +332,12 @@ def setup(hass, base_config): hass.data[FIBARO_CONTROLLERS][controller.hub_serial] = controller for component in FIBARO_COMPONENTS: hass.data[FIBARO_DEVICES][component].extend( - controller.fibaro_devices[component]) + controller.fibaro_devices[component] + ) if hass.data[FIBARO_CONTROLLERS]: for component in FIBARO_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, - base_config) + discovery.load_platform(hass, component, DOMAIN, {}, base_config) for controller in hass.data[FIBARO_CONTROLLERS].values(): controller.enable_state_handler() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_fibaro) @@ -333,35 +367,37 @@ class FibaroDevice(Entity): @property def level(self): """Get the level of Fibaro device.""" - if 'value' in self.fibaro_device.properties: + if "value" in self.fibaro_device.properties: return self.fibaro_device.properties.value return None @property def level2(self): """Get the tilt level of Fibaro device.""" - if 'value2' in self.fibaro_device.properties: + if "value2" in self.fibaro_device.properties: return self.fibaro_device.properties.value2 return None def dont_know_message(self, action): """Make a warning in case we don't know how to perform an action.""" - _LOGGER.warning("Not sure how to setValue: %s " - "(available actions: %s)", str(self.ha_id), - str(self.fibaro_device.actions)) + _LOGGER.warning( + "Not sure how to setValue: %s " "(available actions: %s)", + str(self.ha_id), + str(self.fibaro_device.actions), + ) def set_level(self, level): """Set the level of Fibaro device.""" self.action("setValue", level) - if 'value' in self.fibaro_device.properties: + if "value" in self.fibaro_device.properties: self.fibaro_device.properties.value = level - if 'brightness' in self.fibaro_device.properties: + if "brightness" in self.fibaro_device.properties: self.fibaro_device.properties.brightness = level def set_level2(self, level): """Set the level2 of Fibaro device.""" self.action("setValue2", level) - if 'value2' in self.fibaro_device.properties: + if "value2" in self.fibaro_device.properties: self.fibaro_device.properties.value2 = level def call_turn_on(self): @@ -380,15 +416,13 @@ class FibaroDevice(Entity): white = int(max(0, min(255, white))) color_str = "{},{},{},{}".format(red, green, blue, white) self.fibaro_device.properties.color = color_str - self.action("setColor", str(red), str(green), - str(blue), str(white)) + self.action("setColor", str(red), str(green), str(blue), str(white)) def action(self, cmd, *args): """Perform an action on the Fibaro HC.""" if cmd in self.fibaro_device.actions: getattr(self.fibaro_device, cmd)(*args) - _LOGGER.debug("-> %s.%s%s called", str(self.ha_id), - str(cmd), str(args)) + _LOGGER.debug("-> %s.%s%s called", str(self.ha_id), str(cmd), str(args)) else: self.dont_know_message(cmd) @@ -400,7 +434,7 @@ class FibaroDevice(Entity): @property def current_power_w(self): """Return the current power usage in W.""" - if 'power' in self.fibaro_device.properties: + if "power" in self.fibaro_device.properties: power = self.fibaro_device.properties.power if power: return convert(power, float, 0.0) @@ -410,10 +444,12 @@ class FibaroDevice(Entity): @property def current_binary_state(self): """Return the current binary state.""" - if self.fibaro_device.properties.value == 'false': + if self.fibaro_device.properties.value == "false": return False - if self.fibaro_device.properties.value == 'true' or \ - int(self.fibaro_device.properties.value) > 0: + if ( + self.fibaro_device.properties.value == "true" + or int(self.fibaro_device.properties.value) > 0 + ): return True return False @@ -442,19 +478,22 @@ class FibaroDevice(Entity): attr = {} try: - if 'battery' in self.fibaro_device.interfaces: - attr[ATTR_BATTERY_LEVEL] = \ - int(self.fibaro_device.properties.batteryLevel) - if 'fibaroAlarmArm' in self.fibaro_device.interfaces: + if "battery" in self.fibaro_device.interfaces: + attr[ATTR_BATTERY_LEVEL] = int( + self.fibaro_device.properties.batteryLevel + ) + if "fibaroAlarmArm" in self.fibaro_device.interfaces: attr[ATTR_ARMED] = bool(self.fibaro_device.properties.armed) - if 'power' in self.fibaro_device.interfaces: + if "power" in self.fibaro_device.interfaces: attr[ATTR_CURRENT_POWER_W] = convert( - self.fibaro_device.properties.power, float, 0.0) - if 'energy' in self.fibaro_device.interfaces: + self.fibaro_device.properties.power, float, 0.0 + ) + if "energy" in self.fibaro_device.interfaces: attr[ATTR_CURRENT_ENERGY_KWH] = convert( - self.fibaro_device.properties.energy, float, 0.0) + self.fibaro_device.properties.energy, float, 0.0 + ) except (ValueError, KeyError): pass - attr['fibaro_id'] = self.fibaro_device.id + attr["fibaro_id"] = self.fibaro_device.id return attr diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index 44448227a1c..af2c2a9401a 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -1,8 +1,7 @@ """Support for Fibaro binary sensors.""" import logging -from homeassistant.components.binary_sensor import ( - ENTITY_ID_FORMAT, BinarySensorDevice) +from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice from homeassistant.const import CONF_DEVICE_CLASS, CONF_ICON from . import FIBARO_DEVICES, FibaroDevice @@ -10,13 +9,13 @@ from . import FIBARO_DEVICES, FibaroDevice _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'com.fibaro.floodSensor': ['Flood', 'mdi:water', 'flood'], - 'com.fibaro.motionSensor': ['Motion', 'mdi:run', 'motion'], - 'com.fibaro.doorSensor': ['Door', 'mdi:window-open', 'door'], - 'com.fibaro.windowSensor': ['Window', 'mdi:window-open', 'window'], - 'com.fibaro.smokeSensor': ['Smoke', 'mdi:smoking', 'smoke'], - 'com.fibaro.FGMS001': ['Motion', 'mdi:run', 'motion'], - 'com.fibaro.heatDetector': ['Heat', 'mdi:fire', 'heat'], + "com.fibaro.floodSensor": ["Flood", "mdi:water", "flood"], + "com.fibaro.motionSensor": ["Motion", "mdi:run", "motion"], + "com.fibaro.doorSensor": ["Door", "mdi:window-open", "door"], + "com.fibaro.windowSensor": ["Window", "mdi:window-open", "window"], + "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", "smoke"], + "com.fibaro.FGMS001": ["Motion", "mdi:run", "motion"], + "com.fibaro.heatDetector": ["Heat", "mdi:fire", "heat"], } @@ -26,8 +25,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return add_entities( - [FibaroBinarySensor(device) - for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True) + [ + FibaroBinarySensor(device) + for device in hass.data[FIBARO_DEVICES]["binary_sensor"] + ], + True, + ) class FibaroBinarySensor(FibaroDevice, BinarySensorDevice): @@ -51,8 +54,7 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorDevice): self._device_class = None self._icon = None # device_config overrides: - self._device_class = devconf.get(CONF_DEVICE_CLASS, - self._device_class) + self._device_class = devconf.get(CONF_DEVICE_CLASS, self._device_class) self._icon = devconf.get(CONF_ICON, self._icon) @property diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 6a4d5429618..ed399fac209 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -3,45 +3,54 @@ import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE) + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_BOOST, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from . import FIBARO_DEVICES, FibaroDevice -PRESET_RESUME = 'resume' -PRESET_MOIST = 'moist' -PRESET_FURNACE = 'furnace' -PRESET_CHANGEOVER = 'changeover' -PRESET_ECO_HEAT = 'eco_heat' -PRESET_ECO_COOL = 'eco_cool' -PRESET_FORCE_OPEN = 'force_open' +PRESET_RESUME = "resume" +PRESET_MOIST = "moist" +PRESET_FURNACE = "furnace" +PRESET_CHANGEOVER = "changeover" +PRESET_ECO_HEAT = "eco_heat" +PRESET_ECO_COOL = "eco_cool" +PRESET_FORCE_OPEN = "force_open" _LOGGER = logging.getLogger(__name__) # SDS13781-10 Z-Wave Application Command Class Specification 2019-01-04 # Table 128, Thermostat Fan Mode Set version 4::Fan Mode encoding FANMODES = { - 0: 'off', - 1: 'low', - 2: 'auto_high', - 3: 'medium', - 4: 'auto_medium', - 5: 'high', - 6: 'circulation', - 7: 'humidity_circulation', - 8: 'left_right', - 9: 'up_down', - 10: 'quiet', - 128: 'auto' + 0: "off", + 1: "low", + 2: "auto_high", + 3: "medium", + 4: "auto_medium", + 5: "high", + 6: "circulation", + 7: "humidity_circulation", + 8: "left_right", + 9: "up_down", + 10: "quiet", + 128: "auto", } HA_FANMODES = {v: k for k, v in FANMODES.items()} # SDS13781-10 Z-Wave Application Command Class Specification 2019-01-04 # Table 130, Thermostat Mode Set version 3::Mode encoding. -# 4 AUXILARY +# 4 AUXILIARY OPMODES_PRESET = { 5: PRESET_RESUME, 7: PRESET_FURNACE, @@ -90,8 +99,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return add_entities( - [FibaroThermostat(device) - for device in hass.data[FIBARO_DEVICES]['climate']], True) + [FibaroThermostat(device) for device in hass.data[FIBARO_DEVICES]["climate"]], + True, + ) class FibaroThermostat(FibaroDevice, ClimateDevice): @@ -105,39 +115,40 @@ class FibaroThermostat(FibaroDevice, ClimateDevice): self._op_mode_device = None self._fan_mode_device = None self._support_flags = 0 - self.entity_id = 'climate.{}'.format(self.ha_id) + self.entity_id = "climate.{}".format(self.ha_id) self._hvac_support = [] self._preset_support = [] self._fan_support = [] - siblings = fibaro_device.fibaro_controller.get_siblings( - fibaro_device.id) - tempunit = 'C' + siblings = fibaro_device.fibaro_controller.get_siblings(fibaro_device.id) + tempunit = "C" for device in siblings: - if device.type == 'com.fibaro.temperatureSensor': + if device.type == "com.fibaro.temperatureSensor": self._temp_sensor_device = FibaroDevice(device) tempunit = device.properties.unit - if 'setTargetLevel' in device.actions or \ - 'setThermostatSetpoint' in device.actions: + if ( + "setTargetLevel" in device.actions + or "setThermostatSetpoint" in device.actions + ): self._target_temp_device = FibaroDevice(device) self._support_flags |= SUPPORT_TARGET_TEMPERATURE tempunit = device.properties.unit - if 'setMode' in device.actions or \ - 'setOperatingMode' in device.actions: + if "setMode" in device.actions or "setOperatingMode" in device.actions: self._op_mode_device = FibaroDevice(device) self._support_flags |= SUPPORT_PRESET_MODE - if 'setFanMode' in device.actions: + if "setFanMode" in device.actions: self._fan_mode_device = FibaroDevice(device) self._support_flags |= SUPPORT_FAN_MODE - if tempunit == 'F': + if tempunit == "F": self._unit_of_temp = TEMP_FAHRENHEIT else: self._unit_of_temp = TEMP_CELSIUS if self._fan_mode_device: - fan_modes = self._fan_mode_device.fibaro_device.\ - properties.supportedModes.split(",") + fan_modes = self._fan_mode_device.fibaro_device.properties.supportedModes.split( + "," + ) for mode in fan_modes: mode = int(mode) if mode not in FANMODES: @@ -162,29 +173,27 @@ class FibaroThermostat(FibaroDevice, ClimateDevice): async def async_added_to_hass(self): """Call when entity is added to hass.""" - _LOGGER.debug("Climate %s\n" - "- _temp_sensor_device %s\n" - "- _target_temp_device %s\n" - "- _op_mode_device %s\n" - "- _fan_mode_device %s", - self.ha_id, - self._temp_sensor_device.ha_id - if self._temp_sensor_device else "None", - self._target_temp_device.ha_id - if self._target_temp_device else "None", - self._op_mode_device.ha_id - if self._op_mode_device else "None", - self._fan_mode_device.ha_id - if self._fan_mode_device else "None") + _LOGGER.debug( + "Climate %s\n" + "- _temp_sensor_device %s\n" + "- _target_temp_device %s\n" + "- _op_mode_device %s\n" + "- _fan_mode_device %s", + self.ha_id, + self._temp_sensor_device.ha_id if self._temp_sensor_device else "None", + self._target_temp_device.ha_id if self._target_temp_device else "None", + self._op_mode_device.ha_id if self._op_mode_device else "None", + self._fan_mode_device.ha_id if self._fan_mode_device else "None", + ) await super().async_added_to_hass() # Register update callback for child devices siblings = self.fibaro_device.fibaro_controller.get_siblings( - self.fibaro_device.id) + self.fibaro_device.id + ) for device in siblings: if device != self.fibaro_device: - self.controller.register(device.id, - self._update_callback) + self.controller.register(device.id, self._update_callback) @property def supported_features(self): @@ -219,8 +228,7 @@ class FibaroThermostat(FibaroDevice, ClimateDevice): return 6 # Fan only if "operatingMode" in self._op_mode_device.fibaro_device.properties: - return int(self._op_mode_device.fibaro_device. - properties.operatingMode) + return int(self._op_mode_device.fibaro_device.properties.operatingMode) return int(self._op_mode_device.fibaro_device.properties.mode) @@ -244,8 +252,7 @@ class FibaroThermostat(FibaroDevice, ClimateDevice): return if "setOperatingMode" in self._op_mode_device.fibaro_device.actions: - self._op_mode_device.action( - "setOperatingMode", HA_OPMODES_HVAC[hvac_mode]) + self._op_mode_device.action("setOperatingMode", HA_OPMODES_HVAC[hvac_mode]) elif "setMode" in self._op_mode_device.fibaro_device.actions: self._op_mode_device.action("setMode", HA_OPMODES_HVAC[hvac_mode]) @@ -259,8 +266,7 @@ class FibaroThermostat(FibaroDevice, ClimateDevice): return None if "operatingMode" in self._op_mode_device.fibaro_device.properties: - mode = int(self._op_mode_device.fibaro_device. - properties.operatingMode) + mode = int(self._op_mode_device.fibaro_device.properties.operatingMode) else: mode = int(self._op_mode_device.fibaro_device.properties.mode) @@ -284,10 +290,10 @@ class FibaroThermostat(FibaroDevice, ClimateDevice): return if "setOperatingMode" in self._op_mode_device.fibaro_device.actions: self._op_mode_device.action( - "setOperatingMode", HA_OPMODES_PRESET[preset_mode]) + "setOperatingMode", HA_OPMODES_PRESET[preset_mode] + ) elif "setMode" in self._op_mode_device.fibaro_device.actions: - self._op_mode_device.action( - "setMode", HA_OPMODES_PRESET[preset_mode]) + self._op_mode_device.action("setMode", HA_OPMODES_PRESET[preset_mode]) @property def temperature_unit(self): @@ -316,7 +322,6 @@ class FibaroThermostat(FibaroDevice, ClimateDevice): target = self._target_temp_device if temperature is not None: if "setThermostatSetpoint" in target.fibaro_device.actions: - target.action("setThermostatSetpoint", - self.fibaro_op_mode, temperature) + target.action("setThermostatSetpoint", self.fibaro_op_mode, temperature) else: target.action("setTargetLevel", temperature) diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index 0ccbed0144b..fe9c0990fa8 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -2,7 +2,11 @@ import logging from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_TILT_POSITION, ENTITY_ID_FORMAT, CoverDevice) + ATTR_POSITION, + ATTR_TILT_POSITION, + ENTITY_ID_FORMAT, + CoverDevice, +) from . import FIBARO_DEVICES, FibaroDevice @@ -15,8 +19,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return add_entities( - [FibaroCover(device) for - device in hass.data[FIBARO_DEVICES]['cover']], True) + [FibaroCover(device) for device in hass.data[FIBARO_DEVICES]["cover"]], True + ) class FibaroCover(FibaroDevice, CoverDevice): diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index a741de972f0..ba77942a448 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -4,13 +4,19 @@ from functools import partial import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, + ENTITY_ID_FORMAT, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, + Light, +) from homeassistant.const import CONF_WHITE_VALUE import homeassistant.util.color as color_util -from . import ( - CONF_COLOR, CONF_DIMMING, CONF_RESET_COLOR, FIBARO_DEVICES, FibaroDevice) +from . import CONF_COLOR, CONF_DIMMING, CONF_RESET_COLOR, FIBARO_DEVICES, FibaroDevice _LOGGER = logging.getLogger(__name__) @@ -32,17 +38,14 @@ def scaleto100(value): return max(0, min(100, ((value * 100.0) / 255.0))) -async def async_setup_platform(hass, - config, - async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Perform the setup for Fibaro controller devices.""" if discovery_info is None: return async_add_entities( - [FibaroLight(device) - for device in hass.data[FIBARO_DEVICES]['light']], True) + [FibaroLight(device) for device in hass.data[FIBARO_DEVICES]["light"]], True + ) class FibaroLight(FibaroDevice, Light): @@ -59,10 +62,11 @@ class FibaroLight(FibaroDevice, Light): devconf = fibaro_device.device_config self._reset_color = devconf.get(CONF_RESET_COLOR, False) - supports_color = 'color' in fibaro_device.properties and \ - 'setColor' in fibaro_device.actions - supports_dimming = 'levelChange' in fibaro_device.interfaces - supports_white_v = 'setW' in fibaro_device.actions + supports_color = ( + "color" in fibaro_device.properties and "setColor" in fibaro_device.actions + ) + supports_dimming = "levelChange" in fibaro_device.interfaces + supports_white_v = "setW" in fibaro_device.actions # Configuration can overrride default capability detection if devconf.get(CONF_DIMMING, supports_dimming): @@ -98,8 +102,7 @@ class FibaroLight(FibaroDevice, Light): async def async_turn_on(self, **kwargs): """Turn the light on.""" async with self._update_lock: - await self.hass.async_add_executor_job( - partial(self._turn_on, **kwargs)) + await self.hass.async_add_executor_job(partial(self._turn_on, **kwargs)) def _turn_on(self, **kwargs): """Really turn the light on.""" @@ -119,10 +122,12 @@ class FibaroLight(FibaroDevice, Light): self._brightness = scaleto100(target_brightness) if self._supported_flags & SUPPORT_COLOR: - if self._reset_color and \ - kwargs.get(ATTR_WHITE_VALUE) is None and \ - kwargs.get(ATTR_HS_COLOR) is None and \ - kwargs.get(ATTR_BRIGHTNESS) is None: + if ( + self._reset_color + and kwargs.get(ATTR_WHITE_VALUE) is None + and kwargs.get(ATTR_HS_COLOR) is None + and kwargs.get(ATTR_BRIGHTNESS) is None + ): self._color = (100, 0) # Update based on parameters @@ -133,9 +138,10 @@ class FibaroLight(FibaroDevice, Light): round(rgb[0] * self._brightness / 100.0), round(rgb[1] * self._brightness / 100.0), round(rgb[2] * self._brightness / 100.0), - round(self._white * self._brightness / 100.0)) + round(self._white * self._brightness / 100.0), + ) - if self.state == 'off': + if self.state == "off": self.set_level(int(self._brightness)) return @@ -153,14 +159,16 @@ class FibaroLight(FibaroDevice, Light): async def async_turn_off(self, **kwargs): """Turn the light off.""" async with self._update_lock: - await self.hass.async_add_executor_job( - partial(self._turn_off, **kwargs)) + await self.hass.async_add_executor_job(partial(self._turn_off, **kwargs)) def _turn_off(self, **kwargs): """Really turn the light off.""" # Let's save the last brightness level before we switch it off - if (self._supported_flags & SUPPORT_BRIGHTNESS) and \ - self._brightness and self._brightness > 0: + if ( + (self._supported_flags & SUPPORT_BRIGHTNESS) + and self._brightness + and self._brightness > 0 + ): self._last_brightness = self._brightness self._brightness = 0 self.call_turn_off() @@ -185,18 +193,17 @@ class FibaroLight(FibaroDevice, Light): if self._brightness > 99: self._brightness = 100 # Color handling - if self._supported_flags & SUPPORT_COLOR and \ - 'color' in self.fibaro_device.properties and \ - ',' in self.fibaro_device.properties.color: + if ( + self._supported_flags & SUPPORT_COLOR + and "color" in self.fibaro_device.properties + and "," in self.fibaro_device.properties.color + ): # Fibaro communicates the color as an 'R, G, B, W' string rgbw_s = self.fibaro_device.properties.color - if rgbw_s == '0,0,0,0' and\ - 'lastColorSet' in self.fibaro_device.properties: + if rgbw_s == "0,0,0,0" and "lastColorSet" in self.fibaro_device.properties: rgbw_s = self.fibaro_device.properties.lastColorSet rgbw_list = [int(i) for i in rgbw_s.split(",")][:4] if rgbw_list[0] or rgbw_list[1] or rgbw_list[2]: self._color = color_util.color_RGB_to_hs(*rgbw_list[:3]) - if (self._supported_flags & SUPPORT_WHITE_VALUE) and \ - self.brightness != 0: - self._white = min(255, max(0, rgbw_list[3]*100.0 / - self._brightness)) + if (self._supported_flags & SUPPORT_WHITE_VALUE) and self.brightness != 0: + self._white = min(255, max(0, rgbw_list[3] * 100.0 / self._brightness)) diff --git a/homeassistant/components/fibaro/scene.py b/homeassistant/components/fibaro/scene.py index f9f96844319..06d11bc1f5c 100644 --- a/homeassistant/components/fibaro/scene.py +++ b/homeassistant/components/fibaro/scene.py @@ -8,15 +8,14 @@ from . import FIBARO_DEVICES, FibaroDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Perform the setup for Fibaro scenes.""" if discovery_info is None: return async_add_entities( - [FibaroScene(scene) - for scene in hass.data[FIBARO_DEVICES]['scene']], True) + [FibaroScene(scene) for scene in hass.data[FIBARO_DEVICES]["scene"]], True + ) class FibaroScene(FibaroDevice, Scene): diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index db9d103d87e..1e0bae212f8 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -3,23 +3,27 @@ import logging from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.helpers.entity import Entity from . import FIBARO_DEVICES, FibaroDevice SENSOR_TYPES = { - 'com.fibaro.temperatureSensor': - ['Temperature', None, None, DEVICE_CLASS_TEMPERATURE], - 'com.fibaro.smokeSensor': - ['Smoke', 'ppm', 'mdi:fire', None], - 'CO2': - ['CO2', 'ppm', 'mdi:cloud', None], - 'com.fibaro.humiditySensor': - ['Humidity', '%', None, DEVICE_CLASS_HUMIDITY], - 'com.fibaro.lightSensor': - ['Light', 'lx', None, DEVICE_CLASS_ILLUMINANCE] + "com.fibaro.temperatureSensor": [ + "Temperature", + None, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "com.fibaro.smokeSensor": ["Smoke", "ppm", "mdi:fire", None], + "CO2": ["CO2", "ppm", "mdi:cloud", None], + "com.fibaro.humiditySensor": ["Humidity", "%", None, DEVICE_CLASS_HUMIDITY], + "com.fibaro.lightSensor": ["Light", "lx", None, DEVICE_CLASS_ILLUMINANCE], } _LOGGER = logging.getLogger(__name__) @@ -31,8 +35,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return add_entities( - [FibaroSensor(device) - for device in hass.data[FIBARO_DEVICES]['sensor']], True) + [FibaroSensor(device) for device in hass.data[FIBARO_DEVICES]["sensor"]], True + ) class FibaroSensor(FibaroDevice, Entity): @@ -54,11 +58,11 @@ class FibaroSensor(FibaroDevice, Entity): self._device_class = None try: if not self._unit: - if self.fibaro_device.properties.unit == 'lux': - self._unit = 'lx' - elif self.fibaro_device.properties.unit == 'C': + if self.fibaro_device.properties.unit == "lux": + self._unit = "lx" + elif self.fibaro_device.properties.unit == "C": self._unit = TEMP_CELSIUS - elif self.fibaro_device.properties.unit == 'F': + elif self.fibaro_device.properties.unit == "F": self._unit = TEMP_FAHRENHEIT else: self._unit = self.fibaro_device.properties.unit diff --git a/homeassistant/components/fibaro/switch.py b/homeassistant/components/fibaro/switch.py index f134b424484..4bb8c34d579 100644 --- a/homeassistant/components/fibaro/switch.py +++ b/homeassistant/components/fibaro/switch.py @@ -15,8 +15,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return add_entities( - [FibaroSwitch(device) for - device in hass.data[FIBARO_DEVICES]['switch']], True) + [FibaroSwitch(device) for device in hass.data[FIBARO_DEVICES]["switch"]], True + ) class FibaroSwitch(FibaroDevice, SwitchDevice): @@ -41,14 +41,14 @@ class FibaroSwitch(FibaroDevice, SwitchDevice): @property def current_power_w(self): """Return the current power usage in W.""" - if 'power' in self.fibaro_device.interfaces: + if "power" in self.fibaro_device.interfaces: return convert(self.fibaro_device.properties.power, float, 0.0) return None @property def today_energy_kwh(self): """Return the today total energy usage in kWh.""" - if 'energy' in self.fibaro_device.interfaces: + if "energy" in self.fibaro_device.interfaces: return convert(self.fibaro_device.properties.energy, float, 0.0) return None diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index ea66acaf808..e556903638c 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -14,61 +14,63 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, - CONF_NAME, CONF_MONITORED_VARIABLES) + CONF_USERNAME, + CONF_PASSWORD, + CONF_NAME, + CONF_MONITORED_VARIABLES, +) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -KILOBITS = 'Kb' # type: str -PRICE = 'CAD' # type: str -MESSAGES = 'messages' # type: str -MINUTES = 'minutes' # type: str +KILOBITS = "Kb" # type: str +PRICE = "CAD" # type: str +MESSAGES = "messages" # type: str +MINUTES = "minutes" # type: str -DEFAULT_NAME = 'Fido' +DEFAULT_NAME = "Fido" REQUESTS_TIMEOUT = 15 MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { - 'fido_dollar': ['Fido dollar', PRICE, 'mdi:square-inc-cash'], - 'balance': ['Balance', PRICE, 'mdi:square-inc-cash'], - 'data_used': ['Data used', KILOBITS, 'mdi:download'], - 'data_limit': ['Data limit', KILOBITS, 'mdi:download'], - 'data_remaining': ['Data remaining', KILOBITS, 'mdi:download'], - 'text_used': ['Text used', MESSAGES, 'mdi:message-text'], - 'text_limit': ['Text limit', MESSAGES, 'mdi:message-text'], - 'text_remaining': ['Text remaining', MESSAGES, 'mdi:message-text'], - 'mms_used': ['MMS used', MESSAGES, 'mdi:message-image'], - 'mms_limit': ['MMS limit', MESSAGES, 'mdi:message-image'], - 'mms_remaining': ['MMS remaining', MESSAGES, 'mdi:message-image'], - 'text_int_used': ['International text used', - MESSAGES, 'mdi:message-alert'], - 'text_int_limit': ['International text limit', - MESSAGES, 'mdi:message-alert'], - 'text_int_remaining': ['International remaining', - MESSAGES, 'mdi:message-alert'], - 'talk_used': ['Talk used', MINUTES, 'mdi:cellphone'], - 'talk_limit': ['Talk limit', MINUTES, 'mdi:cellphone'], - 'talk_remaining': ['Talk remaining', MINUTES, 'mdi:cellphone'], - 'other_talk_used': ['Other Talk used', MINUTES, 'mdi:cellphone'], - 'other_talk_limit': ['Other Talk limit', MINUTES, 'mdi:cellphone'], - 'other_talk_remaining': ['Other Talk remaining', MINUTES, 'mdi:cellphone'], + "fido_dollar": ["Fido dollar", PRICE, "mdi:square-inc-cash"], + "balance": ["Balance", PRICE, "mdi:square-inc-cash"], + "data_used": ["Data used", KILOBITS, "mdi:download"], + "data_limit": ["Data limit", KILOBITS, "mdi:download"], + "data_remaining": ["Data remaining", KILOBITS, "mdi:download"], + "text_used": ["Text used", MESSAGES, "mdi:message-text"], + "text_limit": ["Text limit", MESSAGES, "mdi:message-text"], + "text_remaining": ["Text remaining", MESSAGES, "mdi:message-text"], + "mms_used": ["MMS used", MESSAGES, "mdi:message-image"], + "mms_limit": ["MMS limit", MESSAGES, "mdi:message-image"], + "mms_remaining": ["MMS remaining", MESSAGES, "mdi:message-image"], + "text_int_used": ["International text used", MESSAGES, "mdi:message-alert"], + "text_int_limit": ["International text limit", MESSAGES, "mdi:message-alert"], + "text_int_remaining": ["International remaining", MESSAGES, "mdi:message-alert"], + "talk_used": ["Talk used", MINUTES, "mdi:cellphone"], + "talk_limit": ["Talk limit", MINUTES, "mdi:cellphone"], + "talk_remaining": ["Talk remaining", MINUTES, "mdi:cellphone"], + "other_talk_used": ["Other Talk used", MINUTES, "mdi:cellphone"], + "other_talk_limit": ["Other Talk limit", MINUTES, "mdi:cellphone"], + "other_talk_remaining": ["Other Talk remaining", MINUTES, "mdi:cellphone"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_VARIABLES): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MONITORED_VARIABLES): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Fido sensor.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -106,7 +108,7 @@ class FidoSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{} {} {}'.format(self.client_name, self._number, self._name) + return "{} {} {}".format(self.client_name, self._number, self._name) @property def state(self): @@ -126,19 +128,16 @@ class FidoSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - return { - 'number': self._number, - } + return {"number": self._number} async def async_update(self): """Get the latest data from Fido and update the state.""" await self.fido_data.async_update() - if self.type == 'balance': + if self.type == "balance": if self.fido_data.data.get(self.type) is not None: self._state = round(self.fido_data.data[self.type], 2) else: - if self.fido_data.data.get(self._number, {}).get(self.type) \ - is not None: + if self.fido_data.data.get(self._number, {}).get(self.type) is not None: self._state = self.fido_data.data[self._number][self.type] self._state = round(self._state, 2) @@ -149,14 +148,15 @@ class FidoData: def __init__(self, username, password, httpsession): """Initialize the data object.""" from pyfido import FidoClient - self.client = FidoClient(username, password, - REQUESTS_TIMEOUT, httpsession) + + self.client = FidoClient(username, password, REQUESTS_TIMEOUT, httpsession) self.data = {} @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from Fido.""" from pyfido.client import PyFidoError + try: await self.client.fetch_data() except PyFidoError as exp: diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index 07718dcf36c..f4d31a5fd6f 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -9,14 +9,20 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) -CONF_TIMESTAMP = 'timestamp' +CONF_TIMESTAMP = "timestamp" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_FILENAME): cv.string, - vol.Optional(CONF_TIMESTAMP, default=False): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_FILENAME): cv.string, + vol.Optional(CONF_TIMESTAMP, default=False): cv.boolean, + } +) _LOGGER = logging.getLogger(__name__) @@ -39,16 +45,17 @@ class FileNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a file.""" - with open(self.filepath, 'a') as file: + with open(self.filepath, "a") as file: if os.stat(self.filepath).st_size == 0: - title = '{} notifications (Log started: {})\n{}\n'.format( + title = "{} notifications (Log started: {})\n{}\n".format( kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), dt_util.utcnow().isoformat(), - '-' * 80) + "-" * 80, + ) file.write(title) if self.add_timestamp: - text = '{} {}\n'.format(dt_util.utcnow().isoformat(), message) + text = "{} {}\n".format(dt_util.utcnow().isoformat(), message) else: - text = '{}\n'.format(message) + text = "{}\n".format(message) file.write(text) diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index a618c1e56dc..60f04b18f24 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -6,28 +6,28 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_VALUE_TEMPLATE, CONF_NAME, CONF_UNIT_OF_MEASUREMENT) +from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_FILE_PATH = 'file_path' +CONF_FILE_PATH = "file_path" -DEFAULT_NAME = 'File' +DEFAULT_NAME = "File" -ICON = 'mdi:file' +ICON = "mdi:file" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_FILE_PATH): cv.isfile, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_FILE_PATH): cv.isfile, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the file sensor.""" file_path = config.get(CONF_FILE_PATH) name = config.get(CONF_NAME) @@ -38,8 +38,7 @@ async def async_setup_platform(hass, config, async_add_entities, value_template.hass = hass if hass.config.is_allowed_path(file_path): - async_add_entities( - [FileSensor(name, file_path, unit, value_template)], True) + async_add_entities([FileSensor(name, file_path, unit, value_template)], True) else: _LOGGER.error("'%s' is not a whitelisted directory", file_path) @@ -78,18 +77,20 @@ class FileSensor(Entity): def update(self): """Get the latest entry from a file and updates the state.""" try: - with open(self._file_path, 'r', encoding='utf-8') as file_data: + with open(self._file_path, "r", encoding="utf-8") as file_data: for line in file_data: data = line data = data.strip() - except (IndexError, FileNotFoundError, IsADirectoryError, - UnboundLocalError): - _LOGGER.warning("File or data not present at the moment: %s", - os.path.basename(self._file_path)) + except (IndexError, FileNotFoundError, IsADirectoryError, UnboundLocalError): + _LOGGER.warning( + "File or data not present at the moment: %s", + os.path.basename(self._file_path), + ) return if self._val_tpl is not None: self._state = self._val_tpl.async_render_with_possible_json_value( - data, None) + data, None + ) else: self._state = data diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 3e1394c72d6..a4b9bc5cd76 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -12,13 +12,12 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) -CONF_FILE_PATHS = 'file_paths' -ICON = 'mdi:file' +CONF_FILE_PATHS = "file_paths" +ICON = "mdi:file" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_FILE_PATHS): - vol.All(cv.ensure_list, [cv.isfile]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_FILE_PATHS): vol.All(cv.ensure_list, [cv.isfile])} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -26,8 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for path in config.get(CONF_FILE_PATHS): if not hass.config.is_allowed_path(path): - _LOGGER.error( - "Filepath %s is not valid or allowed", path) + _LOGGER.error("Filepath %s is not valid or allowed", path) continue else: sensors.append(Filesize(path)) @@ -41,11 +39,11 @@ class Filesize(Entity): def __init__(self, path): """Initialize the data object.""" - self._path = path # Need to check its a valid path + self._path = path # Need to check its a valid path self._size = None self._last_updated = None self._name = path.split("/")[-1] - self._unit_of_measurement = 'MB' + self._unit_of_measurement = "MB" def update(self): """Update the sensor.""" @@ -63,7 +61,7 @@ class Filesize(Entity): def state(self): """Return the size of the file in MB.""" decimals = 2 - state_mb = round(self._size/1e6, decimals) + state_mb = round(self._size / 1e6, decimals) return state_mb @property @@ -75,10 +73,10 @@ class Filesize(Entity): def device_state_attributes(self): """Return other details about the sensor state.""" attr = { - 'path': self._path, - 'last_updated': self._last_updated, - 'bytes': self._size, - } + "path": self._path, + "last_updated": self._last_updated, + "bytes": self._size, + } return attr @property diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 734caa31270..b1ce967d6cd 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -12,8 +12,14 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, ATTR_ENTITY_ID, - ATTR_ICON, STATE_UNKNOWN, STATE_UNAVAILABLE) + CONF_NAME, + CONF_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + ATTR_ENTITY_ID, + ATTR_ICON, + STATE_UNKNOWN, + STATE_UNAVAILABLE, +) import homeassistant.helpers.config_validation as cv from homeassistant.util.decorator import Registry from homeassistant.helpers.entity import Entity @@ -23,25 +29,25 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -FILTER_NAME_RANGE = 'range' -FILTER_NAME_LOWPASS = 'lowpass' -FILTER_NAME_OUTLIER = 'outlier' -FILTER_NAME_THROTTLE = 'throttle' -FILTER_NAME_TIME_THROTTLE = 'time_throttle' -FILTER_NAME_TIME_SMA = 'time_simple_moving_average' +FILTER_NAME_RANGE = "range" +FILTER_NAME_LOWPASS = "lowpass" +FILTER_NAME_OUTLIER = "outlier" +FILTER_NAME_THROTTLE = "throttle" +FILTER_NAME_TIME_THROTTLE = "time_throttle" +FILTER_NAME_TIME_SMA = "time_simple_moving_average" FILTERS = Registry() -CONF_FILTERS = 'filters' -CONF_FILTER_NAME = 'filter' -CONF_FILTER_WINDOW_SIZE = 'window_size' -CONF_FILTER_PRECISION = 'precision' -CONF_FILTER_RADIUS = 'radius' -CONF_FILTER_TIME_CONSTANT = 'time_constant' -CONF_FILTER_LOWER_BOUND = 'lower_bound' -CONF_FILTER_UPPER_BOUND = 'upper_bound' -CONF_TIME_SMA_TYPE = 'type' +CONF_FILTERS = "filters" +CONF_FILTER_NAME = "filter" +CONF_FILTER_WINDOW_SIZE = "window_size" +CONF_FILTER_PRECISION = "precision" +CONF_FILTER_RADIUS = "radius" +CONF_FILTER_TIME_CONSTANT = "time_constant" +CONF_FILTER_LOWER_BOUND = "lower_bound" +CONF_FILTER_UPPER_BOUND = "upper_bound" +CONF_TIME_SMA_TYPE = "type" -TIME_SMA_LAST = 'last' +TIME_SMA_LAST = "last" WINDOW_SIZE_UNIT_NUMBER_EVENTS = 1 WINDOW_SIZE_UNIT_TIME = 2 @@ -52,79 +58,104 @@ DEFAULT_FILTER_RADIUS = 2.0 DEFAULT_FILTER_TIME_CONSTANT = 10 NAME_TEMPLATE = "{} filter" -ICON = 'mdi:chart-line-variant' +ICON = "mdi:chart-line-variant" -FILTER_SCHEMA = vol.Schema({ - vol.Optional(CONF_FILTER_PRECISION, - default=DEFAULT_PRECISION): vol.Coerce(int), -}) +FILTER_SCHEMA = vol.Schema( + {vol.Optional(CONF_FILTER_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int)} +) -FILTER_OUTLIER_SCHEMA = FILTER_SCHEMA.extend({ - vol.Required(CONF_FILTER_NAME): FILTER_NAME_OUTLIER, - vol.Optional(CONF_FILTER_WINDOW_SIZE, - default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), - vol.Optional(CONF_FILTER_RADIUS, - default=DEFAULT_FILTER_RADIUS): vol.Coerce(float), -}) +FILTER_OUTLIER_SCHEMA = FILTER_SCHEMA.extend( + { + vol.Required(CONF_FILTER_NAME): FILTER_NAME_OUTLIER, + vol.Optional(CONF_FILTER_WINDOW_SIZE, default=DEFAULT_WINDOW_SIZE): vol.Coerce( + int + ), + vol.Optional(CONF_FILTER_RADIUS, default=DEFAULT_FILTER_RADIUS): vol.Coerce( + float + ), + } +) -FILTER_LOWPASS_SCHEMA = FILTER_SCHEMA.extend({ - vol.Required(CONF_FILTER_NAME): FILTER_NAME_LOWPASS, - vol.Optional(CONF_FILTER_WINDOW_SIZE, - default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), - vol.Optional(CONF_FILTER_TIME_CONSTANT, - default=DEFAULT_FILTER_TIME_CONSTANT): vol.Coerce(int), -}) +FILTER_LOWPASS_SCHEMA = FILTER_SCHEMA.extend( + { + vol.Required(CONF_FILTER_NAME): FILTER_NAME_LOWPASS, + vol.Optional(CONF_FILTER_WINDOW_SIZE, default=DEFAULT_WINDOW_SIZE): vol.Coerce( + int + ), + vol.Optional( + CONF_FILTER_TIME_CONSTANT, default=DEFAULT_FILTER_TIME_CONSTANT + ): vol.Coerce(int), + } +) -FILTER_RANGE_SCHEMA = vol.Schema({ - vol.Required(CONF_FILTER_NAME): FILTER_NAME_RANGE, - vol.Optional(CONF_FILTER_LOWER_BOUND): vol.Coerce(float), - vol.Optional(CONF_FILTER_UPPER_BOUND): vol.Coerce(float), -}) +FILTER_RANGE_SCHEMA = vol.Schema( + { + vol.Required(CONF_FILTER_NAME): FILTER_NAME_RANGE, + vol.Optional(CONF_FILTER_LOWER_BOUND): vol.Coerce(float), + vol.Optional(CONF_FILTER_UPPER_BOUND): vol.Coerce(float), + } +) -FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ - vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, - vol.Optional(CONF_TIME_SMA_TYPE, - default=TIME_SMA_LAST): vol.In( - [TIME_SMA_LAST]), +FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend( + { + vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, + vol.Optional(CONF_TIME_SMA_TYPE, default=TIME_SMA_LAST): vol.In( + [TIME_SMA_LAST] + ), + vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All( + cv.time_period, cv.positive_timedelta + ), + } +) - vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, - cv.positive_timedelta) -}) +FILTER_THROTTLE_SCHEMA = FILTER_SCHEMA.extend( + { + vol.Required(CONF_FILTER_NAME): FILTER_NAME_THROTTLE, + vol.Optional(CONF_FILTER_WINDOW_SIZE, default=DEFAULT_WINDOW_SIZE): vol.Coerce( + int + ), + } +) -FILTER_THROTTLE_SCHEMA = FILTER_SCHEMA.extend({ - vol.Required(CONF_FILTER_NAME): FILTER_NAME_THROTTLE, - vol.Optional(CONF_FILTER_WINDOW_SIZE, - default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), -}) +FILTER_TIME_THROTTLE_SCHEMA = FILTER_SCHEMA.extend( + { + vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_THROTTLE, + vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All( + cv.time_period, cv.positive_timedelta + ), + } +) -FILTER_TIME_THROTTLE_SCHEMA = FILTER_SCHEMA.extend({ - vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_THROTTLE, - vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, - cv.positive_timedelta) -}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_FILTERS): vol.All(cv.ensure_list, - [vol.Any(FILTER_OUTLIER_SCHEMA, - FILTER_LOWPASS_SCHEMA, - FILTER_TIME_SMA_SCHEMA, - FILTER_THROTTLE_SCHEMA, - FILTER_TIME_THROTTLE_SCHEMA, - FILTER_RANGE_SCHEMA)]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_FILTERS): vol.All( + cv.ensure_list, + [ + vol.Any( + FILTER_OUTLIER_SCHEMA, + FILTER_LOWPASS_SCHEMA, + FILTER_TIME_SMA_SCHEMA, + FILTER_THROTTLE_SCHEMA, + FILTER_TIME_THROTTLE_SCHEMA, + FILTER_RANGE_SCHEMA, + ) + ], + ), + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template sensors.""" name = config.get(CONF_NAME) entity_id = config.get(CONF_ENTITY_ID) - filters = [FILTERS[_filter.pop(CONF_FILTER_NAME)]( - entity=entity_id, **_filter) - for _filter in config[CONF_FILTERS]] + filters = [ + FILTERS[_filter.pop(CONF_FILTER_NAME)](entity=entity_id, **_filter) + for _filter in config[CONF_FILTERS] + ] async_add_entities([SensorFilter(name, entity_id, filters)]) @@ -143,9 +174,9 @@ class SensorFilter(Entity): async def async_added_to_hass(self): """Register callbacks.""" + @callback - def filter_sensor_state_listener(entity, old_state, new_state, - update_ha=True): + def filter_sensor_state_listener(entity, old_state, new_state, update_ha=True): """Handle device state changes.""" if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: return @@ -155,76 +186,94 @@ class SensorFilter(Entity): try: for filt in self._filters: filtered_state = filt.filter_state(copy(temp_state)) - _LOGGER.debug("%s(%s=%s) -> %s", filt.name, - self._entity, - temp_state.state, - "skip" if filt.skip_processing else - filtered_state.state) + _LOGGER.debug( + "%s(%s=%s) -> %s", + filt.name, + self._entity, + temp_state.state, + "skip" if filt.skip_processing else filtered_state.state, + ) if filt.skip_processing: return temp_state = filtered_state except ValueError: - _LOGGER.error("Could not convert state: %s to number", - self._state) + _LOGGER.error("Could not convert state: %s to number", self._state) return self._state = temp_state.state if self._icon is None: - self._icon = new_state.attributes.get( - ATTR_ICON, ICON) + self._icon = new_state.attributes.get(ATTR_ICON, ICON) if self._unit_of_measurement is None: self._unit_of_measurement = new_state.attributes.get( - ATTR_UNIT_OF_MEASUREMENT) + ATTR_UNIT_OF_MEASUREMENT + ) if update_ha: self.async_schedule_update_ha_state() - if 'recorder' in self.hass.config.components: + if "recorder" in self.hass.config.components: history_list = [] largest_window_items = 0 largest_window_time = timedelta(0) # Determine the largest window_size by type for filt in self._filters: - if filt.window_unit == WINDOW_SIZE_UNIT_NUMBER_EVENTS\ - and largest_window_items < filt.window_size: + if ( + filt.window_unit == WINDOW_SIZE_UNIT_NUMBER_EVENTS + and largest_window_items < filt.window_size + ): largest_window_items = filt.window_size - elif filt.window_unit == WINDOW_SIZE_UNIT_TIME\ - and largest_window_time < filt.window_size: + elif ( + filt.window_unit == WINDOW_SIZE_UNIT_TIME + and largest_window_time < filt.window_size + ): largest_window_time = filt.window_size # Retrieve the largest window_size of each type if largest_window_items > 0: - filter_history = await self.hass.async_add_job(partial( - history.get_last_state_changes, self.hass, - largest_window_items, entity_id=self._entity)) - history_list.extend( - [state for state in filter_history[self._entity]]) + filter_history = await self.hass.async_add_job( + partial( + history.get_last_state_changes, + self.hass, + largest_window_items, + entity_id=self._entity, + ) + ) + history_list.extend([state for state in filter_history[self._entity]]) if largest_window_time > timedelta(seconds=0): start = dt_util.utcnow() - largest_window_time - filter_history = await self.hass.async_add_job(partial( - history.state_changes_during_period, self.hass, - start, entity_id=self._entity)) + filter_history = await self.hass.async_add_job( + partial( + history.state_changes_during_period, + self.hass, + start, + entity_id=self._entity, + ) + ) history_list.extend( - [state for state in filter_history[self._entity] - if state not in history_list]) + [ + state + for state in filter_history[self._entity] + if state not in history_list + ] + ) # Sort the window states history_list = sorted(history_list, key=lambda s: s.last_updated) - _LOGGER.debug("Loading from history: %s", - [(s.state, s.last_updated) for s in history_list]) + _LOGGER.debug( + "Loading from history: %s", + [(s.state, s.last_updated) for s in history_list], + ) # Replay history through the filter chain prev_state = None for state in history_list: - filter_sensor_state_listener( - self._entity, prev_state, state, False) + filter_sensor_state_listener(self._entity, prev_state, state, False) prev_state = state - async_track_state_change( - self.hass, self._entity, filter_sensor_state_listener) + async_track_state_change(self.hass, self._entity, filter_sensor_state_listener) @property def name(self): @@ -254,9 +303,7 @@ class SensorFilter(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - state_attr = { - ATTR_ENTITY_ID: self._entity - } + state_attr = {ATTR_ENTITY_ID: self._entity} return state_attr @@ -353,8 +400,7 @@ class RangeFilter(Filter): lower_bound (float): band lower bound """ - def __init__(self, entity, - lower_bound=None, upper_bound=None): + def __init__(self, entity, lower_bound=None, upper_bound=None): """Initialize Filter.""" super().__init__(FILTER_NAME_RANGE, entity=entity) self._lower_bound = lower_bound @@ -363,24 +409,28 @@ class RangeFilter(Filter): def _filter_state(self, new_state): """Implement the range filter.""" - if (self._upper_bound is not None - and new_state.state > self._upper_bound): + if self._upper_bound is not None and new_state.state > self._upper_bound: - self._stats_internal['erasures_up'] += 1 + self._stats_internal["erasures_up"] += 1 - _LOGGER.debug("Upper outlier nr. %s in %s: %s", - self._stats_internal['erasures_up'], - self._entity, new_state) + _LOGGER.debug( + "Upper outlier nr. %s in %s: %s", + self._stats_internal["erasures_up"], + self._entity, + new_state, + ) new_state.state = self._upper_bound - elif (self._lower_bound is not None - and new_state.state < self._lower_bound): + elif self._lower_bound is not None and new_state.state < self._lower_bound: - self._stats_internal['erasures_low'] += 1 + self._stats_internal["erasures_low"] += 1 - _LOGGER.debug("Lower outlier nr. %s in %s: %s", - self._stats_internal['erasures_low'], - self._entity, new_state) + _LOGGER.debug( + "Lower outlier nr. %s in %s: %s", + self._stats_internal["erasures_low"], + self._entity, + new_state, + ) new_state.state = self._lower_bound return new_state @@ -405,17 +455,20 @@ class OutlierFilter(Filter): def _filter_state(self, new_state): """Implement the outlier filter.""" - median = statistics.median([s.state for s in self.states]) \ - if self.states else 0 - if (len(self.states) == self.states.maxlen and - abs(new_state.state - median) > - self._radius): + median = statistics.median([s.state for s in self.states]) if self.states else 0 + if ( + len(self.states) == self.states.maxlen + and abs(new_state.state - median) > self._radius + ): - self._stats_internal['erasures'] += 1 + self._stats_internal["erasures"] += 1 - _LOGGER.debug("Outlier nr. %s in %s: %s", - self._stats_internal['erasures'], - self._entity, new_state) + _LOGGER.debug( + "Outlier nr. %s in %s: %s", + self._stats_internal["erasures"], + self._entity, + new_state, + ) new_state.state = median return new_state @@ -440,8 +493,9 @@ class LowPassFilter(Filter): new_weight = 1.0 / self._time_constant prev_weight = 1.0 - new_weight - new_state.state = prev_weight * self.states[-1].state +\ - new_weight * new_state.state + new_state.state = ( + prev_weight * self.states[-1].state + new_weight * new_state.state + ) return new_state @@ -456,8 +510,9 @@ class TimeSMAFilter(Filter): type (enum): type of algorithm used to connect discrete values """ - def __init__(self, window_size, precision, entity, - type): # pylint: disable=redefined-builtin + def __init__( + self, window_size, precision, entity, type + ): # pylint: disable=redefined-builtin """Initialize Filter.""" super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity) self._time_window = window_size @@ -481,8 +536,7 @@ class TimeSMAFilter(Filter): start = new_state.timestamp - self._time_window prev_state = self.last_leak or self.queue[0] for state in self.queue: - moving_sum += (state.timestamp-start).total_seconds()\ - * prev_state.state + moving_sum += (state.timestamp - start).total_seconds() * prev_state.state start = state.timestamp prev_state = state @@ -522,8 +576,7 @@ class TimeThrottleFilter(Filter): def __init__(self, window_size, precision, entity): """Initialize Filter.""" - super().__init__(FILTER_NAME_TIME_THROTTLE, - window_size, precision, entity) + super().__init__(FILTER_NAME_TIME_THROTTLE, window_size, precision, entity) self._time_window = window_size self._last_emitted_at = None diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index cb993ada8da..7a1760ea3d5 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -14,33 +14,37 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(hours=4) -ICON = 'mdi:currency-eur' +ICON = "mdi:currency-eur" -BankCredentials = namedtuple('BankCredentials', 'blz login pin url') +BankCredentials = namedtuple("BankCredentials", "blz login pin url") -CONF_BIN = 'bank_identification_number' -CONF_ACCOUNTS = 'accounts' -CONF_HOLDINGS = 'holdings' -CONF_ACCOUNT = 'account' +CONF_BIN = "bank_identification_number" +CONF_ACCOUNTS = "accounts" +CONF_HOLDINGS = "holdings" +CONF_ACCOUNT = "account" ATTR_ACCOUNT = CONF_ACCOUNT -ATTR_BANK = 'bank' -ATTR_ACCOUNT_TYPE = 'account_type' +ATTR_BANK = "bank" +ATTR_ACCOUNT_TYPE = "account_type" -SCHEMA_ACCOUNTS = vol.Schema({ - vol.Required(CONF_ACCOUNT): cv.string, - vol.Optional(CONF_NAME, default=None): vol.Any(None, cv.string), -}) +SCHEMA_ACCOUNTS = vol.Schema( + { + vol.Required(CONF_ACCOUNT): cv.string, + vol.Optional(CONF_NAME, default=None): vol.Any(None, cv.string), + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_BIN): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PIN): cv.string, - vol.Required(CONF_URL): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ACCOUNTS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS), - vol.Optional(CONF_HOLDINGS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_BIN): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PIN): cv.string, + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ACCOUNTS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS), + vol.Optional(CONF_HOLDINGS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -49,15 +53,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): Login to the bank and get a list of existing accounts. Create a sensor for each account. """ - credentials = BankCredentials(config[CONF_BIN], config[CONF_USERNAME], - config[CONF_PIN], config[CONF_URL]) + credentials = BankCredentials( + config[CONF_BIN], config[CONF_USERNAME], config[CONF_PIN], config[CONF_URL] + ) fints_name = config.get(CONF_NAME, config[CONF_BIN]) - account_config = {acc[CONF_ACCOUNT]: acc[CONF_NAME] - for acc in config[CONF_ACCOUNTS]} + account_config = { + acc[CONF_ACCOUNT]: acc[CONF_NAME] for acc in config[CONF_ACCOUNTS] + } - holdings_config = {acc[CONF_ACCOUNT]: acc[CONF_NAME] - for acc in config[CONF_HOLDINGS]} + holdings_config = { + acc[CONF_ACCOUNT]: acc[CONF_NAME] for acc in config[CONF_HOLDINGS] + } client = FinTsClient(credentials, fints_name) balance_accounts, holdings_accounts = client.detect_accounts() @@ -65,31 +72,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for account in balance_accounts: if config[CONF_ACCOUNTS] and account.iban not in account_config: - _LOGGER.info('skipping account %s for bank %s', - account.iban, fints_name) + _LOGGER.info("skipping account %s for bank %s", account.iban, fints_name) continue account_name = account_config.get(account.iban) if not account_name: - account_name = '{} - {}'.format(fints_name, account.iban) + account_name = "{} - {}".format(fints_name, account.iban) accounts.append(FinTsAccount(client, account, account_name)) - _LOGGER.debug('Creating account %s for bank %s', - account.iban, fints_name) + _LOGGER.debug("Creating account %s for bank %s", account.iban, fints_name) for account in holdings_accounts: - if config[CONF_HOLDINGS] and \ - account.accountnumber not in holdings_config: - _LOGGER.info('skipping holdings %s for bank %s', - account.accountnumber, fints_name) + if config[CONF_HOLDINGS] and account.accountnumber not in holdings_config: + _LOGGER.info( + "skipping holdings %s for bank %s", account.accountnumber, fints_name + ) continue account_name = holdings_config.get(account.accountnumber) if not account_name: - account_name = '{} - {}'.format( - fints_name, account.accountnumber) + account_name = "{} - {}".format(fints_name, account.accountnumber) accounts.append(FinTsHoldingsAccount(client, account, account_name)) - _LOGGER.debug('Creating holdings %s for bank %s', - account.accountnumber, fints_name) + _LOGGER.debug( + "Creating holdings %s for bank %s", account.accountnumber, fints_name + ) add_entities(accounts, True) @@ -114,13 +119,18 @@ class FinTsClient: object and also think about potential concurrency problems. """ from fints.client import FinTS3PinTanClient + return FinTS3PinTanClient( - self._credentials.blz, self._credentials.login, - self._credentials.pin, self._credentials.url) + self._credentials.blz, + self._credentials.login, + self._credentials.pin, + self._credentials.url, + ) def detect_accounts(self): """Identify the accounts of the bank.""" from fints.dialog import FinTSDialogError + balance_accounts = [] holdings_accounts = [] for account in self.client.get_sepa_accounts(): @@ -155,7 +165,7 @@ class FinTsAccount(Entity): self._client = client # type: FinTsClient self._account = account self._name = name # type: str - self._balance = None # type: float + self._balance = None # type: float self._currency = None # type: str @property @@ -172,7 +182,7 @@ class FinTsAccount(Entity): balance = bank.get_balance(self._account) self._balance = balance.amount.amount self._currency = balance.amount.currency - _LOGGER.debug('updated balance of account %s', self.name) + _LOGGER.debug("updated balance of account %s", self.name) @property def name(self) -> str: @@ -192,10 +202,7 @@ class FinTsAccount(Entity): @property def device_state_attributes(self) -> dict: """Additional attributes of the sensor.""" - attributes = { - ATTR_ACCOUNT: self._account.iban, - ATTR_ACCOUNT_TYPE: 'balance', - } + attributes = {ATTR_ACCOUNT: self._account.iban, ATTR_ACCOUNT_TYPE: "balance"} if self._client.name: attributes[ATTR_BANK] = self._client.name return attributes @@ -253,16 +260,16 @@ class FinTsHoldingsAccount(Entity): """ attributes = { ATTR_ACCOUNT: self._account.accountnumber, - ATTR_ACCOUNT_TYPE: 'holdings', + ATTR_ACCOUNT_TYPE: "holdings", } if self._client.name: attributes[ATTR_BANK] = self._client.name for holding in self._holdings: - total_name = '{} total'.format(holding.name) + total_name = "{} total".format(holding.name) attributes[total_name] = holding.total_value - pieces_name = '{} pieces'.format(holding.name) + pieces_name = "{} pieces".format(holding.name) attributes[pieces_name] = holding.pieces - price_name = '{} price'.format(holding.name) + price_name = "{} price".format(holding.name) attributes[price_name] = holding.market_value return attributes diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 889920239ed..830914ce113 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -20,130 +20,134 @@ from homeassistant.util.json import load_json, save_json _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -ATTR_ACCESS_TOKEN = 'access_token' -ATTR_REFRESH_TOKEN = 'refresh_token' -ATTR_CLIENT_ID = 'client_id' -ATTR_CLIENT_SECRET = 'client_secret' -ATTR_LAST_SAVED_AT = 'last_saved_at' +ATTR_ACCESS_TOKEN = "access_token" +ATTR_REFRESH_TOKEN = "refresh_token" +ATTR_CLIENT_ID = "client_id" +ATTR_CLIENT_SECRET = "client_secret" +ATTR_LAST_SAVED_AT = "last_saved_at" -CONF_MONITORED_RESOURCES = 'monitored_resources' -CONF_CLOCK_FORMAT = 'clock_format' -ATTRIBUTION = 'Data provided by Fitbit.com' +CONF_MONITORED_RESOURCES = "monitored_resources" +CONF_CLOCK_FORMAT = "clock_format" +ATTRIBUTION = "Data provided by Fitbit.com" -FITBIT_AUTH_CALLBACK_PATH = '/api/fitbit/callback' -FITBIT_AUTH_START = '/api/fitbit' -FITBIT_CONFIG_FILE = 'fitbit.conf' -FITBIT_DEFAULT_RESOURCES = ['activities/steps'] +FITBIT_AUTH_CALLBACK_PATH = "/api/fitbit/callback" +FITBIT_AUTH_START = "/api/fitbit" +FITBIT_CONFIG_FILE = "fitbit.conf" +FITBIT_DEFAULT_RESOURCES = ["activities/steps"] SCAN_INTERVAL = datetime.timedelta(minutes=30) -DEFAULT_CONFIG = { - 'client_id': 'CLIENT_ID_HERE', - 'client_secret': 'CLIENT_SECRET_HERE' -} +DEFAULT_CONFIG = {"client_id": "CLIENT_ID_HERE", "client_secret": "CLIENT_SECRET_HERE"} FITBIT_RESOURCES_LIST = { - 'activities/activityCalories': ['Activity Calories', 'cal', 'fire'], - 'activities/calories': ['Calories', 'cal', 'fire'], - 'activities/caloriesBMR': ['Calories BMR', 'cal', 'fire'], - 'activities/distance': ['Distance', '', 'map-marker'], - 'activities/elevation': ['Elevation', '', 'walk'], - 'activities/floors': ['Floors', 'floors', 'walk'], - 'activities/heart': ['Resting Heart Rate', 'bpm', 'heart-pulse'], - 'activities/minutesFairlyActive': - ['Minutes Fairly Active', 'minutes', 'walk'], - 'activities/minutesLightlyActive': - ['Minutes Lightly Active', 'minutes', 'walk'], - 'activities/minutesSedentary': - ['Minutes Sedentary', 'minutes', 'seat-recline-normal'], - 'activities/minutesVeryActive': ['Minutes Very Active', 'minutes', 'run'], - 'activities/steps': ['Steps', 'steps', 'walk'], - 'activities/tracker/activityCalories': - ['Tracker Activity Calories', 'cal', 'fire'], - 'activities/tracker/calories': ['Tracker Calories', 'cal', 'fire'], - 'activities/tracker/distance': ['Tracker Distance', '', 'map-marker'], - 'activities/tracker/elevation': ['Tracker Elevation', '', 'walk'], - 'activities/tracker/floors': ['Tracker Floors', 'floors', 'walk'], - 'activities/tracker/minutesFairlyActive': - ['Tracker Minutes Fairly Active', 'minutes', 'walk'], - 'activities/tracker/minutesLightlyActive': - ['Tracker Minutes Lightly Active', 'minutes', 'walk'], - 'activities/tracker/minutesSedentary': - ['Tracker Minutes Sedentary', 'minutes', 'seat-recline-normal'], - 'activities/tracker/minutesVeryActive': - ['Tracker Minutes Very Active', 'minutes', 'run'], - 'activities/tracker/steps': ['Tracker Steps', 'steps', 'walk'], - 'body/bmi': ['BMI', 'BMI', 'human'], - 'body/fat': ['Body Fat', '%', 'human'], - 'body/weight': ['Weight', '', 'human'], - 'devices/battery': ['Battery', None, None], - 'sleep/awakeningsCount': - ['Awakenings Count', 'times awaken', 'sleep'], - 'sleep/efficiency': ['Sleep Efficiency', '%', 'sleep'], - 'sleep/minutesAfterWakeup': ['Minutes After Wakeup', 'minutes', 'sleep'], - 'sleep/minutesAsleep': ['Sleep Minutes Asleep', 'minutes', 'sleep'], - 'sleep/minutesAwake': ['Sleep Minutes Awake', 'minutes', 'sleep'], - 'sleep/minutesToFallAsleep': - ['Sleep Minutes to Fall Asleep', 'minutes', 'sleep'], - 'sleep/startTime': ['Sleep Start Time', None, 'clock'], - 'sleep/timeInBed': ['Sleep Time in Bed', 'minutes', 'hotel'] + "activities/activityCalories": ["Activity Calories", "cal", "fire"], + "activities/calories": ["Calories", "cal", "fire"], + "activities/caloriesBMR": ["Calories BMR", "cal", "fire"], + "activities/distance": ["Distance", "", "map-marker"], + "activities/elevation": ["Elevation", "", "walk"], + "activities/floors": ["Floors", "floors", "walk"], + "activities/heart": ["Resting Heart Rate", "bpm", "heart-pulse"], + "activities/minutesFairlyActive": ["Minutes Fairly Active", "minutes", "walk"], + "activities/minutesLightlyActive": ["Minutes Lightly Active", "minutes", "walk"], + "activities/minutesSedentary": [ + "Minutes Sedentary", + "minutes", + "seat-recline-normal", + ], + "activities/minutesVeryActive": ["Minutes Very Active", "minutes", "run"], + "activities/steps": ["Steps", "steps", "walk"], + "activities/tracker/activityCalories": ["Tracker Activity Calories", "cal", "fire"], + "activities/tracker/calories": ["Tracker Calories", "cal", "fire"], + "activities/tracker/distance": ["Tracker Distance", "", "map-marker"], + "activities/tracker/elevation": ["Tracker Elevation", "", "walk"], + "activities/tracker/floors": ["Tracker Floors", "floors", "walk"], + "activities/tracker/minutesFairlyActive": [ + "Tracker Minutes Fairly Active", + "minutes", + "walk", + ], + "activities/tracker/minutesLightlyActive": [ + "Tracker Minutes Lightly Active", + "minutes", + "walk", + ], + "activities/tracker/minutesSedentary": [ + "Tracker Minutes Sedentary", + "minutes", + "seat-recline-normal", + ], + "activities/tracker/minutesVeryActive": [ + "Tracker Minutes Very Active", + "minutes", + "run", + ], + "activities/tracker/steps": ["Tracker Steps", "steps", "walk"], + "body/bmi": ["BMI", "BMI", "human"], + "body/fat": ["Body Fat", "%", "human"], + "body/weight": ["Weight", "", "human"], + "devices/battery": ["Battery", None, None], + "sleep/awakeningsCount": ["Awakenings Count", "times awaken", "sleep"], + "sleep/efficiency": ["Sleep Efficiency", "%", "sleep"], + "sleep/minutesAfterWakeup": ["Minutes After Wakeup", "minutes", "sleep"], + "sleep/minutesAsleep": ["Sleep Minutes Asleep", "minutes", "sleep"], + "sleep/minutesAwake": ["Sleep Minutes Awake", "minutes", "sleep"], + "sleep/minutesToFallAsleep": ["Sleep Minutes to Fall Asleep", "minutes", "sleep"], + "sleep/startTime": ["Sleep Start Time", None, "clock"], + "sleep/timeInBed": ["Sleep Time in Bed", "minutes", "hotel"], } FITBIT_MEASUREMENTS = { - 'en_US': { - 'duration': 'ms', - 'distance': 'mi', - 'elevation': 'ft', - 'height': 'in', - 'weight': 'lbs', - 'body': 'in', - 'liquids': 'fl. oz.', - 'blood glucose': 'mg/dL', - 'battery': '', + "en_US": { + "duration": "ms", + "distance": "mi", + "elevation": "ft", + "height": "in", + "weight": "lbs", + "body": "in", + "liquids": "fl. oz.", + "blood glucose": "mg/dL", + "battery": "", }, - 'en_GB': { - 'duration': 'milliseconds', - 'distance': 'kilometers', - 'elevation': 'meters', - 'height': 'centimeters', - 'weight': 'stone', - 'body': 'centimeters', - 'liquids': 'milliliters', - 'blood glucose': 'mmol/L', - 'battery': '', + "en_GB": { + "duration": "milliseconds", + "distance": "kilometers", + "elevation": "meters", + "height": "centimeters", + "weight": "stone", + "body": "centimeters", + "liquids": "milliliters", + "blood glucose": "mmol/L", + "battery": "", }, - 'metric': { - 'duration': 'milliseconds', - 'distance': 'kilometers', - 'elevation': 'meters', - 'height': 'centimeters', - 'weight': 'kilograms', - 'body': 'centimeters', - 'liquids': 'milliliters', - 'blood glucose': 'mmol/L', - 'battery': '', + "metric": { + "duration": "milliseconds", + "distance": "kilometers", + "elevation": "meters", + "height": "centimeters", + "weight": "kilograms", + "body": "centimeters", + "liquids": "milliliters", + "blood glucose": "mmol/L", + "battery": "", + }, +} + +BATTERY_LEVELS = {"High": 100, "Medium": 50, "Low": 20, "Empty": 0} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional( + CONF_MONITORED_RESOURCES, default=FITBIT_DEFAULT_RESOURCES + ): vol.All(cv.ensure_list, [vol.In(FITBIT_RESOURCES_LIST)]), + vol.Optional(CONF_CLOCK_FORMAT, default="24H"): vol.In(["12H", "24H"]), + vol.Optional(CONF_UNIT_SYSTEM, default="default"): vol.In( + ["en_GB", "en_US", "metric", "default"] + ), } -} - -BATTERY_LEVELS = { - 'High': 100, - 'Medium': 50, - 'Low': 20, - 'Empty': 0 -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MONITORED_RESOURCES, default=FITBIT_DEFAULT_RESOURCES): - vol.All(cv.ensure_list, [vol.In(FITBIT_RESOURCES_LIST)]), - vol.Optional(CONF_CLOCK_FORMAT, default='24H'): - vol.In(['12H', '24H']), - vol.Optional(CONF_UNIT_SYSTEM, default='default'): - vol.In(['en_GB', 'en_US', 'metric', 'default']) -}) +) -def request_app_setup(hass, config, add_entities, config_path, - discovery_info=None): +def request_app_setup(hass, config, add_entities, config_path, discovery_info=None): """Assist user with configuring the Fitbit dev application.""" configurator = hass.components.configurator @@ -153,17 +157,17 @@ def request_app_setup(hass, config, add_entities, config_path, if os.path.isfile(config_path): config_file = load_json(config_path) if config_file == DEFAULT_CONFIG: - error_msg = ("You didn't correctly modify fitbit.conf", - " please try again") - configurator.notify_errors(_CONFIGURING['fitbit'], - error_msg) + error_msg = ( + "You didn't correctly modify fitbit.conf", + " please try again", + ) + configurator.notify_errors(_CONFIGURING["fitbit"], error_msg) else: setup_platform(hass, config, add_entities, discovery_info) else: setup_platform(hass, config, add_entities, discovery_info) - start_url = "{}{}".format(hass.config.api.base_url, - FITBIT_AUTH_CALLBACK_PATH) + start_url = "{}{}".format(hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH) description = """Please create a Fitbit developer app at https://dev.fitbit.com/apps/new. @@ -172,14 +176,18 @@ def request_app_setup(hass, config, add_entities, config_path, They will provide you a Client ID and secret. These need to be saved into the file located at: {}. Then come back here and hit the below button. - """.format(start_url, config_path) + """.format( + start_url, config_path + ) submit = "I have saved my Client ID and Client Secret into fitbit.conf." - _CONFIGURING['fitbit'] = configurator.request_config( - 'Fitbit', fitbit_configuration_callback, - description=description, submit_caption=submit, - description_image="/static/images/config_fitbit_app.png" + _CONFIGURING["fitbit"] = configurator.request_config( + "Fitbit", + fitbit_configuration_callback, + description=description, + submit_caption=submit, + description_image="/static/images/config_fitbit_app.png", ) @@ -188,21 +196,23 @@ def request_oauth_completion(hass): configurator = hass.components.configurator if "fitbit" in _CONFIGURING: configurator.notify_errors( - _CONFIGURING['fitbit'], "Failed to register, please try again.") + _CONFIGURING["fitbit"], "Failed to register, please try again." + ) return def fitbit_configuration_callback(callback_data): """Handle configuration updates.""" - start_url = '{}{}'.format(hass.config.api.base_url, FITBIT_AUTH_START) + start_url = "{}{}".format(hass.config.api.base_url, FITBIT_AUTH_START) description = "Please authorize Fitbit by visiting {}".format(start_url) - _CONFIGURING['fitbit'] = configurator.request_config( - 'Fitbit', fitbit_configuration_callback, + _CONFIGURING["fitbit"] = configurator.request_config( + "Fitbit", + fitbit_configuration_callback, description=description, - submit_caption="I have authorized Fitbit." + submit_caption="I have authorized Fitbit.", ) @@ -213,12 +223,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config_file = load_json(config_path) if config_file == DEFAULT_CONFIG: request_app_setup( - hass, config, add_entities, config_path, discovery_info=None) + hass, config, add_entities, config_path, discovery_info=None + ) return False else: save_json(config_path, DEFAULT_CONFIG) - request_app_setup( - hass, config, add_entities, config_path, discovery_info=None) + request_app_setup(hass, config, add_entities, config_path, discovery_info=None) return False if "fitbit" in _CONFIGURING: @@ -230,25 +240,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): refresh_token = config_file.get(ATTR_REFRESH_TOKEN) expires_at = config_file.get(ATTR_LAST_SAVED_AT) if None not in (access_token, refresh_token): - authd_client = fitbit.Fitbit(config_file.get(ATTR_CLIENT_ID), - config_file.get(ATTR_CLIENT_SECRET), - access_token=access_token, - refresh_token=refresh_token, - expires_at=expires_at, - refresh_cb=lambda x: None) + authd_client = fitbit.Fitbit( + config_file.get(ATTR_CLIENT_ID), + config_file.get(ATTR_CLIENT_SECRET), + access_token=access_token, + refresh_token=refresh_token, + expires_at=expires_at, + refresh_cb=lambda x: None, + ) if int(time.time()) - expires_at > 3600: authd_client.client.refresh_token() unit_system = config.get(CONF_UNIT_SYSTEM) - if unit_system == 'default': - authd_client.system = authd_client. \ - user_profile_get()["user"]["locale"] - if authd_client.system != 'en_GB': + if unit_system == "default": + authd_client.system = authd_client.user_profile_get()["user"]["locale"] + if authd_client.system != "en_GB": if hass.config.units.is_metric: - authd_client.system = 'metric' + authd_client.system = "metric" else: - authd_client.system = 'en_US' + authd_client.system = "en_US" else: authd_client.system = unit_system @@ -258,33 +269,54 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for resource in config.get(CONF_MONITORED_RESOURCES): # monitor battery for all linked FitBit devices - if resource == 'devices/battery': + if resource == "devices/battery": for dev_extra in registered_devs: - dev.append(FitbitSensor( - authd_client, config_path, resource, - hass.config.units.is_metric, clock_format, dev_extra)) + dev.append( + FitbitSensor( + authd_client, + config_path, + resource, + hass.config.units.is_metric, + clock_format, + dev_extra, + ) + ) else: - dev.append(FitbitSensor( - authd_client, config_path, resource, - hass.config.units.is_metric, clock_format)) + dev.append( + FitbitSensor( + authd_client, + config_path, + resource, + hass.config.units.is_metric, + clock_format, + ) + ) add_entities(dev, True) else: oauth = fitbit.api.FitbitOauth2Client( - config_file.get(ATTR_CLIENT_ID), - config_file.get(ATTR_CLIENT_SECRET)) + config_file.get(ATTR_CLIENT_ID), config_file.get(ATTR_CLIENT_SECRET) + ) - redirect_uri = '{}{}'.format(hass.config.api.base_url, - FITBIT_AUTH_CALLBACK_PATH) + redirect_uri = "{}{}".format( + hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH + ) fitbit_auth_start_url, _ = oauth.authorize_token_url( redirect_uri=redirect_uri, - scope=['activity', 'heartrate', 'nutrition', 'profile', - 'settings', 'sleep', 'weight']) + scope=[ + "activity", + "heartrate", + "nutrition", + "profile", + "settings", + "sleep", + "weight", + ], + ) hass.http.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url) - hass.http.register_view(FitbitAuthCallbackView( - config, add_entities, oauth)) + hass.http.register_view(FitbitAuthCallbackView(config, add_entities, oauth)) request_oauth_completion(hass) @@ -294,7 +326,7 @@ class FitbitAuthCallbackView(HomeAssistantView): requires_auth = False url = FITBIT_AUTH_CALLBACK_PATH - name = 'api:fitbit:callback' + name = "api:fitbit:callback" def __init__(self, config, add_entities, oauth): """Initialize the OAuth callback view.""" @@ -308,30 +340,34 @@ class FitbitAuthCallbackView(HomeAssistantView): from oauthlib.oauth2.rfc6749.errors import MismatchingStateError from oauthlib.oauth2.rfc6749.errors import MissingTokenError - hass = request.app['hass'] + hass = request.app["hass"] data = request.query response_message = """Fitbit has been successfully authorized! You can close this window now!""" result = None - if data.get('code') is not None: - redirect_uri = '{}{}'.format( - hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH) + if data.get("code") is not None: + redirect_uri = "{}{}".format( + hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH + ) try: - result = self.oauth.fetch_access_token(data.get('code'), - redirect_uri) + result = self.oauth.fetch_access_token(data.get("code"), redirect_uri) except MissingTokenError as error: _LOGGER.error("Missing token: %s", error) response_message = """Something went wrong when attempting authenticating with Fitbit. The error - encountered was {}. Please try again!""".format(error) + encountered was {}. Please try again!""".format( + error + ) except MismatchingStateError as error: _LOGGER.error("Mismatched state, CSRF error: %s", error) response_message = """Something went wrong when attempting authenticating with Fitbit. The error - encountered was {}. Please try again!""".format(error) + encountered was {}. Please try again!""".format( + error + ) else: _LOGGER.error("Unknown error when authing") response_message = """Something went wrong when @@ -347,20 +383,21 @@ class FitbitAuthCallbackView(HomeAssistantView): """ html_response = """Fitbit Auth -

{}

""".format(response_message) +

{}

""".format( + response_message + ) if result: config_contents = { - ATTR_ACCESS_TOKEN: result.get('access_token'), - ATTR_REFRESH_TOKEN: result.get('refresh_token'), + ATTR_ACCESS_TOKEN: result.get("access_token"), + ATTR_REFRESH_TOKEN: result.get("refresh_token"), ATTR_CLIENT_ID: self.oauth.client_id, ATTR_CLIENT_SECRET: self.oauth.client_secret, - ATTR_LAST_SAVED_AT: int(time.time()) + ATTR_LAST_SAVED_AT: int(time.time()), } save_json(hass.config.path(FITBIT_CONFIG_FILE), config_contents) - hass.async_add_job(setup_platform, hass, self.config, - self.add_entities) + hass.async_add_job(setup_platform, hass, self.config, self.add_entities) return html_response @@ -368,8 +405,9 @@ class FitbitAuthCallbackView(HomeAssistantView): class FitbitSensor(Entity): """Implementation of a Fitbit sensor.""" - def __init__(self, client, config_path, resource_type, - is_metric, clock_format, extra=None): + def __init__( + self, client, config_path, resource_type, is_metric, clock_format, extra=None + ): """Initialize the Fitbit sensor.""" self.client = client self.config_path = config_path @@ -379,17 +417,17 @@ class FitbitSensor(Entity): self.extra = extra self._name = FITBIT_RESOURCES_LIST[self.resource_type][0] if self.extra: - self._name = '{0} Battery'.format(self.extra.get('deviceVersion')) + self._name = "{0} Battery".format(self.extra.get("deviceVersion")) unit_type = FITBIT_RESOURCES_LIST[self.resource_type][1] if unit_type == "": - split_resource = self.resource_type.split('/') + split_resource = self.resource_type.split("/") try: measurement_system = FITBIT_MEASUREMENTS[self.client.system] except KeyError: if self.is_metric: - measurement_system = FITBIT_MEASUREMENTS['metric'] + measurement_system = FITBIT_MEASUREMENTS["metric"] else: - measurement_system = FITBIT_MEASUREMENTS['en_US'] + measurement_system = FITBIT_MEASUREMENTS["en_US"] unit_type = measurement_system[split_resource[-1]] self._unit_of_measurement = unit_type self._state = 0 @@ -412,11 +450,10 @@ class FitbitSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - if self.resource_type == 'devices/battery' and self.extra: - battery_level = BATTERY_LEVELS[self.extra.get('battery')] - return icon_for_battery_level( - battery_level=battery_level, charging=None) - return 'mdi:{}'.format(FITBIT_RESOURCES_LIST[self.resource_type][2]) + if self.resource_type == "devices/battery" and self.extra: + battery_level = BATTERY_LEVELS[self.extra.get("battery")] + return icon_for_battery_level(battery_level=battery_level, charging=None) + return "mdi:{}".format(FITBIT_RESOURCES_LIST[self.resource_type][2]) @property def device_state_attributes(self): @@ -426,43 +463,42 @@ class FitbitSensor(Entity): attrs[ATTR_ATTRIBUTION] = ATTRIBUTION if self.extra: - attrs['model'] = self.extra.get('deviceVersion') - attrs['type'] = self.extra.get('type').lower() + attrs["model"] = self.extra.get("deviceVersion") + attrs["type"] = self.extra.get("type").lower() return attrs def update(self): """Get the latest data from the Fitbit API and update the states.""" - if self.resource_type == 'devices/battery' and self.extra: - self._state = self.extra.get('battery') + if self.resource_type == "devices/battery" and self.extra: + self._state = self.extra.get("battery") else: container = self.resource_type.replace("/", "-") - response = self.client.time_series(self.resource_type, period='7d') - raw_state = response[container][-1].get('value') - if self.resource_type == 'activities/distance': - self._state = format(float(raw_state), '.2f') - elif self.resource_type == 'activities/tracker/distance': - self._state = format(float(raw_state), '.2f') - elif self.resource_type == 'body/bmi': - self._state = format(float(raw_state), '.1f') - elif self.resource_type == 'body/fat': - self._state = format(float(raw_state), '.1f') - elif self.resource_type == 'body/weight': - self._state = format(float(raw_state), '.1f') - elif self.resource_type == 'sleep/startTime': - if raw_state == '': - self._state = '-' - elif self.clock_format == '12H': - hours, minutes = raw_state.split(':') + response = self.client.time_series(self.resource_type, period="7d") + raw_state = response[container][-1].get("value") + if self.resource_type == "activities/distance": + self._state = format(float(raw_state), ".2f") + elif self.resource_type == "activities/tracker/distance": + self._state = format(float(raw_state), ".2f") + elif self.resource_type == "body/bmi": + self._state = format(float(raw_state), ".1f") + elif self.resource_type == "body/fat": + self._state = format(float(raw_state), ".1f") + elif self.resource_type == "body/weight": + self._state = format(float(raw_state), ".1f") + elif self.resource_type == "sleep/startTime": + if raw_state == "": + self._state = "-" + elif self.clock_format == "12H": + hours, minutes = raw_state.split(":") hours, minutes = int(hours), int(minutes) - setting = 'AM' + setting = "AM" if hours > 12: - setting = 'PM' + setting = "PM" hours -= 12 elif hours == 0: hours = 12 - self._state = '{}:{:02d} {}'.format(hours, minutes, - setting) + self._state = "{}:{:02d} {}".format(hours, minutes, setting) else: self._state = raw_state else: @@ -470,20 +506,19 @@ class FitbitSensor(Entity): self._state = raw_state else: try: - self._state = '{0:,}'.format(int(raw_state)) + self._state = "{0:,}".format(int(raw_state)) except TypeError: self._state = raw_state - if self.resource_type == 'activities/heart': - self._state = response[container][-1]. \ - get('value').get('restingHeartRate') + if self.resource_type == "activities/heart": + self._state = response[container][-1].get("value").get("restingHeartRate") token = self.client.client.session.token config_contents = { - ATTR_ACCESS_TOKEN: token.get('access_token'), - ATTR_REFRESH_TOKEN: token.get('refresh_token'), + ATTR_ACCESS_TOKEN: token.get("access_token"), + ATTR_REFRESH_TOKEN: token.get("refresh_token"), ATTR_CLIENT_ID: self.client.client.client_id, ATTR_CLIENT_SECRET: self.client.client.client_secret, - ATTR_LAST_SAVED_AT: int(time.time()) + ATTR_LAST_SAVED_AT: int(time.time()), } save_json(self.config_path, config_contents) diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index 4cf2b0b9243..a97f77138db 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -11,24 +11,26 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_EXCHANGE_RATE = 'Exchange rate' -ATTR_TARGET = 'Target currency' +ATTR_EXCHANGE_RATE = "Exchange rate" +ATTR_TARGET = "Target currency" ATTRIBUTION = "Data provided by the European Central Bank (ECB)" -CONF_TARGET = 'target' +CONF_TARGET = "target" -DEFAULT_BASE = 'USD' -DEFAULT_NAME = 'Exchange rate' +DEFAULT_BASE = "USD" +DEFAULT_NAME = "Exchange rate" -ICON = 'mdi:currency-usd' +ICON = "mdi:currency-usd" SCAN_INTERVAL = timedelta(days=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_TARGET): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_TARGET): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -80,7 +82,7 @@ class ExchangeRateSensor(Entity): if self.data.rate is not None: return { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_EXCHANGE_RATE: self.data.rate['rates'][self._target], + ATTR_EXCHANGE_RATE: self.data.rate["rates"][self._target], ATTR_TARGET: self._target, } @@ -92,7 +94,7 @@ class ExchangeRateSensor(Entity): def update(self): """Get the latest data and updates the states.""" self.data.update() - self._state = round(self.data.rate['rates'][self._target], 3) + self._state = round(self.data.rate["rates"][self._target], 3) class ExchangeData: @@ -105,8 +107,7 @@ class ExchangeData: self.api_key = api_key self.rate = None self.target_currency = target_currency - self.exchange = Fixerio( - symbols=[self.target_currency], access_key=self.api_key) + self.exchange = Fixerio(symbols=[self.target_currency], access_key=self.api_key) def update(self): """Get the latest data from Fixer.io.""" diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index dac5d654c41..0561530345c 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -11,25 +11,26 @@ from homeassistant.helpers.event import track_utc_time_change _LOGGER = logging.getLogger(__name__) -CONF_CLIENT_ID = 'client_id' -CONF_CLIENT_SECRET = 'client_secret' -CONF_INCLUDE = 'include' +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" +CONF_INCLUDE = "include" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_INCLUDE, default=[]): - vol.All(cv.ensure_list, [cv.string]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_INCLUDE, default=[]): vol.All(cv.ensure_list, [cv.string]), + } +) def setup_scanner(hass, config: dict, see, discovery_info=None): """Set up the DeviceScanner and check if login is valid.""" scanner = FleetGoDeviceScanner(config, see) if not scanner.login(hass): - _LOGGER.error('FleetGO authentication failed') + _LOGGER.error("FleetGO authentication failed") return False return True @@ -44,17 +45,19 @@ class FleetGoDeviceScanner: self._include = config.get(CONF_INCLUDE) self._see = see - self._api = API(config.get(CONF_CLIENT_ID), - config.get(CONF_CLIENT_SECRET), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)) + self._api = API( + config.get(CONF_CLIENT_ID), + config.get(CONF_CLIENT_SECRET), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), + ) def setup(self, hass): """Set up a timer and start gathering devices.""" self._refresh() - track_utc_time_change(hass, - lambda now: self._refresh(), - second=range(0, 60, 30)) + track_utc_time_change( + hass, lambda now: self._refresh(), second=range(0, 60, 30) + ) def login(self, hass): """Perform a login on the FleetGO API.""" @@ -69,16 +72,17 @@ class FleetGoDeviceScanner: devices = self._api.get_devices() for device in devices: - if (not self._include or - device.license_plate in self._include): + if not self._include or device.license_plate in self._include: if device.active or device.current_address is None: device.get_map_details() - self._see(dev_id=device.plate_as_id, - gps=(device.latitude, device.longitude), - attributes=device.state_attributes, - icon='mdi:car') + self._see( + dev_id=device.plate_as_id, + gps=(device.latitude, device.longitude), + attributes=device.state_attributes, + icon="mdi:car", + ) except requests.exceptions.ConnectionError: - _LOGGER.error('ConnectionError: Could not connect to FleetGO') + _LOGGER.error("ConnectionError: Could not connect to FleetGO") diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 86789285e60..951033849b6 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -16,21 +16,32 @@ from typing import List import voluptuous as vol from homeassistant.const import ( - CONF_NAME, CONF_SLAVE, TEMP_CELSIUS, - ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME) + CONF_NAME, + CONF_SLAVE, + TEMP_CELSIUS, + ATTR_TEMPERATURE, + DEVICE_DEFAULT_NAME, +) from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, HVAC_MODE_COOL) + SUPPORT_FAN_MODE, + HVAC_MODE_COOL, +) from homeassistant.components.modbus import ( - CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) + CONF_HUB, + DEFAULT_HUB, + DOMAIN as MODBUS_DOMAIN, +) import homeassistant.helpers.config_validation as cv -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - vol.Required(CONF_SLAVE): vol.All(int, vol.Range(min=0, max=32)), - vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, + vol.Required(CONF_SLAVE): vol.All(int, vol.Range(min=0, max=32)), + vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): cv.string, + } +) _LOGGER = logging.getLogger(__name__) @@ -51,6 +62,7 @@ class Flexit(ClimateDevice): def __init__(self, hub, modbus_slave, name): """Initialize the unit.""" from pyflexit import pyflexit + self._hub = hub self._name = name self._slave = modbus_slave @@ -58,7 +70,7 @@ class Flexit(ClimateDevice): self._current_temperature = None self._current_fan_mode = None self._current_operation = None - self._fan_modes = ['Off', 'Low', 'Medium', 'High'] + self._fan_modes = ["Off", "Low", "Medium", "High"] self._current_operation = None self._filter_hours = None self._filter_alarm = None @@ -81,8 +93,7 @@ class Flexit(ClimateDevice): self._target_temperature = self.unit.get_target_temp self._current_temperature = self.unit.get_temp - self._current_fan_mode =\ - self._fan_modes[self.unit.get_fan_speed] + self._current_fan_mode = self._fan_modes[self.unit.get_fan_speed] self._filter_hours = self.unit.get_filter_hours # Mechanical heat recovery, 0-100% self._heat_recovery = self.unit.get_heat_recovery @@ -101,12 +112,12 @@ class Flexit(ClimateDevice): def device_state_attributes(self): """Return device specific state attributes.""" return { - 'filter_hours': self._filter_hours, - 'filter_alarm': self._filter_alarm, - 'heat_recovery': self._heat_recovery, - 'heating': self._heating, - 'heater_enabled': self._heater_enabled, - 'cooling': self._cooling + "filter_hours": self._filter_hours, + "filter_alarm": self._filter_alarm, + "heat_recovery": self._heat_recovery, + "heating": self._heating, + "heater_enabled": self._heater_enabled, + "cooling": self._cooling, } @property diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index 3381550b578..4fa97334889 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -6,39 +6,45 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_HOST, CONF_PORT, CONF_DISCOVERY, CONF_TIMEOUT, - EVENT_HOMEASSISTANT_STOP) -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) + CONF_HOST, + CONF_PORT, + CONF_DISCOVERY, + CONF_TIMEOUT, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) DEFAULT_TIMEOUT = 3 -CLICK_TYPE_SINGLE = 'single' -CLICK_TYPE_DOUBLE = 'double' -CLICK_TYPE_HOLD = 'hold' +CLICK_TYPE_SINGLE = "single" +CLICK_TYPE_DOUBLE = "double" +CLICK_TYPE_HOLD = "hold" CLICK_TYPES = [CLICK_TYPE_SINGLE, CLICK_TYPE_DOUBLE, CLICK_TYPE_HOLD] -CONF_IGNORED_CLICK_TYPES = 'ignored_click_types' +CONF_IGNORED_CLICK_TYPES = "ignored_click_types" -DEFAULT_HOST = 'localhost' +DEFAULT_HOST = "localhost" DEFAULT_PORT = 5551 -EVENT_NAME = 'flic_click' -EVENT_DATA_NAME = 'button_name' -EVENT_DATA_ADDRESS = 'button_address' -EVENT_DATA_TYPE = 'click_type' -EVENT_DATA_QUEUED_TIME = 'queued_time' +EVENT_NAME = "flic_click" +EVENT_DATA_NAME = "button_name" +EVENT_DATA_ADDRESS = "button_address" +EVENT_DATA_TYPE = "click_type" +EVENT_DATA_QUEUED_TIME = "queued_time" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_IGNORED_CLICK_TYPES): - vol.All(cv.ensure_list, [vol.In(CLICK_TYPES)]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_IGNORED_CLICK_TYPES): vol.All( + cv.ensure_list, [vol.In(CLICK_TYPES)] + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -65,15 +71,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery: start_scanning(config, add_entities, client) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: client.close()) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: client.close()) # Start the pyflic event handling thread threading.Thread(target=client.handle_events).start() def get_info_callback(items): """Add entities for already verified buttons.""" - addresses = items['bd_addr_of_verified_buttons'] or [] + addresses = items["bd_addr_of_verified_buttons"] or [] for address in addresses: setup_button(hass, config, add_entities, client, address) @@ -93,7 +98,8 @@ def start_scanning(config, add_entities, client): _LOGGER.info("Found new button %s", address) elif result != pyflic.ScanWizardResult.WizardFailedTimeout: _LOGGER.warning( - "Failed to connect to button %s. Reason: %s", address, result) + "Failed to connect to button %s. Reason: %s", address, result + ) # Restart scan wizard start_scanning(config, add_entities, client) @@ -160,7 +166,7 @@ class FlicButton(BinarySensorDevice): @property def name(self): """Return the name of the device.""" - return 'flic_{}'.format(self.address.replace(':', '')) + return "flic_{}".format(self.address.replace(":", "")) @property def address(self): @@ -180,21 +186,28 @@ class FlicButton(BinarySensorDevice): @property def device_state_attributes(self): """Return device specific state attributes.""" - return {'address': self.address} + return {"address": self.address} def _queued_event_check(self, click_type, time_diff): """Generate a log message and returns true if timeout exceeded.""" time_string = "{:d} {}".format( - time_diff, 'second' if time_diff == 1 else 'seconds') + time_diff, "second" if time_diff == 1 else "seconds" + ) if time_diff > self._timeout: _LOGGER.warning( "Queued %s dropped for %s. Time in queue was %s", - click_type, self.address, time_string) + click_type, + self.address, + time_string, + ) return True _LOGGER.info( "Queued %s allowed for %s. Time in queue was %s", - click_type, self.address, time_string) + click_type, + self.address, + time_string, + ) return False def _on_up_down(self, channel, click_type, was_queued, time_diff): @@ -218,18 +231,21 @@ class FlicButton(BinarySensorDevice): if hass_click_type in self._ignored_click_types: return - self._hass.bus.fire(EVENT_NAME, { - EVENT_DATA_NAME: self.name, - EVENT_DATA_ADDRESS: self.address, - EVENT_DATA_QUEUED_TIME: time_diff, - EVENT_DATA_TYPE: hass_click_type - }) + self._hass.bus.fire( + EVENT_NAME, + { + EVENT_DATA_NAME: self.name, + EVENT_DATA_ADDRESS: self.address, + EVENT_DATA_QUEUED_TIME: time_diff, + EVENT_DATA_TYPE: hass_click_type, + }, + ) - def _connection_status_changed( - self, channel, connection_status, disconnect_reason): + def _connection_status_changed(self, channel, connection_status, disconnect_reason): """Remove device, if button disconnects.""" import pyflic if connection_status == pyflic.ConnectionStatus.Disconnected: - _LOGGER.warning("Button (%s) disconnected. Reason: %s", - self.address, disconnect_reason) + _LOGGER.warning( + "Button (%s) disconnected. Reason: %s", self.address, disconnect_reason + ) diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index 93a478611db..07abd097c87 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -9,21 +9,18 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService _LOGGER = logging.getLogger(__name__) -_RESOURCE = 'https://api.flock.com/hooks/sendMessage/' +_RESOURCE = "https://api.flock.com/hooks/sendMessage/" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_ACCESS_TOKEN): cv.string}) async def get_service(hass, config, discovery_info=None): """Get the Flock notification service.""" access_token = config.get(CONF_ACCESS_TOKEN) - url = '{}{}'.format(_RESOURCE, access_token) + url = "{}{}".format(_RESOURCE, access_token) session = async_get_clientsession(hass) return FlockNotificationService(url, session) @@ -39,7 +36,7 @@ class FlockNotificationService(BaseNotificationService): async def async_send_message(self, message, **kwargs): """Send the message to the user.""" - payload = {'text': message} + payload = {"text": message} _LOGGER.debug("Attempting to call Flock at %s", self._url) @@ -48,9 +45,11 @@ class FlockNotificationService(BaseNotificationService): response = await self._session.post(self._url, json=payload) result = await response.json() - if response.status != 200 or 'error' in result: + if response.status != 200 or "error" in result: _LOGGER.error( "Flock service returned HTTP status %d, response %s", - response.status, result) + response.status, + result, + ) except asyncio.TimeoutError: _LOGGER.error("Timeout accessing Flock at %s", self._url) diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 148a3ee4159..97453c41af0 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -7,72 +7,78 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_STATE, CONF_LATITUDE, CONF_MONITORED_CONDITIONS, - CONF_LONGITUDE) + ATTR_ATTRIBUTION, + ATTR_STATE, + CONF_LATITUDE, + CONF_MONITORED_CONDITIONS, + CONF_LONGITUDE, +) from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTR_CITY = 'city' -ATTR_REPORTED_DATE = 'reported_date' -ATTR_REPORTED_LATITUDE = 'reported_latitude' -ATTR_REPORTED_LONGITUDE = 'reported_longitude' -ATTR_STATE_REPORTS_LAST_WEEK = 'state_reports_last_week' -ATTR_STATE_REPORTS_THIS_WEEK = 'state_reports_this_week' -ATTR_ZIP_CODE = 'zip_code' +ATTR_CITY = "city" +ATTR_REPORTED_DATE = "reported_date" +ATTR_REPORTED_LATITUDE = "reported_latitude" +ATTR_REPORTED_LONGITUDE = "reported_longitude" +ATTR_STATE_REPORTS_LAST_WEEK = "state_reports_last_week" +ATTR_STATE_REPORTS_THIS_WEEK = "state_reports_this_week" +ATTR_ZIP_CODE = "zip_code" -DEFAULT_ATTRIBUTION = 'Data provided by Flu Near You' +DEFAULT_ATTRIBUTION = "Data provided by Flu Near You" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) SCAN_INTERVAL = timedelta(minutes=30) -CATEGORY_CDC_REPORT = 'cdc_report' -CATEGORY_USER_REPORT = 'user_report' +CATEGORY_CDC_REPORT = "cdc_report" +CATEGORY_USER_REPORT = "user_report" -TYPE_CDC_LEVEL = 'level' -TYPE_CDC_LEVEL2 = 'level2' -TYPE_USER_CHICK = 'chick' -TYPE_USER_DENGUE = 'dengue' -TYPE_USER_FLU = 'flu' -TYPE_USER_LEPTO = 'lepto' -TYPE_USER_NO_SYMPTOMS = 'none' -TYPE_USER_SYMPTOMS = 'symptoms' -TYPE_USER_TOTAL = 'total' +TYPE_CDC_LEVEL = "level" +TYPE_CDC_LEVEL2 = "level2" +TYPE_USER_CHICK = "chick" +TYPE_USER_DENGUE = "dengue" +TYPE_USER_FLU = "flu" +TYPE_USER_LEPTO = "lepto" +TYPE_USER_NO_SYMPTOMS = "none" +TYPE_USER_SYMPTOMS = "symptoms" +TYPE_USER_TOTAL = "total" EXTENDED_TYPE_MAPPING = { - TYPE_USER_FLU: 'ili', - TYPE_USER_NO_SYMPTOMS: 'no_symptoms', - TYPE_USER_TOTAL: 'total_surveys', + TYPE_USER_FLU: "ili", + TYPE_USER_NO_SYMPTOMS: "no_symptoms", + TYPE_USER_TOTAL: "total_surveys", } SENSORS = { CATEGORY_CDC_REPORT: [ - (TYPE_CDC_LEVEL, 'CDC Level', 'mdi:biohazard', None), - (TYPE_CDC_LEVEL2, 'CDC Level 2', 'mdi:biohazard', None), + (TYPE_CDC_LEVEL, "CDC Level", "mdi:biohazard", None), + (TYPE_CDC_LEVEL2, "CDC Level 2", "mdi:biohazard", None), ], CATEGORY_USER_REPORT: [ - (TYPE_USER_CHICK, 'Avian Flu Symptoms', 'mdi:alert', 'reports'), - (TYPE_USER_DENGUE, 'Dengue Fever Symptoms', 'mdi:alert', 'reports'), - (TYPE_USER_FLU, 'Flu Symptoms', 'mdi:alert', 'reports'), - (TYPE_USER_LEPTO, 'Leptospirosis Symptoms', 'mdi:alert', 'reports'), - (TYPE_USER_NO_SYMPTOMS, 'No Symptoms', 'mdi:alert', 'reports'), - (TYPE_USER_SYMPTOMS, 'Flu-like Symptoms', 'mdi:alert', 'reports'), - (TYPE_USER_TOTAL, 'Total Symptoms', 'mdi:alert', 'reports'), - ] + (TYPE_USER_CHICK, "Avian Flu Symptoms", "mdi:alert", "reports"), + (TYPE_USER_DENGUE, "Dengue Fever Symptoms", "mdi:alert", "reports"), + (TYPE_USER_FLU, "Flu Symptoms", "mdi:alert", "reports"), + (TYPE_USER_LEPTO, "Leptospirosis Symptoms", "mdi:alert", "reports"), + (TYPE_USER_NO_SYMPTOMS, "No Symptoms", "mdi:alert", "reports"), + (TYPE_USER_SYMPTOMS, "Flu-like Symptoms", "mdi:alert", "reports"), + (TYPE_USER_TOTAL, "Total Symptoms", "mdi:alert", "reports"), + ], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): - vol.All(cv.ensure_list, [vol.In(SENSORS)]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( + cv.ensure_list, [vol.In(SENSORS)] + ), + } +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Configure the platform and add the sensors.""" from pyflunearyou import Client @@ -82,8 +88,8 @@ async def async_setup_platform( longitude = config.get(CONF_LONGITUDE, hass.config.longitude) fny = FluNearYouData( - Client(websession), latitude, longitude, - config[CONF_MONITORED_CONDITIONS]) + Client(websession), latitude, longitude, config[CONF_MONITORED_CONDITIONS] + ) await fny.async_update() sensors = [ @@ -137,8 +143,7 @@ class FluNearYouSensor(Entity): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return '{0},{1}_{2}'.format( - self.fny.latitude, self.fny.longitude, self._kind) + return "{0},{1}_{2}".format(self.fny.latitude, self.fny.longitude, self._kind) @property def unit_of_measurement(self): @@ -153,37 +158,51 @@ class FluNearYouSensor(Entity): user_data = self.fny.data.get(CATEGORY_USER_REPORT) if self._category == CATEGORY_CDC_REPORT and cdc_data: - self._attrs.update({ - ATTR_REPORTED_DATE: cdc_data['week_date'], - ATTR_STATE: cdc_data['name'], - }) + self._attrs.update( + { + ATTR_REPORTED_DATE: cdc_data["week_date"], + ATTR_STATE: cdc_data["name"], + } + ) self._state = cdc_data[self._kind] elif self._category == CATEGORY_USER_REPORT and user_data: - self._attrs.update({ - ATTR_CITY: user_data['local']['city'].split('(')[0], - ATTR_REPORTED_LATITUDE: user_data['local']['latitude'], - ATTR_REPORTED_LONGITUDE: user_data['local']['longitude'], - ATTR_STATE: user_data['state']['name'], - ATTR_ZIP_CODE: user_data['local']['zip'], - }) + self._attrs.update( + { + ATTR_CITY: user_data["local"]["city"].split("(")[0], + ATTR_REPORTED_LATITUDE: user_data["local"]["latitude"], + ATTR_REPORTED_LONGITUDE: user_data["local"]["longitude"], + ATTR_STATE: user_data["state"]["name"], + ATTR_ZIP_CODE: user_data["local"]["zip"], + } + ) - if self._kind in user_data['state']['data']: + if self._kind in user_data["state"]["data"]: states_key = self._kind elif self._kind in EXTENDED_TYPE_MAPPING: states_key = EXTENDED_TYPE_MAPPING[self._kind] - self._attrs[ATTR_STATE_REPORTS_THIS_WEEK] = user_data['state'][ - 'data'][states_key] - self._attrs[ATTR_STATE_REPORTS_LAST_WEEK] = user_data['state'][ - 'last_week_data'][states_key] + self._attrs[ATTR_STATE_REPORTS_THIS_WEEK] = user_data["state"]["data"][ + states_key + ] + self._attrs[ATTR_STATE_REPORTS_LAST_WEEK] = user_data["state"][ + "last_week_data" + ][states_key] if self._kind == TYPE_USER_TOTAL: self._state = sum( - v for k, v in user_data['local'].items() if k in ( - TYPE_USER_CHICK, TYPE_USER_DENGUE, TYPE_USER_FLU, - TYPE_USER_LEPTO, TYPE_USER_SYMPTOMS)) + v + for k, v in user_data["local"].items() + if k + in ( + TYPE_USER_CHICK, + TYPE_USER_DENGUE, + TYPE_USER_FLU, + TYPE_USER_LEPTO, + TYPE_USER_SYMPTOMS, + ) + ) else: - self._state = user_data['local'][self._kind] + self._state = user_data["local"][self._kind] class FluNearYouData: @@ -202,17 +221,15 @@ class FluNearYouData: """Update Flu Near You data.""" from pyflunearyou.errors import FluNearYouError - for key, method in [(CATEGORY_CDC_REPORT, - self._client.cdc_reports.status_by_coordinates), - (CATEGORY_USER_REPORT, - self._client.user_reports.status_by_coordinates)]: + for key, method in [ + (CATEGORY_CDC_REPORT, self._client.cdc_reports.status_by_coordinates), + (CATEGORY_USER_REPORT, self._client.user_reports.status_by_coordinates), + ]: if key in self._sensor_types: try: - self.data[key] = await method( - self.latitude, self.longitude) + self.data[key] = await method(self.latitude, self.longitude) except FluNearYouError as err: - _LOGGER.error( - 'There was an error with "%s" data: %s', key, err) + _LOGGER.error('There was an error with "%s" data: %s', key, err) self.data[key] = {} - _LOGGER.debug('New data stored: %s', self.data) + _LOGGER.debug("New data stored: %s", self.data) diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index f0134f04d89..800ccd1938f 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -13,60 +13,83 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION) + is_on, + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, + DOMAIN as LIGHT_DOMAIN, + VALID_TRANSITION, +) from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, CONF_LIGHTS, CONF_MODE, - SERVICE_TURN_ON, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) + ATTR_ENTITY_ID, + CONF_NAME, + CONF_PLATFORM, + CONF_LIGHTS, + CONF_MODE, + SERVICE_TURN_ON, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, +) from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify from homeassistant.util.color import ( - color_temperature_to_rgb, color_RGB_to_xy_brightness, - color_temperature_kelvin_to_mired) + color_temperature_to_rgb, + color_RGB_to_xy_brightness, + color_temperature_kelvin_to_mired, +) from homeassistant.util.dt import utcnow as dt_utcnow, as_local _LOGGER = logging.getLogger(__name__) -CONF_START_TIME = 'start_time' -CONF_STOP_TIME = 'stop_time' -CONF_START_CT = 'start_colortemp' -CONF_SUNSET_CT = 'sunset_colortemp' -CONF_STOP_CT = 'stop_colortemp' -CONF_BRIGHTNESS = 'brightness' -CONF_DISABLE_BRIGHTNESS_ADJUST = 'disable_brightness_adjust' -CONF_INTERVAL = 'interval' +CONF_START_TIME = "start_time" +CONF_STOP_TIME = "stop_time" +CONF_START_CT = "start_colortemp" +CONF_SUNSET_CT = "sunset_colortemp" +CONF_STOP_CT = "stop_colortemp" +CONF_BRIGHTNESS = "brightness" +CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" +CONF_INTERVAL = "interval" -MODE_XY = 'xy' -MODE_MIRED = 'mired' -MODE_RGB = 'rgb' +MODE_XY = "xy" +MODE_MIRED = "mired" +MODE_RGB = "rgb" DEFAULT_MODE = MODE_XY -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'flux', - vol.Required(CONF_LIGHTS): cv.entity_ids, - vol.Optional(CONF_NAME, default="Flux"): cv.string, - vol.Optional(CONF_START_TIME): cv.time, - vol.Optional(CONF_STOP_TIME): cv.time, - vol.Optional(CONF_START_CT, default=4000): - vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)), - vol.Optional(CONF_SUNSET_CT, default=3000): - vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)), - vol.Optional(CONF_STOP_CT, default=1900): - vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)), - vol.Optional(CONF_BRIGHTNESS): - vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), - vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST): cv.boolean, - vol.Optional(CONF_MODE, default=DEFAULT_MODE): - vol.Any(MODE_XY, MODE_MIRED, MODE_RGB), - vol.Optional(CONF_INTERVAL, default=30): cv.positive_int, - vol.Optional(ATTR_TRANSITION, default=30): VALID_TRANSITION -}) +PLATFORM_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "flux", + vol.Required(CONF_LIGHTS): cv.entity_ids, + vol.Optional(CONF_NAME, default="Flux"): cv.string, + vol.Optional(CONF_START_TIME): cv.time, + vol.Optional(CONF_STOP_TIME): cv.time, + vol.Optional(CONF_START_CT, default=4000): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=40000) + ), + vol.Optional(CONF_SUNSET_CT, default=3000): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=40000) + ), + vol.Optional(CONF_STOP_CT, default=1900): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=40000) + ), + vol.Optional(CONF_BRIGHTNESS): vol.All( + vol.Coerce(int), vol.Range(min=0, max=255) + ), + vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST): cv.boolean, + vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.Any( + MODE_XY, MODE_MIRED, MODE_RGB + ), + vol.Optional(CONF_INTERVAL, default=30): cv.positive_int, + vol.Optional(ATTR_TRANSITION, default=30): VALID_TRANSITION, + } +) -async def async_set_lights_xy(hass, lights, x_val, y_val, brightness, - transition): +async def async_set_lights_xy(hass, lights, x_val, y_val, brightness, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): @@ -78,8 +101,7 @@ async def async_set_lights_xy(hass, lights, x_val, y_val, brightness, service_data[ATTR_WHITE_VALUE] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - await hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + await hass.services.async_call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) async def async_set_lights_temp(hass, lights, mired, brightness, transition): @@ -93,8 +115,7 @@ async def async_set_lights_temp(hass, lights, mired, brightness, transition): service_data[ATTR_BRIGHTNESS] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - await hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + await hass.services.async_call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) async def async_set_lights_rgb(hass, lights, rgb, transition): @@ -106,12 +127,10 @@ async def async_set_lights_rgb(hass, lights, rgb, transition): service_data[ATTR_RGB_COLOR] = rgb if transition is not None: service_data[ATTR_TRANSITION] = transition - await hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + await hass.services.async_call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Flux switches.""" name = config.get(CONF_NAME) lights = config.get(CONF_LIGHTS) @@ -125,27 +144,50 @@ async def async_setup_platform(hass, config, async_add_entities, mode = config.get(CONF_MODE) interval = config.get(CONF_INTERVAL) transition = config.get(ATTR_TRANSITION) - flux = FluxSwitch(name, hass, lights, start_time, stop_time, - start_colortemp, sunset_colortemp, stop_colortemp, - brightness, disable_brightness_adjust, mode, interval, - transition) + flux = FluxSwitch( + name, + hass, + lights, + start_time, + stop_time, + start_colortemp, + sunset_colortemp, + stop_colortemp, + brightness, + disable_brightness_adjust, + mode, + interval, + transition, + ) async_add_entities([flux]) async def async_update(call=None): """Update lights.""" await flux.async_flux_update() - service_name = slugify("{} {}".format(name, 'update')) + service_name = slugify("{} {}".format(name, "update")) hass.services.async_register(DOMAIN, service_name, async_update) class FluxSwitch(SwitchDevice): """Representation of a Flux switch.""" - def __init__(self, name, hass, lights, start_time, stop_time, - start_colortemp, sunset_colortemp, stop_colortemp, - brightness, disable_brightness_adjust, mode, interval, - transition): + def __init__( + self, + name, + hass, + lights, + start_time, + stop_time, + start_colortemp, + sunset_colortemp, + stop_colortemp, + brightness, + disable_brightness_adjust, + mode, + interval, + transition, + ): """Initialize the Flux switch.""" self._name = name self.hass = hass @@ -180,7 +222,8 @@ class FluxSwitch(SwitchDevice): self.unsub_tracker = async_track_time_interval( self.hass, self.async_flux_update, - datetime.timedelta(seconds=self._interval)) + datetime.timedelta(seconds=self._interval), + ) # Make initial update await self.async_flux_update() @@ -217,7 +260,7 @@ class FluxSwitch(SwitchDevice): if start_time < now < sunset: # Daytime - time_state = 'day' + time_state = "day" temp_range = abs(self._start_colortemp - self._sunset_colortemp) day_length = int(sunset.timestamp() - start_time.timestamp()) seconds_from_start = int(now.timestamp() - start_time.timestamp()) @@ -229,7 +272,7 @@ class FluxSwitch(SwitchDevice): temp = self._start_colortemp + temp_offset else: # Night time - time_state = 'night' + time_state = "night" if now < stop_time: if stop_time < start_time and stop_time.day == sunset.day: @@ -238,10 +281,8 @@ class FluxSwitch(SwitchDevice): else: sunset_time = sunset - night_length = int(stop_time.timestamp() - - sunset_time.timestamp()) - seconds_from_sunset = int(now.timestamp() - - sunset_time.timestamp()) + night_length = int(stop_time.timestamp() - sunset_time.timestamp()) + seconds_from_sunset = int(now.timestamp() - sunset_time.timestamp()) percentage_complete = seconds_from_sunset / night_length else: percentage_complete = 1 @@ -258,44 +299,60 @@ class FluxSwitch(SwitchDevice): if self._disable_brightness_adjust: brightness = None if self._mode == MODE_XY: - await async_set_lights_xy(self.hass, self._lights, x_val, - y_val, brightness, self._transition) - _LOGGER.info("Lights updated to x:%s y:%s brightness:%s, %s%% " - "of %s cycle complete at %s", x_val, y_val, - brightness, round( - percentage_complete * 100), time_state, now) + await async_set_lights_xy( + self.hass, self._lights, x_val, y_val, brightness, self._transition + ) + _LOGGER.info( + "Lights updated to x:%s y:%s brightness:%s, %s%% " + "of %s cycle complete at %s", + x_val, + y_val, + brightness, + round(percentage_complete * 100), + time_state, + now, + ) elif self._mode == MODE_RGB: - await async_set_lights_rgb(self.hass, self._lights, rgb, - self._transition) - _LOGGER.info("Lights updated to rgb:%s, %s%% " - "of %s cycle complete at %s", rgb, - round(percentage_complete * 100), time_state, now) + await async_set_lights_rgb(self.hass, self._lights, rgb, self._transition) + _LOGGER.info( + "Lights updated to rgb:%s, %s%% " "of %s cycle complete at %s", + rgb, + round(percentage_complete * 100), + time_state, + now, + ) else: # Convert to mired and clamp to allowed values mired = color_temperature_kelvin_to_mired(temp) - await async_set_lights_temp(self.hass, self._lights, mired, - brightness, self._transition) - _LOGGER.info("Lights updated to mired:%s brightness:%s, %s%% " - "of %s cycle complete at %s", mired, brightness, - round(percentage_complete * 100), time_state, now) + await async_set_lights_temp( + self.hass, self._lights, mired, brightness, self._transition + ) + _LOGGER.info( + "Lights updated to mired:%s brightness:%s, %s%% " + "of %s cycle complete at %s", + mired, + brightness, + round(percentage_complete * 100), + time_state, + now, + ) def find_start_time(self, now): """Return sunrise or start_time if given.""" if self._start_time: sunrise = now.replace( - hour=self._start_time.hour, minute=self._start_time.minute, - second=0) + hour=self._start_time.hour, minute=self._start_time.minute, second=0 + ) else: - sunrise = get_astral_event_date(self.hass, SUN_EVENT_SUNRISE, - now.date()) + sunrise = get_astral_event_date(self.hass, SUN_EVENT_SUNRISE, now.date()) return sunrise def find_stop_time(self, now): """Return dusk or stop_time if given.""" if self._stop_time: dusk = now.replace( - hour=self._stop_time.hour, minute=self._stop_time.minute, - second=0) + hour=self._stop_time.hour, minute=self._stop_time.minute, second=0 + ) else: - dusk = get_astral_event_date(self.hass, 'dusk', now.date()) + dusk = get_astral_event_date(self.hass, "dusk", now.date()) return dusk diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 38809e94c92..23fdb38aa05 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -2,129 +2,149 @@ import logging import socket import random -from asyncio import sleep -from functools import partial import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, ATTR_WHITE_VALUE, - EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, - SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_EFFECT, + ATTR_WHITE_VALUE, + EFFECT_COLORLOOP, + EFFECT_RANDOM, + SUPPORT_BRIGHTNESS, + SUPPORT_EFFECT, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, + Light, + PLATFORM_SCHEMA, +) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -CONF_AUTOMATIC_ADD = 'automatic_add' -CONF_CUSTOM_EFFECT = 'custom_effect' -CONF_COLORS = 'colors' -CONF_SPEED_PCT = 'speed_pct' -CONF_TRANSITION = 'transition' -ATTR_MODE = 'mode' +CONF_AUTOMATIC_ADD = "automatic_add" +CONF_CUSTOM_EFFECT = "custom_effect" +CONF_COLORS = "colors" +CONF_SPEED_PCT = "speed_pct" +CONF_TRANSITION = "transition" +ATTR_MODE = "mode" -DOMAIN = 'flux_led' +DOMAIN = "flux_led" -SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | - SUPPORT_COLOR) +SUPPORT_FLUX_LED = SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_COLOR -MODE_RGB = 'rgb' -MODE_RGBW = 'rgbw' +MODE_RGB = "rgb" +MODE_RGBW = "rgbw" # This mode enables white value to be controlled by brightness. # RGB value is ignored when this mode is specified. -MODE_WHITE = 'w' +MODE_WHITE = "w" # List of supported effects which aren't already declared in LIGHT -EFFECT_RED_FADE = 'red_fade' -EFFECT_GREEN_FADE = 'green_fade' -EFFECT_BLUE_FADE = 'blue_fade' -EFFECT_YELLOW_FADE = 'yellow_fade' -EFFECT_CYAN_FADE = 'cyan_fade' -EFFECT_PURPLE_FADE = 'purple_fade' -EFFECT_WHITE_FADE = 'white_fade' -EFFECT_RED_GREEN_CROSS_FADE = 'rg_cross_fade' -EFFECT_RED_BLUE_CROSS_FADE = 'rb_cross_fade' -EFFECT_GREEN_BLUE_CROSS_FADE = 'gb_cross_fade' -EFFECT_COLORSTROBE = 'colorstrobe' -EFFECT_RED_STROBE = 'red_strobe' -EFFECT_GREEN_STROBE = 'green_strobe' -EFFECT_BLUE_STROBE = 'blue_strobe' -EFFECT_YELLOW_STROBE = 'yellow_strobe' -EFFECT_CYAN_STROBE = 'cyan_strobe' -EFFECT_PURPLE_STROBE = 'purple_strobe' -EFFECT_WHITE_STROBE = 'white_strobe' -EFFECT_COLORJUMP = 'colorjump' -EFFECT_CUSTOM = 'custom' +EFFECT_RED_FADE = "red_fade" +EFFECT_GREEN_FADE = "green_fade" +EFFECT_BLUE_FADE = "blue_fade" +EFFECT_YELLOW_FADE = "yellow_fade" +EFFECT_CYAN_FADE = "cyan_fade" +EFFECT_PURPLE_FADE = "purple_fade" +EFFECT_WHITE_FADE = "white_fade" +EFFECT_RED_GREEN_CROSS_FADE = "rg_cross_fade" +EFFECT_RED_BLUE_CROSS_FADE = "rb_cross_fade" +EFFECT_GREEN_BLUE_CROSS_FADE = "gb_cross_fade" +EFFECT_COLORSTROBE = "colorstrobe" +EFFECT_RED_STROBE = "red_strobe" +EFFECT_GREEN_STROBE = "green_strobe" +EFFECT_BLUE_STROBE = "blue_strobe" +EFFECT_YELLOW_STROBE = "yellow_strobe" +EFFECT_CYAN_STROBE = "cyan_strobe" +EFFECT_PURPLE_STROBE = "purple_strobe" +EFFECT_WHITE_STROBE = "white_strobe" +EFFECT_COLORJUMP = "colorjump" +EFFECT_CUSTOM = "custom" EFFECT_MAP = { - EFFECT_COLORLOOP: 0x25, - EFFECT_RED_FADE: 0x26, - EFFECT_GREEN_FADE: 0x27, - EFFECT_BLUE_FADE: 0x28, - EFFECT_YELLOW_FADE: 0x29, - EFFECT_CYAN_FADE: 0x2a, - EFFECT_PURPLE_FADE: 0x2b, - EFFECT_WHITE_FADE: 0x2c, - EFFECT_RED_GREEN_CROSS_FADE: 0x2d, - EFFECT_RED_BLUE_CROSS_FADE: 0x2e, - EFFECT_GREEN_BLUE_CROSS_FADE: 0x2f, - EFFECT_COLORSTROBE: 0x30, - EFFECT_RED_STROBE: 0x31, - EFFECT_GREEN_STROBE: 0x32, - EFFECT_BLUE_STROBE: 0x33, - EFFECT_YELLOW_STROBE: 0x34, - EFFECT_CYAN_STROBE: 0x35, - EFFECT_PURPLE_STROBE: 0x36, - EFFECT_WHITE_STROBE: 0x37, - EFFECT_COLORJUMP: 0x38 + EFFECT_COLORLOOP: 0x25, + EFFECT_RED_FADE: 0x26, + EFFECT_GREEN_FADE: 0x27, + EFFECT_BLUE_FADE: 0x28, + EFFECT_YELLOW_FADE: 0x29, + EFFECT_CYAN_FADE: 0x2A, + EFFECT_PURPLE_FADE: 0x2B, + EFFECT_WHITE_FADE: 0x2C, + EFFECT_RED_GREEN_CROSS_FADE: 0x2D, + EFFECT_RED_BLUE_CROSS_FADE: 0x2E, + EFFECT_GREEN_BLUE_CROSS_FADE: 0x2F, + EFFECT_COLORSTROBE: 0x30, + EFFECT_RED_STROBE: 0x31, + EFFECT_GREEN_STROBE: 0x32, + EFFECT_BLUE_STROBE: 0x33, + EFFECT_YELLOW_STROBE: 0x34, + EFFECT_CYAN_STROBE: 0x35, + EFFECT_PURPLE_STROBE: 0x36, + EFFECT_WHITE_STROBE: 0x37, + EFFECT_COLORJUMP: 0x38, } EFFECT_CUSTOM_CODE = 0x60 -TRANSITION_GRADUAL = 'gradual' -TRANSITION_JUMP = 'jump' -TRANSITION_STROBE = 'strobe' +TRANSITION_GRADUAL = "gradual" +TRANSITION_JUMP = "jump" +TRANSITION_STROBE = "strobe" FLUX_EFFECT_LIST = sorted(list(EFFECT_MAP)) + [EFFECT_RANDOM] -CUSTOM_EFFECT_SCHEMA = vol.Schema({ - vol.Required(CONF_COLORS): - vol.All(cv.ensure_list, vol.Length(min=1, max=16), - [vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), - vol.Coerce(tuple))]), - vol.Optional(CONF_SPEED_PCT, default=50): - vol.All(vol.Range(min=0, max=100), vol.Coerce(int)), - vol.Optional(CONF_TRANSITION, default=TRANSITION_GRADUAL): - vol.All(cv.string, vol.In( - [TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE])), -}) +CUSTOM_EFFECT_SCHEMA = vol.Schema( + { + vol.Required(CONF_COLORS): vol.All( + cv.ensure_list, + vol.Length(min=1, max=16), + [ + vol.All( + vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + ) + ], + ), + vol.Optional(CONF_SPEED_PCT, default=50): vol.All( + vol.Range(min=0, max=100), vol.Coerce(int) + ), + vol.Optional(CONF_TRANSITION, default=TRANSITION_GRADUAL): vol.All( + cv.string, vol.In([TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE]) + ), + } +) -DEVICE_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(ATTR_MODE, default=MODE_RGBW): - vol.All(cv.string, vol.In([MODE_RGBW, MODE_RGB, MODE_WHITE])), - vol.Optional(CONF_PROTOCOL): - vol.All(cv.string, vol.In(['ledenet'])), - vol.Optional(CONF_CUSTOM_EFFECT): CUSTOM_EFFECT_SCHEMA, -}) +DEVICE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(ATTR_MODE, default=MODE_RGBW): vol.All( + cv.string, vol.In([MODE_RGBW, MODE_RGB, MODE_WHITE]) + ), + vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(["ledenet"])), + vol.Optional(CONF_CUSTOM_EFFECT): CUSTOM_EFFECT_SCHEMA, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, - vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Flux lights.""" import flux_led + lights = [] light_ips = [] for ipaddr, device_config in config.get(CONF_DEVICES, {}).items(): device = {} - device['name'] = device_config[CONF_NAME] - device['ipaddr'] = ipaddr + device["name"] = device_config[CONF_NAME] + device["ipaddr"] = ipaddr device[CONF_PROTOCOL] = device_config.get(CONF_PROTOCOL) device[ATTR_MODE] = device_config[ATTR_MODE] device[CONF_CUSTOM_EFFECT] = device_config.get(CONF_CUSTOM_EFFECT) @@ -140,10 +160,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): scanner = flux_led.BulbScanner() scanner.scan(timeout=10) for device in scanner.getBulbInfo(): - ipaddr = device['ipaddr'] + ipaddr = device["ipaddr"] if ipaddr in light_ips: continue - device['name'] = '{} {}'.format(device['id'], ipaddr) + device["name"] = "{} {}".format(device["id"], ipaddr) device[ATTR_MODE] = None device[CONF_PROTOCOL] = None device[CONF_CUSTOM_EFFECT] = None @@ -158,15 +178,13 @@ class FluxLight(Light): def __init__(self, device): """Initialize the light.""" - self._name = device['name'] - self._ipaddr = device['ipaddr'] + self._name = device["name"] + self._ipaddr = device["ipaddr"] self._protocol = device[CONF_PROTOCOL] self._mode = device[ATTR_MODE] self._custom_effect = device[CONF_CUSTOM_EFFECT] self._bulb = None self._error_reported = False - self._color = (0, 0, 100) - self._white_value = 0 def _connect(self): """Connect to Flux light.""" @@ -207,14 +225,14 @@ class FluxLight(Light): def brightness(self): """Return the brightness of this light between 0..255.""" if self._mode == MODE_WHITE: - return self._white_value + return self.white_value - return int(self._color[2] / 100 * 255) + return self._bulb.brightness @property def hs_color(self): """Return the color property.""" - return self._color[0:2] + return color_util.color_RGB_to_hs(*self._bulb.getRgb()) @property def supported_features(self): @@ -230,7 +248,7 @@ class FluxLight(Light): @property def white_value(self): """Return the white value of this light between 0..255.""" - return self._white_value + return self._bulb.getRgbw()[3] @property def effect_list(self): @@ -251,66 +269,75 @@ class FluxLight(Light): for effect, code in EFFECT_MAP.items(): if current_mode == code: return effect + return None - async def async_turn_on(self, **kwargs): - """Turn the specified or all lights on and wait for state.""" - await self.hass.async_add_executor_job(partial(self._turn_on, - **kwargs)) - # The bulb needs a bit to tell its new values, - # so we wait 1 second before updating - await sleep(1) - - def _turn_on(self, **kwargs): + def turn_on(self, **kwargs): """Turn the specified or all lights on.""" - self._bulb.turnOn() + if not self.is_on: + self._bulb.turnOn() hs_color = kwargs.get(ATTR_HS_COLOR) + + if hs_color: + rgb = color_util.color_hs_to_RGB(*hs_color) + else: + rgb = None + brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) white = kwargs.get(ATTR_WHITE_VALUE) - if all(item is None for item in [hs_color, brightness, effect, white]): + # Show warning if effect set with rgb, brightness, or white level + if effect and (brightness or white or rgb): + _LOGGER.warning( + "RGB, brightness and white level are ignored when" + " an effect is specified for a flux bulb" + ) + + # Random color effect + if effect == EFFECT_RANDOM: + self._bulb.setRgb( + random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) + ) return + if effect == EFFECT_CUSTOM: + if self._custom_effect: + self._bulb.setCustomPattern( + self._custom_effect[CONF_COLORS], + self._custom_effect[CONF_SPEED_PCT], + self._custom_effect[CONF_TRANSITION], + ) + return + + # Effect selection + if effect in EFFECT_MAP: + self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) + return + + # Preserve current brightness on color/white level change + if brightness is None: + brightness = self.brightness + + # Preserve color on brightness/white level change + if rgb is None: + rgb = self._bulb.getRgb() + + if white is None and self._mode == MODE_RGBW: + white = self.white_value + # handle W only mode (use brightness instead of white value) if self._mode == MODE_WHITE: - if brightness is not None: - self._bulb.setWarmWhite255(brightness) - return - if effect is not None: - # Random color effect - if effect == EFFECT_RANDOM: - self._bulb.setRgb(random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 255)) - elif effect == EFFECT_CUSTOM: - if self._custom_effect: - self._bulb.setCustomPattern( - self._custom_effect[CONF_COLORS], - self._custom_effect[CONF_SPEED_PCT], - self._custom_effect[CONF_TRANSITION]) - # Effect selection - elif effect in EFFECT_MAP: - self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) - return - # Preserve current brightness on color/white level change - if hs_color is not None: - if brightness is None: - brightness = self.brightness - color = (hs_color[0], hs_color[1], brightness / 255 * 100) - elif brightness is not None: - color = (self._color[0], self._color[1], - brightness / 255 * 100) + self._bulb.setRgbw(0, 0, 0, w=brightness) + # handle RGBW mode - if self._mode == MODE_RGBW: - if white is None: - self._bulb.setRgbw(*color_util.color_hsv_to_RGB(*color)) - else: - self._bulb.setRgbw(w=white) + elif self._mode == MODE_RGBW: + self._bulb.setRgbw(*tuple(rgb), w=white, brightness=brightness) + # handle RGB mode else: - self._bulb.setRgb(*color_util.color_hsv_to_RGB(*color)) + self._bulb.setRgb(*tuple(rgb), brightness=brightness) def turn_off(self, **kwargs): """Turn the specified or all lights off.""" @@ -325,14 +352,10 @@ class FluxLight(Light): except socket.error: self._disconnect() if not self._error_reported: - _LOGGER.warning("Failed to connect to bulb %s, %s", - self._ipaddr, self._name) + _LOGGER.warning( + "Failed to connect to bulb %s, %s", self._ipaddr, self._name + ) self._error_reported = True return + self._bulb.update_state(retry=2) - if self._mode != MODE_WHITE and self._bulb.getRgb() != (0, 0, 0): - color = self._bulb.getRgbw() - self._color = color_util.color_RGB_to_hsv(*color[0:3]) - self._white_value = color[3] - elif self._mode == MODE_WHITE: - self._white_value = self._bulb.getRgbw()[3] diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index d742166a192..b06d77d93c9 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -12,16 +12,18 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) -CONF_FOLDER_PATHS = 'folder' -CONF_FILTER = 'filter' -DEFAULT_FILTER = '*' +CONF_FOLDER_PATHS = "folder" +CONF_FILTER = "filter" +DEFAULT_FILTER = "*" SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_FOLDER_PATHS): cv.isdir, - vol.Optional(CONF_FILTER, default=DEFAULT_FILTER): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_FOLDER_PATHS): cv.isdir, + vol.Optional(CONF_FILTER, default=DEFAULT_FILTER): cv.string, + } +) def get_files_list(folder_path, filter_term): @@ -51,17 +53,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class Folder(Entity): """Representation of a folder.""" - ICON = 'mdi:folder' + ICON = "mdi:folder" def __init__(self, folder_path, filter_term): """Initialize the data object.""" - folder_path = os.path.join(folder_path, '') # If no trailing / add it - self._folder_path = folder_path # Need to check its a valid path + folder_path = os.path.join(folder_path, "") # If no trailing / add it + self._folder_path = folder_path # Need to check its a valid path self._filter_term = filter_term self._number_of_files = None self._size = None self._name = os.path.split(os.path.split(folder_path)[0])[1] - self._unit_of_measurement = 'MB' + self._unit_of_measurement = "MB" def update(self): """Update the sensor.""" @@ -78,7 +80,7 @@ class Folder(Entity): def state(self): """Return the state of the sensor.""" decimals = 2 - size_mb = round(self._size/1e6, decimals) + size_mb = round(self._size / 1e6, decimals) return size_mb @property @@ -90,11 +92,11 @@ class Folder(Entity): def device_state_attributes(self): """Return other details about the sensor state.""" attr = { - 'path': self._folder_path, - 'filter': self._filter_term, - 'number_of_files': self._number_of_files, - 'bytes': self._size, - } + "path": self._folder_path, + "filter": self._filter_term, + "number_of_files": self._number_of_files, + "bytes": self._size, + } return attr @property diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py index 411f6b480dc..b328744aaba 100644 --- a/homeassistant/components/folder_watcher/__init__.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -4,24 +4,34 @@ import os import voluptuous as vol -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_FOLDER = 'folder' -CONF_PATTERNS = 'patterns' -DEFAULT_PATTERN = '*' +CONF_FOLDER = "folder" +CONF_PATTERNS = "patterns" +DEFAULT_PATTERN = "*" DOMAIN = "folder_watcher" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ - vol.Required(CONF_FOLDER): cv.isdir, - vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]): - vol.All(cv.ensure_list, [cv.string]), - })]) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_FOLDER): cv.isdir, + vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]): vol.All( + cv.ensure_list, [cv.string] + ), + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -56,12 +66,14 @@ def create_event_handler(patterns, hass): if not event.is_directory: folder, file_name = os.path.split(event.src_path) self.hass.bus.fire( - DOMAIN, { + DOMAIN, + { "event_type": event.event_type, - 'path': event.src_path, - 'file': file_name, - 'folder': folder, - }) + "path": event.src_path, + "file": file_name, + "folder": folder, + }, + ) def on_modified(self, event): """File modified.""" @@ -82,17 +94,17 @@ def create_event_handler(patterns, hass): return EventHandler(patterns, hass) -class Watcher(): +class Watcher: """Class for starting Watchdog.""" def __init__(self, path, patterns, hass): """Initialise the watchdog observer.""" from watchdog.observers import Observer + self._observer = Observer() self._observer.schedule( - create_event_handler(patterns, hass), - path, - recursive=True) + create_event_handler(patterns, hass), path, recursive=True + ) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.startup) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown) diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index f59392bde98..8ec3541d188 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -9,7 +9,12 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( - ATTR_TIME, ATTR_TEMPERATURE, CONF_TOKEN, CONF_USERNAME, TEMP_CELSIUS) + ATTR_TIME, + ATTR_TEMPERATURE, + CONF_TOKEN, + CONF_USERNAME, + TEMP_CELSIUS, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity @@ -18,62 +23,63 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTR_HUMIDITY = 'humidity' -ATTR_PM2_5 = 'PM2.5' -ATTR_CARBON_DIOXIDE = 'CO2' -ATTR_VOLATILE_ORGANIC_COMPOUNDS = 'VOC' -ATTR_FOOBOT_INDEX = 'index' +ATTR_HUMIDITY = "humidity" +ATTR_PM2_5 = "PM2.5" +ATTR_CARBON_DIOXIDE = "CO2" +ATTR_VOLATILE_ORGANIC_COMPOUNDS = "VOC" +ATTR_FOOBOT_INDEX = "index" -SENSOR_TYPES = {'time': [ATTR_TIME, 's'], - 'pm': [ATTR_PM2_5, 'µg/m3', 'mdi:cloud'], - 'tmp': [ATTR_TEMPERATURE, TEMP_CELSIUS, 'mdi:thermometer'], - 'hum': [ATTR_HUMIDITY, '%', 'mdi:water-percent'], - 'co2': [ATTR_CARBON_DIOXIDE, 'ppm', - 'mdi:periodic-table-co2'], - 'voc': [ATTR_VOLATILE_ORGANIC_COMPOUNDS, 'ppb', - 'mdi:cloud'], - 'allpollu': [ATTR_FOOBOT_INDEX, '%', 'mdi:percent']} +SENSOR_TYPES = { + "time": [ATTR_TIME, "s"], + "pm": [ATTR_PM2_5, "µg/m3", "mdi:cloud"], + "tmp": [ATTR_TEMPERATURE, TEMP_CELSIUS, "mdi:thermometer"], + "hum": [ATTR_HUMIDITY, "%", "mdi:water-percent"], + "co2": [ATTR_CARBON_DIOXIDE, "ppm", "mdi:periodic-table-co2"], + "voc": [ATTR_VOLATILE_ORGANIC_COMPOUNDS, "ppb", "mdi:cloud"], + "allpollu": [ATTR_FOOBOT_INDEX, "%", "mdi:percent"], +} SCAN_INTERVAL = timedelta(minutes=10) PARALLEL_UPDATES = 1 TIMEOUT = 10 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TOKEN): cv.string, - vol.Required(CONF_USERNAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_USERNAME): cv.string} +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the devices associated with the account.""" from foobot_async import FoobotClient token = config.get(CONF_TOKEN) username = config.get(CONF_USERNAME) - client = FoobotClient(token, username, - async_get_clientsession(hass), - timeout=TIMEOUT) + client = FoobotClient( + token, username, async_get_clientsession(hass), timeout=TIMEOUT + ) dev = [] try: devices = await client.get_devices() _LOGGER.debug("The following devices were found: %s", devices) for device in devices: - foobot_data = FoobotData(client, device['uuid']) + foobot_data = FoobotData(client, device["uuid"]) for sensor_type in SENSOR_TYPES: - if sensor_type == 'time': + if sensor_type == "time": continue foobot_sensor = FoobotSensor(foobot_data, device, sensor_type) dev.append(foobot_sensor) - except (aiohttp.client_exceptions.ClientConnectorError, - asyncio.TimeoutError, FoobotClient.TooManyRequests, - FoobotClient.InternalError): - _LOGGER.exception('Failed to connect to foobot servers.') + except ( + aiohttp.client_exceptions.ClientConnectorError, + asyncio.TimeoutError, + FoobotClient.TooManyRequests, + FoobotClient.InternalError, + ): + _LOGGER.exception("Failed to connect to foobot servers.") raise PlatformNotReady except FoobotClient.ClientError: - _LOGGER.error('Failed to fetch data from foobot servers.') + _LOGGER.error("Failed to fetch data from foobot servers.") return async_add_entities(dev, True) @@ -83,10 +89,9 @@ class FoobotSensor(Entity): def __init__(self, data, device, sensor_type): """Initialize the sensor.""" - self._uuid = device['uuid'] + self._uuid = device["uuid"] self.foobot_data = data - self._name = 'Foobot {} {}'.format(device['name'], - SENSOR_TYPES[sensor_type][0]) + self._name = "Foobot {} {}".format(device["name"], SENSOR_TYPES[sensor_type][0]) self.type = sensor_type self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] @@ -105,7 +110,7 @@ class FoobotSensor(Entity): """Return the state of the device.""" try: data = self.foobot_data.data[self.type] - except(KeyError, TypeError): + except (KeyError, TypeError): data = None return data @@ -138,12 +143,15 @@ class FoobotData(Entity): """Get the data from Foobot API.""" interval = SCAN_INTERVAL.total_seconds() try: - response = await self._client.get_last_data(self._uuid, - interval, - interval + 1) - except (aiohttp.client_exceptions.ClientConnectorError, - asyncio.TimeoutError, self._client.TooManyRequests, - self._client.InternalError): + response = await self._client.get_last_data( + self._uuid, interval, interval + 1 + ) + except ( + aiohttp.client_exceptions.ClientConnectorError, + asyncio.TimeoutError, + self._client.TooManyRequests, + self._client.InternalError, + ): _LOGGER.debug("Couldn't fetch data") return False _LOGGER.debug("The data response is: %s", response) diff --git a/homeassistant/components/fortigate/__init__.py b/homeassistant/components/fortigate/__init__.py index 7ae3b1e0e92..d1f6eb52333 100644 --- a/homeassistant/components/fortigate/__init__.py +++ b/homeassistant/components/fortigate/__init__.py @@ -4,26 +4,36 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_DEVICES, CONF_HOST, CONF_API_KEY, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP) + CONF_DEVICES, + CONF_HOST, + CONF_API_KEY, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform _LOGGER = logging.getLogger(__name__) -DOMAIN = 'fortigate' +DOMAIN = "fortigate" DATA_FGT = DOMAIN -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_DEVICES, default=[]): - vol.All(cv.ensure_list, [cv.string]), - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_DEVICES, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -35,9 +45,7 @@ async def async_setup(hass, config): api_key = conf[CONF_API_KEY] devices = conf[CONF_DEVICES] - is_success = await async_setup_fortigate( - hass, config, host, user, api_key, devices - ) + is_success = await async_setup_fortigate(hass, config, host, user, api_key, devices) return is_success @@ -54,13 +62,11 @@ async def async_setup_fortigate(hass, config, host, user, api_key, devices): _LOGGER.error("Failed to connect to Fortigate") return False - hass.data[DATA_FGT] = { - 'fgt': fgt, - 'devices': devices - } + hass.data[DATA_FGT] = {"fgt": fgt, "devices": devices} - hass.async_create_task(async_load_platform( - hass, 'device_tracker', DOMAIN, {}, config)) + hass.async_create_task( + async_load_platform(hass, "device_tracker", DOMAIN, {}, config) + ) async def close_fgt(event): """Close Fortigate connection on HA Stop.""" diff --git a/homeassistant/components/fortigate/device_tracker.py b/homeassistant/components/fortigate/device_tracker.py index b8aa7060857..b51dc6843aa 100644 --- a/homeassistant/components/fortigate/device_tracker.py +++ b/homeassistant/components/fortigate/device_tracker.py @@ -18,14 +18,12 @@ async def async_get_scanner(hass, config): return scanner if scanner.success_init else None -Device = namedtuple('Device', ['hostname', 'mac']) +Device = namedtuple("Device", ["hostname", "mac"]) def _build_device(device_dict): """Return a Device from data.""" - return Device( - device_dict['hostname'], - device_dict['mac']) + return Device(device_dict["hostname"], device_dict["mac"]) class FortigateDeviceScanner(DeviceScanner): @@ -35,17 +33,16 @@ class FortigateDeviceScanner(DeviceScanner): """Initialize the scanner.""" self.last_results = {} self.success_init = False - self.connection = hass_data['fgt'] - self.devices = hass_data['devices'] + self.connection = hass_data["fgt"] + self.devices = hass_data["devices"] def get_results(self): """Get the results from the Fortigate.""" - results = self.connection.get( - DETECTED_DEVICES, "vdom=root")[1]['results'] + results = self.connection.get(DETECTED_DEVICES, "vdom=root")[1]["results"] ret = [] for result in results: - if 'hostname' not in result: + if "hostname" not in result: continue ret.append(result) @@ -65,9 +62,10 @@ class FortigateDeviceScanner(DeviceScanner): async def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - name = next(( - result.hostname for result in self.last_results - if result.mac == device), None) + name = next( + (result.hostname for result in self.last_results if result.mac == device), + None, + ) return name async def async_update_info(self): @@ -76,14 +74,12 @@ class FortigateDeviceScanner(DeviceScanner): hosts = self.get_results() - all_results = [_build_device(device) for device in hosts - if device['is_online']] + all_results = [_build_device(device) for device in hosts if device["is_online"]] # If the 'devices' configuration field is filled if self.devices is not None: last_results = [ - device for device in all_results - if device.hostname in self.devices + device for device in all_results if device.hostname in self.devices ] _LOGGER.debug(last_results) # If the 'devices' configuration field is not filled diff --git a/homeassistant/components/fortios/__init__.py b/homeassistant/components/fortios/__init__.py new file mode 100644 index 00000000000..873d6c00c65 --- /dev/null +++ b/homeassistant/components/fortios/__init__.py @@ -0,0 +1 @@ +"""Fortinet FortiOS components.""" diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py new file mode 100755 index 00000000000..51bce5429f6 --- /dev/null +++ b/homeassistant/components/fortios/device_tracker.py @@ -0,0 +1,101 @@ +""" +Support to use FortiOS device like FortiGate as device tracker. + +This component is part of the device_tracker platform. +""" +import logging + +import voluptuous as vol +from fortiosapi import FortiOSAPI + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import ( + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) +from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.const import CONF_VERIFY_SSL + +_LOGGER = logging.getLogger(__name__) +DEFAULT_VERIFY_SSL = False + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + } +) + + +def get_scanner(hass, config): + """Validate the configuration and return a FortiOSDeviceScanner.""" + host = config[DOMAIN][CONF_HOST] + verify_ssl = config[DOMAIN][CONF_VERIFY_SSL] + token = config[DOMAIN][CONF_TOKEN] + + fgt = FortiOSAPI() + + try: + fgt.tokenlogin(host, token, verify_ssl) + except ConnectionError as ex: + _LOGGER.error("ConnectionError to FortiOS API: %s", ex) + return None + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error("Failed to login to FortiOS API: %s", ex) + return None + + return FortiOSDeviceScanner(fgt) + + +class FortiOSDeviceScanner(DeviceScanner): + """This class queries a FortiOS unit for connected devices.""" + + def __init__(self, fgt) -> None: + """Initialize the scanner.""" + self._clients = {} + self._clients_json = {} + self._fgt = fgt + + def update(self): + """Update clients from the device.""" + clients_json = self._fgt.monitor("user/device/select", "") + self._clients_json = clients_json + + self._clients = [] + + if clients_json: + for client in clients_json["results"]: + if client["last_seen"] < 180: + self._clients.append(client["mac"].upper()) + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self.update() + return self._clients + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + _LOGGER.debug("Getting name of device %s", device) + + device = device.lower() + + data = self._clients_json + + if data == 0: + _LOGGER.error("No json results to get device names") + return None + + for client in data["results"]: + if client["mac"] == device: + try: + name = client["host"]["name"] + _LOGGER.debug("Getting device name=%s", name) + return name + except KeyError as kex: + _LOGGER.error("Name not found in client data: %s", kex) + return None + + return None diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json new file mode 100644 index 00000000000..a63d4b292ad --- /dev/null +++ b/homeassistant/components/fortios/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "fortios", + "name": "Home Assistant Device Tracker to support FortiOS", + "documentation": "https://www.home-assistant.io/components/fortios/", + "requirements": [ + "fortiosapi==0.10.8" + ], + "dependencies": [], + "codeowners": ["@kimfrellsen"] +} diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 3bb000380d7..37f792cec45 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -3,30 +3,30 @@ import logging import voluptuous as vol -from homeassistant.components.camera import ( - Camera, PLATFORM_SCHEMA, SUPPORT_STREAM) -from homeassistant.const import ( - CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM +from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_IP = 'ip' -CONF_RTSP_PORT = 'rtsp_port' +CONF_IP = "ip" +CONF_RTSP_PORT = "rtsp_port" -DEFAULT_NAME = 'Foscam Camera' +DEFAULT_NAME = "Foscam Camera" DEFAULT_PORT = 88 FOSCAM_COMM_ERROR = -8 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_IP): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_RTSP_PORT): cv.port -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_IP): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_RTSP_PORT): cv.port, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -51,14 +51,14 @@ class FoscamCam(Camera): self._motion_status = False self._foscam_session = FoscamCamera( - ip_address, port, self._username, self._password, verbose=False) + ip_address, port, self._username, self._password, verbose=False + ) self._rtsp_port = device_info.get(CONF_RTSP_PORT) if not self._rtsp_port: result, response = self._foscam_session.get_port_info() if result == 0: - self._rtsp_port = response.get('rtspPort') or \ - response.get('mediaPort') + self._rtsp_port = response.get("rtspPort") or response.get("mediaPort") def camera_image(self): """Return a still image response from the camera.""" @@ -80,11 +80,12 @@ class FoscamCam(Camera): async def stream_source(self): """Return the stream source.""" if self._rtsp_port: - return 'rtsp://{}:{}@{}:{}/videoMain'.format( + return "rtsp://{}:{}@{}:{}/videoMain".format( self._username, self._password, self._foscam_session.host, - self._rtsp_port) + self._rtsp_port, + ) return None @property diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py index dd834999888..7efb989e142 100644 --- a/homeassistant/components/foursquare/__init__.py +++ b/homeassistant/components/foursquare/__init__.py @@ -10,33 +10,40 @@ from homeassistant.components.http import HomeAssistantView _LOGGER = logging.getLogger(__name__) -CONF_PUSH_SECRET = 'push_secret' +CONF_PUSH_SECRET = "push_secret" -DOMAIN = 'foursquare' +DOMAIN = "foursquare" -EVENT_CHECKIN = 'foursquare.checkin' -EVENT_PUSH = 'foursquare.push' +EVENT_CHECKIN = "foursquare.checkin" +EVENT_PUSH = "foursquare.push" -SERVICE_CHECKIN = 'checkin' +SERVICE_CHECKIN = "checkin" -CHECKIN_SERVICE_SCHEMA = vol.Schema({ - vol.Optional('alt'): cv.string, - vol.Optional('altAcc'): cv.string, - vol.Optional('broadcast'): cv.string, - vol.Optional('eventId'): cv.string, - vol.Optional('ll'): cv.string, - vol.Optional('llAcc'): cv.string, - vol.Optional('mentions'): cv.string, - vol.Optional('shout'): cv.string, - vol.Required('venueId'): cv.string, -}) +CHECKIN_SERVICE_SCHEMA = vol.Schema( + { + vol.Optional("alt"): cv.string, + vol.Optional("altAcc"): cv.string, + vol.Optional("broadcast"): cv.string, + vol.Optional("eventId"): cv.string, + vol.Optional("ll"): cv.string, + vol.Optional("llAcc"): cv.string, + vol.Optional("mentions"): cv.string, + vol.Optional("shout"): cv.string, + vol.Required("venueId"): cv.string, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Required(CONF_PUSH_SECRET): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_PUSH_SECRET): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -45,22 +52,27 @@ def setup(hass, config): def checkin_user(call): """Check a user in on Swarm.""" - url = ("https://api.foursquare.com/v2/checkins/add" - "?oauth_token={}" - "&v=20160802" - "&m=swarm").format(config[CONF_ACCESS_TOKEN]) + url = ( + "https://api.foursquare.com/v2/checkins/add" + "?oauth_token={}" + "&v=20160802" + "&m=swarm" + ).format(config[CONF_ACCESS_TOKEN]) response = requests.post(url, data=call.data, timeout=10) if response.status_code not in (200, 201): _LOGGER.exception( "Error checking in user. Response %d: %s:", - response.status_code, response.reason) + response.status_code, + response.reason, + ) hass.bus.fire(EVENT_CHECKIN, response.text) # Register our service with Home Assistant. - hass.services.register(DOMAIN, 'checkin', checkin_user, - schema=CHECKIN_SERVICE_SCHEMA) + hass.services.register( + DOMAIN, "checkin", checkin_user, schema=CHECKIN_SERVICE_SCHEMA + ) hass.http.register_view(FoursquarePushReceiver(config[CONF_PUSH_SECRET])) @@ -83,15 +95,16 @@ class FoursquarePushReceiver(HomeAssistantView): try: data = await request.json() except ValueError: - return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) - secret = data.pop('secret', None) + secret = data.pop("secret", None) _LOGGER.debug("Received Foursquare push: %s", data) if self.push_secret != secret: - _LOGGER.error("Received Foursquare push with invalid" - "push secret: %s", secret) - return self.json_message('Incorrect secret', HTTP_BAD_REQUEST) + _LOGGER.error( + "Received Foursquare push with invalid" "push secret: %s", secret + ) + return self.json_message("Incorrect secret", HTTP_BAD_REQUEST) - request.app['hass'].bus.async_fire(EVENT_PUSH, data) + request.app["hass"].bus.async_fire(EVENT_PUSH, data) diff --git a/homeassistant/components/free_mobile/notify.py b/homeassistant/components/free_mobile/notify.py index c7dacd44019..5733e3c19c0 100644 --- a/homeassistant/components/free_mobile/notify.py +++ b/homeassistant/components/free_mobile/notify.py @@ -6,21 +6,18 @@ import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_ACCESS_TOKEN): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_ACCESS_TOKEN): cv.string} +) def get_service(hass, config, discovery_info=None): """Get the Free Mobile SMS notification service.""" - return FreeSMSNotificationService( - config[CONF_USERNAME], config[CONF_ACCESS_TOKEN]) + return FreeSMSNotificationService(config[CONF_USERNAME], config[CONF_ACCESS_TOKEN]) class FreeSMSNotificationService(BaseNotificationService): @@ -29,6 +26,7 @@ class FreeSMSNotificationService(BaseNotificationService): def __init__(self, username, access_token): """Initialize the service.""" from freesms import FreeClient + self.free_client = FreeClient(username, access_token) def send_message(self, message="", **kwargs): diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 2cd9f6b3572..0bffedd46dc 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -14,14 +14,16 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "freebox" DATA_FREEBOX = DOMAIN -FREEBOX_CONFIG_FILE = 'freebox.conf' +FREEBOX_CONFIG_FILE = "freebox.conf" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port} + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -30,8 +32,8 @@ async def async_setup(hass, config): async def discovery_dispatch(service, discovery_info): if conf is None: - host = discovery_info.get('properties', {}).get('api_domain') - port = discovery_info.get('properties', {}).get('https_port') + host = discovery_info.get("properties", {}).get("api_domain") + port = discovery_info.get("properties", {}).get("https_port") _LOGGER.info("Discovered Freebox server: %s:%s", host, port) await async_setup_freebox(hass, config, host, port) @@ -51,33 +53,29 @@ async def async_setup_freebox(hass, config, host, port): from aiofreepybox.exceptions import HttpRequestError app_desc = { - 'app_id': 'hass', - 'app_name': 'Home Assistant', - 'app_version': '0.65', - 'device_name': socket.gethostname() - } + "app_id": "hass", + "app_name": "Home Assistant", + "app_version": "0.65", + "device_name": socket.gethostname(), + } token_file = hass.config.path(FREEBOX_CONFIG_FILE) - api_version = 'v4' + api_version = "v4" - fbx = Freepybox( - app_desc=app_desc, - token_file=token_file, - api_version=api_version) + fbx = Freepybox(app_desc=app_desc, token_file=token_file, api_version=api_version) try: await fbx.open(host, port) except HttpRequestError: - _LOGGER.exception('Failed to connect to Freebox') + _LOGGER.exception("Failed to connect to Freebox") else: hass.data[DATA_FREEBOX] = fbx - hass.async_create_task(async_load_platform( - hass, 'sensor', DOMAIN, {}, config)) - hass.async_create_task(async_load_platform( - hass, 'device_tracker', DOMAIN, {}, config)) - hass.async_create_task(async_load_platform( - hass, 'switch', DOMAIN, {}, config)) + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) + hass.async_create_task( + async_load_platform(hass, "device_tracker", DOMAIN, {}, config) + ) + hass.async_create_task(async_load_platform(hass, "switch", DOMAIN, {}, config)) async def close_fbx(event): """Close Freebox connection on HA Stop.""" diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 40c1967f60f..63cf869990d 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -15,14 +15,16 @@ async def async_get_scanner(hass, config): await scanner.async_connect() return scanner if scanner.success_init else None -Device = namedtuple('Device', ['id', 'name', 'ip']) + +Device = namedtuple("Device", ["id", "name", "ip"]) def _build_device(device_dict): return Device( - device_dict['l2ident']['id'], - device_dict['primary_name'], - device_dict['l3connectivities'][0]['addr']) + device_dict["l2ident"]["id"], + device_dict["primary_name"], + device_dict["l3connectivities"][0]["addr"], + ) class FreeboxDeviceScanner(DeviceScanner): @@ -47,19 +49,17 @@ class FreeboxDeviceScanner(DeviceScanner): async def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - name = next(( - result.name for result in self.last_results - if result.id == device), None) + name = next( + (result.name for result in self.last_results if result.id == device), None + ) return name async def async_update_info(self): """Ensure the information from the Freebox router is up to date.""" - _LOGGER.debug('Checking Devices') + _LOGGER.debug("Checking Devices") hosts = await self.connection.lan.get_hosts_list() - last_results = [_build_device(device) - for device in hosts - if device['active']] + last_results = [_build_device(device) for device in hosts if device["active"]] self.last_results = last_results diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 8dcc5f54b2e..e8f96586300 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -8,8 +8,7 @@ from . import DATA_FREEBOX _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the sensors.""" fbx = hass.data[DATA_FREEBOX] async_add_entities([FbxRXSensor(fbx), FbxTXSensor(fbx)], True) @@ -18,7 +17,7 @@ async def async_setup_platform( class FbxSensor(Entity): """Representation of a freebox sensor.""" - _name = 'generic' + _name = "generic" def __init__(self, fbx): """Initialize the sensor.""" @@ -44,8 +43,8 @@ class FbxSensor(Entity): class FbxRXSensor(FbxSensor): """Update the Freebox RxSensor.""" - _name = 'Freebox download speed' - _unit = 'KB/s' + _name = "Freebox download speed" + _unit = "KB/s" @property def unit_of_measurement(self): @@ -56,14 +55,14 @@ class FbxRXSensor(FbxSensor): """Get the value from fetched datas.""" await super().async_update() if self._datas is not None: - self._state = round(self._datas['rate_down'] / 1000, 2) + self._state = round(self._datas["rate_down"] / 1000, 2) class FbxTXSensor(FbxSensor): """Update the Freebox TxSensor.""" - _name = 'Freebox upload speed' - _unit = 'KB/s' + _name = "Freebox upload speed" + _unit = "KB/s" @property def unit_of_measurement(self): @@ -74,4 +73,4 @@ class FbxTXSensor(FbxSensor): """Get the value from fetched datas.""" await super().async_update() if self._datas is not None: - self._state = round(self._datas['rate_up'] / 1000, 2) + self._state = round(self._datas["rate_up"] / 1000, 2) diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index e0c24d2b9f9..b6655c9634f 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -8,8 +8,7 @@ from . import DATA_FREEBOX _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the switch.""" fbx = hass.data[DATA_FREEBOX] async_add_entities([FbxWifiSwitch(fbx)], True) @@ -20,7 +19,7 @@ class FbxWifiSwitch(SwitchDevice): def __init__(self, fbx): """Initilize the Wifi switch.""" - self._name = 'Freebox WiFi' + self._name = "Freebox WiFi" self._state = None self._fbx = fbx @@ -42,9 +41,11 @@ class FbxWifiSwitch(SwitchDevice): try: await self._fbx.wifi.set_global_config(wifi_config) except InsufficientPermissionsError: - _LOGGER.warning('Home Assistant does not have permissions to' - ' modify the Freebox settings. Please refer' - ' to documentation.') + _LOGGER.warning( + "Home Assistant does not have permissions to" + " modify the Freebox settings. Please refer" + " to documentation." + ) async def async_turn_on(self, **kwargs): """Turn the switch on.""" @@ -57,5 +58,5 @@ class FbxWifiSwitch(SwitchDevice): async def async_update(self): """Get the state and update it.""" datas = await self._fbx.wifi.get_global_config() - active = datas['enabled'] + active = datas["enabled"] self._state = bool(active) diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index 6125057ca33..30f80280c1f 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -8,27 +8,31 @@ import async_timeout import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL, CONF_URL -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL, CONF_URL _LOGGER = logging.getLogger(__name__) -DOMAIN = 'freedns' +DOMAIN = "freedns" DEFAULT_INTERVAL = timedelta(minutes=10) TIMEOUT = 10 -UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php' +UPDATE_URL = "https://freedns.afraid.org/dynamic/update.php" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Exclusive(CONF_URL, DOMAIN): cv.string, - vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): - vol.All(cv.time_period, cv.positive_timedelta), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Exclusive(CONF_URL, DOMAIN): cv.string, + vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): vol.All( + cv.time_period, cv.positive_timedelta + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -40,8 +44,7 @@ async def async_setup(hass, config): session = hass.helpers.aiohttp_client.async_get_clientsession() - result = await _update_freedns( - hass, session, url, auth_token) + result = await _update_freedns(hass, session, url, auth_token) if result is False: return False @@ -51,7 +54,8 @@ async def async_setup(hass, config): await _update_freedns(hass, session, url, auth_token) hass.helpers.event.async_track_time_interval( - update_domain_callback, update_interval) + update_domain_callback, update_interval + ) return True diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index fc9f65633ff..f34cda67ad9 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -5,18 +5,23 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME _LOGGER = logging.getLogger(__name__) -CONF_DEFAULT_IP = '169.254.1.1' # This IP is valid for all FRITZ!Box routers. +CONF_DEFAULT_IP = "169.254.1.1" # This IP is valid for all FRITZ!Box routers. -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, - vol.Optional(CONF_PASSWORD, default='admin'): cv.string, - vol.Optional(CONF_USERNAME, default=''): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, + vol.Optional(CONF_PASSWORD, default="admin"): cv.string, + vol.Optional(CONF_USERNAME, default=""): cv.string, + } +) def get_scanner(hass, config): @@ -41,7 +46,8 @@ class FritzBoxScanner(DeviceScanner): # Establish a connection to the FRITZ!Box. try: self.fritz_box = fc.FritzHosts( - address=self.host, user=self.username, password=self.password) + address=self.host, user=self.username, password=self.password + ) except (ValueError, TypeError): self.fritz_box = None @@ -51,27 +57,25 @@ class FritzBoxScanner(DeviceScanner): self.success_init = False if self.success_init: - _LOGGER.info("Successfully connected to %s", - self.fritz_box.modelname) + _LOGGER.info("Successfully connected to %s", self.fritz_box.modelname) self._update_info() else: - _LOGGER.error("Failed to establish connection to FRITZ!Box " - "with IP: %s", self.host) + _LOGGER.error( + "Failed to establish connection to FRITZ!Box " "with IP: %s", self.host + ) def scan_devices(self): """Scan for new devices and return a list of found device ids.""" self._update_info() active_hosts = [] for known_host in self.last_results: - if known_host['status'] == '1' and known_host.get('mac'): - active_hosts.append(known_host['mac']) + if known_host["status"] == "1" and known_host.get("mac"): + active_hosts.append(known_host["mac"]) return active_hosts def get_device_name(self, device): """Return the name of the given device or None if is not known.""" - ret = self.fritz_box.get_specific_host_entry(device).get( - 'NewHostName' - ) + ret = self.fritz_box.get_specific_host_entry(device).get("NewHostName") if ret == {}: return None return ret diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 610c6874140..a053bc6c7ca 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -5,36 +5,49 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP) + CONF_DEVICES, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers import discovery _LOGGER = logging.getLogger(__name__) -SUPPORTED_DOMAINS = ['binary_sensor', 'climate', 'switch', 'sensor'] +SUPPORTED_DOMAINS = ["binary_sensor", "climate", "switch", "sensor"] -DOMAIN = 'fritzbox' +DOMAIN = "fritzbox" -ATTR_STATE_BATTERY_LOW = 'battery_low' -ATTR_STATE_DEVICE_LOCKED = 'device_locked' -ATTR_STATE_HOLIDAY_MODE = 'holiday_mode' -ATTR_STATE_LOCKED = 'locked' -ATTR_STATE_SUMMER_MODE = 'summer_mode' -ATTR_STATE_WINDOW_OPEN = 'window_open' +ATTR_STATE_BATTERY_LOW = "battery_low" +ATTR_STATE_DEVICE_LOCKED = "device_locked" +ATTR_STATE_HOLIDAY_MODE = "holiday_mode" +ATTR_STATE_LOCKED = "locked" +ATTR_STATE_SUMMER_MODE = "summer_mode" +ATTR_STATE_WINDOW_OPEN = "window_open" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICES): - vol.All(cv.ensure_list, [ - vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - }), - ]), - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_DEVICES): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + } + ) + ], + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -48,14 +61,12 @@ def setup(hass, config): host = device.get(CONF_HOST) username = device.get(CONF_USERNAME) password = device.get(CONF_PASSWORD) - fritzbox = Fritzhome(host=host, user=username, - password=password) + fritzbox = Fritzhome(host=host, user=username, password=password) try: fritzbox.login() _LOGGER.info("Connected to device %s", device) except LoginError: - _LOGGER.warning("Login to Fritz!Box %s as %s failed", - host, username) + _LOGGER.warning("Login to Fritz!Box %s as %s failed", host, username) continue fritz_list.append(fritzbox) diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index a763a3b3b0e..3d8d676d1d0 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -40,7 +40,7 @@ class FritzboxBinarySensor(BinarySensorDevice): @property def device_class(self): """Return the class of this sensor.""" - return 'window' + return "window" @property def is_on(self): diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index b4bb32e5655..40c16930206 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -5,16 +5,30 @@ import requests from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - ATTR_HVAC_MODE, HVAC_MODE_HEAT, PRESET_ECO, PRESET_COMFORT, - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_OFF, SUPPORT_PRESET_MODE) + ATTR_HVAC_MODE, + HVAC_MODE_HEAT, + PRESET_ECO, + PRESET_COMFORT, + SUPPORT_TARGET_TEMPERATURE, + HVAC_MODE_OFF, + SUPPORT_PRESET_MODE, +) from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES, - TEMP_CELSIUS) + ATTR_BATTERY_LEVEL, + ATTR_TEMPERATURE, + PRECISION_HALVES, + TEMP_CELSIUS, +) from . import ( - ATTR_STATE_BATTERY_LOW, ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_HOLIDAY_MODE, - ATTR_STATE_LOCKED, ATTR_STATE_SUMMER_MODE, ATTR_STATE_WINDOW_OPEN, - DOMAIN as FRITZBOX_DOMAIN) + ATTR_STATE_BATTERY_LOW, + ATTR_STATE_DEVICE_LOCKED, + ATTR_STATE_HOLIDAY_MODE, + ATTR_STATE_LOCKED, + ATTR_STATE_SUMMER_MODE, + ATTR_STATE_WINDOW_OPEN, + DOMAIN as FRITZBOX_DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -25,7 +39,7 @@ OPERATION_LIST = [HVAC_MODE_HEAT, HVAC_MODE_OFF] MIN_TEMPERATURE = 8 MAX_TEMPERATURE = 28 -PRESET_MANUAL = 'manual' +PRESET_MANUAL = "manual" # special temperatures for on/off in Fritz!Box API (modified by pyfritzhome) ON_API_TEMPERATURE = 127.0 @@ -93,9 +107,10 @@ class FritzboxThermostat(ClimateDevice): @property def target_temperature(self): """Return the temperature we try to reach.""" - if self._target_temperature in (ON_API_TEMPERATURE, - OFF_API_TEMPERATURE): - return None + if self._target_temperature == ON_API_TEMPERATURE: + return ON_REPORT_SET_TEMPERATURE + if self._target_temperature == OFF_API_TEMPERATURE: + return OFF_REPORT_SET_TEMPERATURE return self._target_temperature def set_temperature(self, **kwargs): @@ -110,7 +125,10 @@ class FritzboxThermostat(ClimateDevice): @property def hvac_mode(self): """Return the current operation mode.""" - if self._target_temperature == OFF_REPORT_SET_TEMPERATURE: + if ( + self._target_temperature == OFF_REPORT_SET_TEMPERATURE + or self._target_temperature == OFF_API_TEMPERATURE + ): return HVAC_MODE_OFF return HVAC_MODE_HEAT diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 123d8835318..4454ea35bbe 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -6,8 +6,7 @@ import requests from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from . import ( - ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, DOMAIN as FRITZBOX_DOMAIN) +from . import ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, DOMAIN as FRITZBOX_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,9 +20,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for fritz in fritz_list: device_list = fritz.get_devices() for device in device_list: - if (device.has_temperature_sensor - and not device.has_switch - and not device.has_thermostat): + if ( + device.has_temperature_sensor + and not device.has_switch + and not device.has_thermostat + ): devices.append(FritzBoxTempSensor(device, fritz)) add_entities(devices) diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index ae1219cefda..c51c952ab06 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -4,19 +4,17 @@ import logging import requests from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ( - ATTR_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS) +from homeassistant.const import ATTR_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS -from . import ( - ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, DOMAIN as FRITZBOX_DOMAIN) +from . import ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, DOMAIN as FRITZBOX_DOMAIN _LOGGER = logging.getLogger(__name__) -ATTR_TOTAL_CONSUMPTION = 'total_consumption' -ATTR_TOTAL_CONSUMPTION_UNIT = 'total_consumption_unit' +ATTR_TOTAL_CONSUMPTION = "total_consumption" +ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR -ATTR_TEMPERATURE_UNIT = 'temperature_unit' +ATTR_TEMPERATURE_UNIT = "temperature_unit" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -81,15 +79,16 @@ class FritzboxSwitch(SwitchDevice): if self._device.has_powermeter: attrs[ATTR_TOTAL_CONSUMPTION] = "{:.3f}".format( - (self._device.energy or 0.0) / 1000) - attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = \ - ATTR_TOTAL_CONSUMPTION_UNIT_VALUE + (self._device.energy or 0.0) / 1000 + ) + attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = ATTR_TOTAL_CONSUMPTION_UNIT_VALUE if self._device.has_temperature_sensor: - attrs[ATTR_TEMPERATURE] = \ - str(self.hass.config.units.temperature( - self._device.temperature, TEMP_CELSIUS)) - attrs[ATTR_TEMPERATURE_UNIT] = \ - self.hass.config.units.temperature_unit + attrs[ATTR_TEMPERATURE] = str( + self.hass.config.units.temperature( + self._device.temperature, TEMP_CELSIUS + ) + ) + attrs[ATTR_TEMPERATURE_UNIT] = self.hass.config.units.temperature_unit return attrs @property diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 95c0879996f..4dada44f4e5 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -9,44 +9,50 @@ import re import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME, - CONF_PASSWORD, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -CONF_PHONEBOOK = 'phonebook' -CONF_PREFIXES = 'prefixes' +CONF_PHONEBOOK = "phonebook" +CONF_PREFIXES = "prefixes" -DEFAULT_HOST = '169.254.1.1' # IP valid for all Fritz!Box routers -DEFAULT_NAME = 'Phone' +DEFAULT_HOST = "169.254.1.1" # IP valid for all Fritz!Box routers +DEFAULT_NAME = "Phone" DEFAULT_PORT = 1012 INTERVAL_RECONNECT = 60 -VALUE_CALL = 'dialing' -VALUE_CONNECT = 'talking' -VALUE_DEFAULT = 'idle' -VALUE_DISCONNECT = 'idle' -VALUE_RING = 'ringing' +VALUE_CALL = "dialing" +VALUE_CONNECT = "talking" +VALUE_DEFAULT = "idle" +VALUE_DISCONNECT = "idle" +VALUE_RING = "ringing" # Return cached results if phonebook was downloaded less then this time ago. MIN_TIME_PHONEBOOK_UPDATE = datetime.timedelta(hours=6) SCAN_INTERVAL = datetime.timedelta(hours=3) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_PASSWORD, default='admin'): cv.string, - vol.Optional(CONF_USERNAME, default=''): cv.string, - vol.Optional(CONF_PHONEBOOK, default=0): cv.positive_int, - vol.Optional(CONF_PREFIXES, default=[]): - vol.All(cv.ensure_list, [cv.string]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PASSWORD, default="admin"): cv.string, + vol.Optional(CONF_USERNAME, default=""): cv.string, + vol.Optional(CONF_PHONEBOOK, default=0): cv.positive_int, + vol.Optional(CONF_PREFIXES, default=[]): vol.All(cv.ensure_list, [cv.string]), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -61,12 +67,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: phonebook = FritzBoxPhonebook( - host=host, port=port, username=username, password=password, - phonebook_id=phonebook_id, prefixes=prefixes) + host=host, + port=port, + username=username, + password=password, + phonebook_id=phonebook_id, + prefixes=prefixes, + ) except: # noqa: E722 pylint: disable=bare-except phonebook = None - _LOGGER.warning("Phonebook with ID %s not found on Fritz!Box", - phonebook_id) + _LOGGER.warning("Phonebook with ID %s not found on Fritz!Box", phonebook_id) sensor = FritzBoxCallSensor(name=name, phonebook=phonebook) @@ -78,10 +88,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def _stop_listener(_event): monitor.stopped.set() - hass.bus.listen_once( - EVENT_HOMEASSISTANT_STOP, - _stop_listener - ) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_listener) return monitor.sock is not None @@ -127,7 +134,7 @@ class FritzBoxCallSensor(Entity): def number_to_name(self, number): """Return a name for a given phone number.""" if self.phonebook is None: - return 'unknown' + return "unknown" return self.phonebook.get_name(number) def update(self): @@ -149,7 +156,7 @@ class FritzBoxCallMonitor: def connect(self): """Connect to the Fritz!Box.""" - _LOGGER.debug('Setting up socket...') + _LOGGER.debug("Setting up socket...") self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(10) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) @@ -158,12 +165,13 @@ class FritzBoxCallMonitor: threading.Thread(target=self._listen).start() except socket.error as err: self.sock = None - _LOGGER.error("Cannot connect to %s on port %s: %s", - self.host, self.port, err) + _LOGGER.error( + "Cannot connect to %s on port %s: %s", self.host, self.port, err + ) def _listen(self): """Listen to incoming or outgoing calls.""" - _LOGGER.debug('Connection established, waiting for response...') + _LOGGER.debug("Connection established, waiting for response...") while not self.stopped.isSet(): try: response = self.sock.recv(2048) @@ -171,12 +179,12 @@ class FritzBoxCallMonitor: # if no response after 10 seconds, just recv again continue response = str(response, "utf-8") - _LOGGER.debug('Received %s', response) + _LOGGER.debug("Received %s", response) if not response: # if the response is empty, the connection has been lost. # try to reconnect - _LOGGER.warning('Connection lost, reconnecting...') + _LOGGER.warning("Connection lost, reconnecting...") self.sock = None while self.sock is None: self.connect() @@ -194,20 +202,24 @@ class FritzBoxCallMonitor: isotime = datetime.datetime.strptime(line[0], df_in).strftime(df_out) if line[1] == "RING": self._sensor.set_state(VALUE_RING) - att = {"type": "incoming", - "from": line[3], - "to": line[4], - "device": line[5], - "initiated": isotime} + att = { + "type": "incoming", + "from": line[3], + "to": line[4], + "device": line[5], + "initiated": isotime, + } att["from_name"] = self._sensor.number_to_name(att["from"]) self._sensor.set_attributes(att) elif line[1] == "CALL": self._sensor.set_state(VALUE_CALL) - att = {"type": "outgoing", - "from": line[4], - "to": line[5], - "device": line[6], - "initiated": isotime} + att = { + "type": "outgoing", + "from": line[4], + "to": line[5], + "device": line[6], + "initiated": isotime, + } att["to_name"] = self._sensor.number_to_name(att["to"]) self._sensor.set_attributes(att) elif line[1] == "CONNECT": @@ -225,8 +237,7 @@ class FritzBoxCallMonitor: class FritzBoxPhonebook: """This connects to a FritzBox router and downloads its phone book.""" - def __init__(self, host, port, username, password, - phonebook_id=0, prefixes=None): + def __init__(self, host, port, username, password, phonebook_id=0, prefixes=None): """Initialize the class.""" self.host = host self.username = username @@ -238,9 +249,11 @@ class FritzBoxPhonebook: self.prefixes = prefixes or [] import fritzconnection as fc # pylint: disable=import-error + # Establish a connection to the FRITZ!Box. self.fph = fc.FritzPhonebook( - address=self.host, user=self.username, password=self.password) + address=self.host, user=self.username, password=self.password + ) if self.phonebook_id not in self.fph.list_phonebooks: raise ValueError("Phonebook with this ID not found.") @@ -251,16 +264,18 @@ class FritzBoxPhonebook: def update_phonebook(self): """Update the phone book dictionary.""" self.phonebook_dict = self.fph.get_all_names(self.phonebook_id) - self.number_dict = {re.sub(r'[^\d\+]', '', nr): name - for name, nrs in self.phonebook_dict.items() - for nr in nrs} + self.number_dict = { + re.sub(r"[^\d\+]", "", nr): name + for name, nrs in self.phonebook_dict.items() + for nr in nrs + } _LOGGER.info("Fritz!Box phone book successfully updated") def get_name(self, number): """Return a name for a given phone number.""" - number = re.sub(r'[^\d\+]', '', str(number)) + number = re.sub(r"[^\d\+]", "", str(number)) if self.number_dict is None: - return 'unknown' + return "unknown" try: return self.number_dict[number] except KeyError: @@ -272,7 +287,7 @@ class FritzBoxPhonebook: except KeyError: pass try: - return self.number_dict[prefix + number.lstrip('0')] + return self.number_dict[prefix + number.lstrip("0")] except KeyError: pass - return 'unknown' + return "unknown" diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py index ec8e38bb24b..9d07e7a8055 100644 --- a/homeassistant/components/fritzbox_netmonitor/sensor.py +++ b/homeassistant/components/fritzbox_netmonitor/sensor.py @@ -6,39 +6,41 @@ from requests.exceptions import RequestException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_UNAVAILABLE) +from homeassistant.const import CONF_NAME, CONF_HOST, STATE_UNAVAILABLE from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -CONF_DEFAULT_NAME = 'fritz_netmonitor' -CONF_DEFAULT_IP = '169.254.1.1' # This IP is valid for all FRITZ!Box routers. +CONF_DEFAULT_NAME = "fritz_netmonitor" +CONF_DEFAULT_IP = "169.254.1.1" # This IP is valid for all FRITZ!Box routers. -ATTR_BYTES_RECEIVED = 'bytes_received' -ATTR_BYTES_SENT = 'bytes_sent' -ATTR_TRANSMISSION_RATE_UP = 'transmission_rate_up' -ATTR_TRANSMISSION_RATE_DOWN = 'transmission_rate_down' -ATTR_EXTERNAL_IP = 'external_ip' -ATTR_IS_CONNECTED = 'is_connected' -ATTR_IS_LINKED = 'is_linked' -ATTR_MAX_BYTE_RATE_DOWN = 'max_byte_rate_down' -ATTR_MAX_BYTE_RATE_UP = 'max_byte_rate_up' -ATTR_UPTIME = 'uptime' -ATTR_WAN_ACCESS_TYPE = 'wan_access_type' +ATTR_BYTES_RECEIVED = "bytes_received" +ATTR_BYTES_SENT = "bytes_sent" +ATTR_TRANSMISSION_RATE_UP = "transmission_rate_up" +ATTR_TRANSMISSION_RATE_DOWN = "transmission_rate_down" +ATTR_EXTERNAL_IP = "external_ip" +ATTR_IS_CONNECTED = "is_connected" +ATTR_IS_LINKED = "is_linked" +ATTR_MAX_BYTE_RATE_DOWN = "max_byte_rate_down" +ATTR_MAX_BYTE_RATE_UP = "max_byte_rate_up" +ATTR_UPTIME = "uptime" +ATTR_WAN_ACCESS_TYPE = "wan_access_type" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -STATE_ONLINE = 'online' -STATE_OFFLINE = 'offline' +STATE_ONLINE = "online" +STATE_OFFLINE = "offline" -ICON = 'mdi:web' +ICON = "mdi:web" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=CONF_DEFAULT_NAME): cv.string, - vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=CONF_DEFAULT_NAME): cv.string, + vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/fritzdect/switch.py b/homeassistant/components/fritzdect/switch.py index d3cd00a73f5..22a44a11133 100644 --- a/homeassistant/components/fritzdect/switch.py +++ b/homeassistant/components/fritzdect/switch.py @@ -5,32 +5,39 @@ from requests.exceptions import RequestException, HTTPError import voluptuous as vol -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, POWER_WATT, ENERGY_KILO_WATT_HOUR) + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + POWER_WATT, + ENERGY_KILO_WATT_HOUR, +) import homeassistant.helpers.config_validation as cv from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE _LOGGER = logging.getLogger(__name__) # Standard Fritz Box IP -DEFAULT_HOST = 'fritz.box' +DEFAULT_HOST = "fritz.box" -ATTR_CURRENT_CONSUMPTION = 'current_consumption' -ATTR_CURRENT_CONSUMPTION_UNIT = 'current_consumption_unit' +ATTR_CURRENT_CONSUMPTION = "current_consumption" +ATTR_CURRENT_CONSUMPTION_UNIT = "current_consumption_unit" ATTR_CURRENT_CONSUMPTION_UNIT_VALUE = POWER_WATT -ATTR_TOTAL_CONSUMPTION = 'total_consumption' -ATTR_TOTAL_CONSUMPTION_UNIT = 'total_consumption_unit' +ATTR_TOTAL_CONSUMPTION = "total_consumption" +ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR -ATTR_TEMPERATURE_UNIT = 'temperature_unit' +ATTR_TEMPERATURE_UNIT = "temperature_unit" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -77,24 +84,27 @@ class FritzDectSwitch(SwitchDevice): """Return the state attributes of the device.""" attrs = {} - if self.data.has_powermeter and \ - self.data.current_consumption is not None and \ - self.data.total_consumption is not None: + if ( + self.data.has_powermeter + and self.data.current_consumption is not None + and self.data.total_consumption is not None + ): attrs[ATTR_CURRENT_CONSUMPTION] = "{:.1f}".format( - self.data.current_consumption) + self.data.current_consumption + ) attrs[ATTR_CURRENT_CONSUMPTION_UNIT] = "{}".format( - ATTR_CURRENT_CONSUMPTION_UNIT_VALUE) - attrs[ATTR_TOTAL_CONSUMPTION] = "{:.3f}".format( - self.data.total_consumption) + ATTR_CURRENT_CONSUMPTION_UNIT_VALUE + ) + attrs[ATTR_TOTAL_CONSUMPTION] = "{:.3f}".format(self.data.total_consumption) attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = "{}".format( - ATTR_TOTAL_CONSUMPTION_UNIT_VALUE) + ATTR_TOTAL_CONSUMPTION_UNIT_VALUE + ) - if self.data.has_temperature and \ - self.data.temperature is not None: + if self.data.has_temperature and self.data.temperature is not None: attrs[ATTR_TEMPERATURE] = "{}".format( - self.units.temperature(self.data.temperature, TEMP_CELSIUS)) - attrs[ATTR_TEMPERATURE_UNIT] = "{}".format( - self.units.temperature_unit) + self.units.temperature(self.data.temperature, TEMP_CELSIUS) + ) + attrs[ATTR_TEMPERATURE_UNIT] = "{}".format(self.units.temperature_unit) return attrs @property @@ -186,7 +196,7 @@ class FritzDectSwitchData: self.temperature = None self.current_consumption = None self.total_consumption = None - raise Exception('Request to actor registry failed') + raise Exception("Request to actor registry failed") if actor is None: _LOGGER.error("Actor could not be found") @@ -194,7 +204,7 @@ class FritzDectSwitchData: self.temperature = None self.current_consumption = None self.total_consumption = None - raise Exception('Actor could not be found') + raise Exception("Actor could not be found") try: self.state = actor.get_state() @@ -206,7 +216,7 @@ class FritzDectSwitchData: self.temperature = None self.current_consumption = None self.total_consumption = None - raise Exception('Request to actor failed') + raise Exception("Request to actor failed") self.temperature = actor.temperature self.has_switch = actor.has_switch diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 07d2e984f23..ff0694afaab 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -1,31 +1,40 @@ """Support for Fronius devices.""" import copy +from datetime import timedelta import logging import voluptuous as vol from pyfronius import Fronius from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_RESOURCE, CONF_SENSOR_TYPE, CONF_DEVICE, - CONF_MONITORED_CONDITIONS) +from homeassistant.const import ( + CONF_RESOURCE, + CONF_SENSOR_TYPE, + CONF_DEVICE, + CONF_MONITORED_CONDITIONS, + CONF_SCAN_INTERVAL, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval + _LOGGER = logging.getLogger(__name__) -CONF_SCOPE = 'scope' +CONF_SCOPE = "scope" -TYPE_INVERTER = 'inverter' -TYPE_STORAGE = 'storage' -TYPE_METER = 'meter' -TYPE_POWER_FLOW = 'power_flow' -SCOPE_DEVICE = 'device' -SCOPE_SYSTEM = 'system' +TYPE_INVERTER = "inverter" +TYPE_STORAGE = "storage" +TYPE_METER = "meter" +TYPE_POWER_FLOW = "power_flow" +SCOPE_DEVICE = "device" +SCOPE_SYSTEM = "system" DEFAULT_SCOPE = SCOPE_DEVICE DEFAULT_DEVICE = 0 DEFAULT_INVERTER = 1 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) SENSOR_TYPES = [TYPE_INVERTER, TYPE_STORAGE, TYPE_METER, TYPE_POWER_FLOW] SCOPE_TYPES = [SCOPE_DEVICE, SCOPE_SYSTEM] @@ -43,72 +52,95 @@ def _device_id_validator(config): return config -PLATFORM_SCHEMA = vol.Schema(vol.All(PLATFORM_SCHEMA.extend({ - vol.Required(CONF_RESOURCE): - cv.url, - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All( - cv.ensure_list, - [{ - vol.Required(CONF_SENSOR_TYPE): vol.In(SENSOR_TYPES), - vol.Optional(CONF_SCOPE, default=DEFAULT_SCOPE): - vol.In(SCOPE_TYPES), - vol.Optional(CONF_DEVICE): - vol.All(vol.Coerce(int), vol.Range(min=0)) - }] - ) -}), _device_id_validator)) +PLATFORM_SCHEMA = vol.Schema( + vol.All( + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_RESOURCE): cv.url, + vol.Required(CONF_MONITORED_CONDITIONS): vol.All( + cv.ensure_list, + [ + { + vol.Required(CONF_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Optional(CONF_SCOPE, default=DEFAULT_SCOPE): vol.In( + SCOPE_TYPES + ), + vol.Optional(CONF_DEVICE): vol.All( + vol.Coerce(int), vol.Range(min=0) + ), + } + ], + ), + } + ), + _device_id_validator, + ) +) -async def async_setup_platform(hass, - config, - async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up of Fronius platform.""" session = async_get_clientsession(hass) fronius = Fronius(session, config[CONF_RESOURCE]) - sensors = [] + scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + adapters = [] + # Creates all adapters for monitored conditions for condition in config[CONF_MONITORED_CONDITIONS]: device = condition[CONF_DEVICE] - name = "Fronius {} {} {}".format( - condition[CONF_SENSOR_TYPE].replace('_', ' ').capitalize(), - device, - config[CONF_RESOURCE], - ) sensor_type = condition[CONF_SENSOR_TYPE] scope = condition[CONF_SCOPE] + name = "Fronius {} {} {}".format( + condition[CONF_SENSOR_TYPE].replace("_", " ").capitalize(), + device if scope == SCOPE_DEVICE else SCOPE_SYSTEM, + config[CONF_RESOURCE], + ) if sensor_type == TYPE_INVERTER: if scope == SCOPE_SYSTEM: - sensor_cls = FroniusInverterSystem + adapter_cls = FroniusInverterSystem else: - sensor_cls = FroniusInverterDevice + adapter_cls = FroniusInverterDevice elif sensor_type == TYPE_METER: if scope == SCOPE_SYSTEM: - sensor_cls = FroniusMeterSystem + adapter_cls = FroniusMeterSystem else: - sensor_cls = FroniusMeterDevice + adapter_cls = FroniusMeterDevice elif sensor_type == TYPE_POWER_FLOW: - sensor_cls = FroniusPowerFlow + adapter_cls = FroniusPowerFlow else: - sensor_cls = FroniusStorage + adapter_cls = FroniusStorage - sensors.append(sensor_cls(fronius, name, device)) + adapters.append(adapter_cls(fronius, name, device, async_add_entities)) - async_add_entities(sensors, True) + # Creates a lamdba that fetches an update when called + def adapter_data_fetcher(data_adapter): + async def fetch_data(*_): + await data_adapter.async_update() + + return fetch_data + + # Set up the fetching in a fixed interval for each adapter + for adapter in adapters: + fetch = adapter_data_fetcher(adapter) + # fetch data once at set-up + await fetch() + async_track_time_interval(hass, fetch, scan_interval) -class FroniusSensor(Entity): - """The Fronius sensor implementation.""" +class FroniusAdapter: + """The Fronius sensor fetching component.""" - def __init__(self, data, name, device): + def __init__(self, bridge, name, device, add_entities): """Initialize the sensor.""" - self.data = data + self.bridge = bridge self._name = name self._device = device - self._state = None - self._attributes = {} + self._fetched = {} + + self.sensors = set() + self._registered_sensors = set() + self._add_entities = add_entities @property def name(self): @@ -116,14 +148,9 @@ class FroniusSensor(Entity): return self._name @property - def state(self): - """Return the current state.""" - return self._state - - @property - def device_state_attributes(self): + def data(self): """Return the state attributes.""" - return self._attributes + return self._fetched async def async_update(self): """Retrieve and update latest state.""" @@ -133,65 +160,134 @@ class FroniusSensor(Entity): except ConnectionError: _LOGGER.error("Failed to update: connection error") except ValueError: - _LOGGER.error("Failed to update: invalid response returned." - "Maybe the configured device is not supported") + _LOGGER.error( + "Failed to update: invalid response returned." + "Maybe the configured device is not supported" + ) - if values: - self._state = values['status']['Code'] - attributes = {} - for key in values: - if 'value' in values[key]: - attributes[key] = values[key].get('value', 0) - self._attributes = attributes + if not values: + return + attributes = self._fetched + # Copy data of current fronius device + for key, entry in values.items(): + # If the data is directly a sensor + if "value" in entry: + attributes[key] = entry + self._fetched = attributes + + # Add discovered value fields as sensors + # because some fields are only sent temporarily + new_sensors = [] + for key in attributes: + if key not in self.sensors: + self.sensors.add(key) + _LOGGER.info("Discovered %s, adding as sensor", key) + new_sensors.append(FroniusTemplateSensor(self, key)) + self._add_entities(new_sensors, True) + + # Schedule an update for all included sensors + for sensor in self._registered_sensors: + sensor.async_schedule_update_ha_state(True) async def _update(self): """Return values of interest.""" pass + async def register(self, sensor): + """Register child sensor for update subscriptions.""" + self._registered_sensors.add(sensor) -class FroniusInverterSystem(FroniusSensor): - """Sensor for the fronius inverter with system scope.""" + +class FroniusInverterSystem(FroniusAdapter): + """Adapter for the fronius inverter with system scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_system_inverter_data() + return await self.bridge.current_system_inverter_data() -class FroniusInverterDevice(FroniusSensor): - """Sensor for the fronius inverter with device scope.""" +class FroniusInverterDevice(FroniusAdapter): + """Adapter for the fronius inverter with device scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_inverter_data(self._device) + return await self.bridge.current_inverter_data(self._device) -class FroniusStorage(FroniusSensor): - """Sensor for the fronius battery storage.""" +class FroniusStorage(FroniusAdapter): + """Adapter for the fronius battery storage.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_storage_data(self._device) + return await self.bridge.current_storage_data(self._device) -class FroniusMeterSystem(FroniusSensor): - """Sensor for the fronius meter with system scope.""" +class FroniusMeterSystem(FroniusAdapter): + """Adapter for the fronius meter with system scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_system_meter_data() + return await self.bridge.current_system_meter_data() -class FroniusMeterDevice(FroniusSensor): - """Sensor for the fronius meter with device scope.""" +class FroniusMeterDevice(FroniusAdapter): + """Adapter for the fronius meter with device scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_meter_data(self._device) + return await self.bridge.current_meter_data(self._device) -class FroniusPowerFlow(FroniusSensor): - """Sensor for the fronius power flow.""" +class FroniusPowerFlow(FroniusAdapter): + """Adapter for the fronius power flow.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_power_flow() + return await self.bridge.current_power_flow() + + +class FroniusTemplateSensor(Entity): + """Sensor for the single values (e.g. pv power, ac power).""" + + def __init__(self, parent: FroniusAdapter, name): + """Initialize a singular value sensor.""" + self._name = name + self.parent = parent + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the sensor.""" + return "{} {}".format( + self._name.replace("_", " ").capitalize(), self.parent.name + ) + + @property + def state(self): + """Return the current state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def should_poll(self): + """Device should not be polled, returns False.""" + return False + + async def async_update(self): + """Update the internal state.""" + state = self.parent.data.get(self._name) + self._state = state.get("value") + self._unit = state.get("unit") + + async def async_added_to_hass(self): + """Register at parent component for updates.""" + await self.parent.register(self) + + def __hash__(self): + """Hash sensor by hashing its name.""" + return hash(self.name) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d311baf8ae1..b769d6b9aea 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -28,89 +28,93 @@ mimetypes.add_type("text/css", ".css") mimetypes.add_type("application/javascript", ".js") -DOMAIN = 'frontend' -CONF_THEMES = 'themes' -CONF_EXTRA_HTML_URL = 'extra_html_url' -CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5' -CONF_EXTRA_MODULE_URL = 'extra_module_url' -CONF_EXTRA_JS_URL_ES5 = 'extra_js_url_es5' -CONF_FRONTEND_REPO = 'development_repo' -CONF_JS_VERSION = 'javascript_version' -EVENT_PANELS_UPDATED = 'panels_updated' +DOMAIN = "frontend" +CONF_THEMES = "themes" +CONF_EXTRA_HTML_URL = "extra_html_url" +CONF_EXTRA_HTML_URL_ES5 = "extra_html_url_es5" +CONF_EXTRA_MODULE_URL = "extra_module_url" +CONF_EXTRA_JS_URL_ES5 = "extra_js_url_es5" +CONF_FRONTEND_REPO = "development_repo" +CONF_JS_VERSION = "javascript_version" +EVENT_PANELS_UPDATED = "panels_updated" -DEFAULT_THEME_COLOR = '#03A9F4' +DEFAULT_THEME_COLOR = "#03A9F4" MANIFEST_JSON = { - 'background_color': '#FFFFFF', - 'description': - 'Home automation platform that puts local control and privacy first.', - 'dir': 'ltr', - 'display': 'standalone', - 'icons': [], - 'lang': 'en-US', - 'name': 'Home Assistant', - 'short_name': 'Assistant', - 'start_url': '/?homescreen=1', - 'theme_color': DEFAULT_THEME_COLOR + "background_color": "#FFFFFF", + "description": "Home automation platform that puts local control and privacy first.", + "dir": "ltr", + "display": "standalone", + "icons": [], + "lang": "en-US", + "name": "Home Assistant", + "short_name": "Assistant", + "start_url": "/?homescreen=1", + "theme_color": DEFAULT_THEME_COLOR, } for size in (192, 384, 512, 1024): - MANIFEST_JSON['icons'].append({ - 'src': '/static/icons/favicon-{size}x{size}.png'.format(size=size), - 'sizes': '{size}x{size}'.format(size=size), - 'type': 'image/png' - }) + MANIFEST_JSON["icons"].append( + { + "src": "/static/icons/favicon-{size}x{size}.png".format(size=size), + "sizes": "{size}x{size}".format(size=size), + "type": "image/png", + } + ) -DATA_PANELS = 'frontend_panels' -DATA_JS_VERSION = 'frontend_js_version' -DATA_EXTRA_HTML_URL = 'frontend_extra_html_url' -DATA_EXTRA_HTML_URL_ES5 = 'frontend_extra_html_url_es5' -DATA_EXTRA_MODULE_URL = 'frontend_extra_module_url' -DATA_EXTRA_JS_URL_ES5 = 'frontend_extra_js_url_es5' -DATA_THEMES = 'frontend_themes' -DATA_DEFAULT_THEME = 'frontend_default_theme' -DEFAULT_THEME = 'default' +DATA_PANELS = "frontend_panels" +DATA_JS_VERSION = "frontend_js_version" +DATA_EXTRA_HTML_URL = "frontend_extra_html_url" +DATA_EXTRA_HTML_URL_ES5 = "frontend_extra_html_url_es5" +DATA_EXTRA_MODULE_URL = "frontend_extra_module_url" +DATA_EXTRA_JS_URL_ES5 = "frontend_extra_js_url_es5" +DATA_THEMES = "frontend_themes" +DATA_DEFAULT_THEME = "frontend_default_theme" +DEFAULT_THEME = "default" -PRIMARY_COLOR = 'primary-color' +PRIMARY_COLOR = "primary-color" _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_FRONTEND_REPO): cv.isdir, - vol.Optional(CONF_THEMES): vol.Schema({ - cv.string: {cv.string: cv.string} - }), - vol.Optional(CONF_EXTRA_HTML_URL): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EXTRA_MODULE_URL): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EXTRA_JS_URL_ES5): - vol.All(cv.ensure_list, [cv.string]), - # We no longer use these options. - vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all, - vol.Optional(CONF_JS_VERSION): cv.match_all, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_FRONTEND_REPO): cv.isdir, + vol.Optional(CONF_THEMES): vol.Schema( + {cv.string: {cv.string: cv.string}} + ), + vol.Optional(CONF_EXTRA_HTML_URL): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXTRA_MODULE_URL): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_EXTRA_JS_URL_ES5): vol.All( + cv.ensure_list, [cv.string] + ), + # We no longer use these options. + vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all, + vol.Optional(CONF_JS_VERSION): cv.match_all, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -SERVICE_SET_THEME = 'set_theme' -SERVICE_RELOAD_THEMES = 'reload_themes' -SERVICE_SET_THEME_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, -}) -WS_TYPE_GET_PANELS = 'get_panels' -SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET_PANELS, -}) -WS_TYPE_GET_THEMES = 'frontend/get_themes' -SCHEMA_GET_THEMES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET_THEMES, -}) -WS_TYPE_GET_TRANSLATIONS = 'frontend/get_translations' -SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET_TRANSLATIONS, - vol.Required('language'): str, -}) +SERVICE_SET_THEME = "set_theme" +SERVICE_RELOAD_THEMES = "reload_themes" +SERVICE_SET_THEME_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) +WS_TYPE_GET_PANELS = "get_panels" +SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_GET_PANELS} +) +WS_TYPE_GET_THEMES = "frontend/get_themes" +SCHEMA_GET_THEMES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_GET_THEMES} +) +WS_TYPE_GET_TRANSLATIONS = "frontend/get_translations" +SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_GET_TRANSLATIONS, vol.Required("language"): str} +) class Panel: @@ -134,8 +138,15 @@ class Panel: # If the panel should only be visible to admins require_admin = False - def __init__(self, component_name, sidebar_title, sidebar_icon, - frontend_url_path, config, require_admin): + def __init__( + self, + component_name, + sidebar_title, + sidebar_icon, + frontend_url_path, + config, + require_admin, + ): """Initialize a built-in panel.""" self.component_name = component_name self.sidebar_title = sidebar_title @@ -148,24 +159,35 @@ class Panel: def to_response(self): """Panel as dictionary.""" return { - 'component_name': self.component_name, - 'icon': self.sidebar_icon, - 'title': self.sidebar_title, - 'config': self.config, - 'url_path': self.frontend_url_path, - 'require_admin': self.require_admin, + "component_name": self.component_name, + "icon": self.sidebar_icon, + "title": self.sidebar_title, + "config": self.config, + "url_path": self.frontend_url_path, + "require_admin": self.require_admin, } @bind_hass @callback -def async_register_built_in_panel(hass, component_name, - sidebar_title=None, sidebar_icon=None, - frontend_url_path=None, config=None, - require_admin=False): +def async_register_built_in_panel( + hass, + component_name, + sidebar_title=None, + sidebar_icon=None, + frontend_url_path=None, + config=None, + require_admin=False, +): """Register a built-in panel.""" - panel = Panel(component_name, sidebar_title, sidebar_icon, - frontend_url_path, config, require_admin) + panel = Panel( + component_name, + sidebar_title, + sidebar_icon, + frontend_url_path, + config, + require_admin, + ) panels = hass.data.setdefault(DATA_PANELS, {}) @@ -217,9 +239,10 @@ def add_manifest_json_key(key, val): def _frontend_root(dev_repo_path): """Return root path to the frontend files.""" if dev_repo_path is not None: - return pathlib.Path(dev_repo_path) / 'hass_frontend' + return pathlib.Path(dev_repo_path) / "hass_frontend" import hass_frontend + return hass_frontend.where() @@ -227,12 +250,14 @@ async def async_setup(hass, config): """Set up the serving of the frontend.""" await async_setup_frontend_storage(hass) hass.components.websocket_api.async_register_command( - WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS) + WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS + ) hass.components.websocket_api.async_register_command( - WS_TYPE_GET_THEMES, websocket_get_themes, SCHEMA_GET_THEMES) + WS_TYPE_GET_THEMES, websocket_get_themes, SCHEMA_GET_THEMES + ) hass.components.websocket_api.async_register_command( - WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, - SCHEMA_GET_TRANSLATIONS) + WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS + ) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -242,37 +267,44 @@ async def async_setup(hass, config): root_path = _frontend_root(repo_path) for path, should_cache in ( - ("service_worker.js", False), - ("robots.txt", False), - ("onboarding.html", True), - ("static", True), - ("frontend_latest", True), - ("frontend_es5", True), + ("service_worker.js", False), + ("robots.txt", False), + ("onboarding.html", True), + ("static", True), + ("frontend_latest", True), + ("frontend_es5", True), ): hass.http.register_static_path( - "/{}".format(path), str(root_path / path), should_cache) + "/{}".format(path), str(root_path / path), should_cache + ) hass.http.register_static_path( - "/auth/authorize", str(root_path / "authorize.html"), False) + "/auth/authorize", str(root_path / "authorize.html"), False + ) - local = hass.config.path('www') + local = hass.config.path("www") if os.path.isdir(local): hass.http.register_static_path("/local", local, not is_dev) hass.http.app.router.register_resource(IndexView(repo_path, hass)) - for panel in ('kiosk', 'states', 'profile'): + for panel in ("kiosk", "states", "profile"): async_register_built_in_panel(hass, panel) # To smooth transition to new urls, add redirects to new urls of dev tools # Added June 27, 2019. Can be removed in 2021. - for panel in ('event', 'info', 'service', 'state', 'template', 'mqtt'): - hass.http.register_redirect('/dev-{}'.format(panel), - '/developer-tools/{}'.format(panel)) + for panel in ("event", "info", "service", "state", "template", "mqtt"): + hass.http.register_redirect( + "/dev-{}".format(panel), "/developer-tools/{}".format(panel) + ) async_register_built_in_panel( - hass, "developer-tools", require_admin=True, - sidebar_title="developer_tools", sidebar_icon="hass:hammer") + hass, + "developer-tools", + require_admin=True, + sidebar_title="developer_tools", + sidebar_icon="hass:hammer", + ) if DATA_EXTRA_HTML_URL not in hass.data: hass.data[DATA_EXTRA_HTML_URL] = set() @@ -313,13 +345,12 @@ def _async_setup_themes(hass, themes): name = hass.data[DATA_DEFAULT_THEME] themes = hass.data[DATA_THEMES] if name != DEFAULT_THEME and PRIMARY_COLOR in themes[name]: - MANIFEST_JSON['theme_color'] = themes[name][PRIMARY_COLOR] + MANIFEST_JSON["theme_color"] = themes[name][PRIMARY_COLOR] else: - MANIFEST_JSON['theme_color'] = DEFAULT_THEME_COLOR - hass.bus.async_fire(EVENT_THEMES_UPDATED, { - 'themes': themes, - 'default_theme': name, - }) + MANIFEST_JSON["theme_color"] = DEFAULT_THEME_COLOR + hass.bus.async_fire( + EVENT_THEMES_UPDATED, {"themes": themes, "default_theme": name} + ) @callback def set_theme(call): @@ -344,7 +375,8 @@ def _async_setup_themes(hass, themes): update_theme_and_fire_event() hass.services.async_register( - DOMAIN, SERVICE_SET_THEME, set_theme, schema=SERVICE_SET_THEME_SCHEMA) + DOMAIN, SERVICE_SET_THEME, set_theme, schema=SERVICE_SET_THEME_SCHEMA + ) hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes) @@ -361,12 +393,12 @@ class IndexView(web_urldispatcher.AbstractResource): @property def canonical(self) -> str: """Return resource's canonical path.""" - return '/' + return "/" @property def _route(self): """Return the index route.""" - return web_urldispatcher.ResourceRoute('GET', self.get, self) + return web_urldispatcher.ResourceRoute("GET", self.get, self) def url_for(self, **kwargs: str) -> URL: """Construct url for resource with additional params.""" @@ -377,14 +409,16 @@ class IndexView(web_urldispatcher.AbstractResource): Return (UrlMappingMatchInfo, allowed_methods) pair. """ - if (request.path != '/' and - request.url.parts[1] not in self.hass.data[DATA_PANELS]): + if ( + request.path != "/" + and request.url.parts[1] not in self.hass.data[DATA_PANELS] + ): return None, set() if request.method != hdrs.METH_GET: - return None, {'GET'} + return None, {"GET"} - return web_urldispatcher.UrlMappingMatchInfo({}, self._route), {'GET'} + return web_urldispatcher.UrlMappingMatchInfo({}, self._route), {"GET"} def add_prefix(self, prefix: str) -> None: """Add a prefix to processed URLs. @@ -394,9 +428,7 @@ class IndexView(web_urldispatcher.AbstractResource): def get_info(self): """Return a dict with additional info useful for introspection.""" - return { - 'panels': list(self.hass.data[DATA_PANELS]) - } + return {"panels": list(self.hass.data[DATA_PANELS])} def freeze(self) -> None: """Freeze the resource.""" @@ -410,9 +442,7 @@ class IndexView(web_urldispatcher.AbstractResource): """Get template.""" tpl = self._template_cache if tpl is None: - with open( - str(_frontend_root(self.repo_path) / 'index.html') - ) as file: + with open(str(_frontend_root(self.repo_path) / "index.html")) as file: tpl = jinja2.Template(file.read()) # Cache template if not running from repository @@ -423,12 +453,10 @@ class IndexView(web_urldispatcher.AbstractResource): async def get(self, request: web.Request): """Serve the index page for panel pages.""" - hass = request.app['hass'] + hass = request.app["hass"] if not hass.components.onboarding.async_is_onboarded(): - return web.Response(status=302, headers={ - 'location': '/onboarding.html' - }) + return web.Response(status=302, headers={"location": "/onboarding.html"}) template = self._template_cache @@ -437,12 +465,12 @@ class IndexView(web_urldispatcher.AbstractResource): return web.Response( text=template.render( - theme_color=MANIFEST_JSON['theme_color'], + theme_color=MANIFEST_JSON["theme_color"], extra_urls=hass.data[DATA_EXTRA_HTML_URL], extra_modules=hass.data[DATA_EXTRA_MODULE_URL], extra_js_es5=hass.data[DATA_EXTRA_JS_URL_ES5], ), - content_type='text/html' + content_type="text/html", ) def __len__(self) -> int: @@ -458,11 +486,11 @@ class ManifestJSONView(HomeAssistantView): """View to return a manifest.json.""" requires_auth = False - url = '/manifest.json' - name = 'manifestjson' + url = "/manifest.json" + name = "manifestjson" @callback - def get(self, request): # pylint: disable=no-self-use + def get(self, request): # pylint: disable=no-self-use """Return the manifest.json.""" msg = json.dumps(MANIFEST_JSON, sort_keys=True) return web.Response(text=msg, content_type="application/manifest+json") @@ -478,10 +506,10 @@ def websocket_get_panels(hass, connection, msg): panels = { panel_key: panel.to_response() for panel_key, panel in connection.hass.data[DATA_PANELS].items() - if user_is_admin or not panel.require_admin} + if user_is_admin or not panel.require_admin + } - connection.send_message(websocket_api.result_message( - msg['id'], panels)) + connection.send_message(websocket_api.result_message(msg["id"], panels)) @callback @@ -490,10 +518,15 @@ def websocket_get_themes(hass, connection, msg): Async friendly. """ - connection.send_message(websocket_api.result_message(msg['id'], { - 'themes': hass.data[DATA_THEMES], - 'default_theme': hass.data[DATA_DEFAULT_THEME], - })) + connection.send_message( + websocket_api.result_message( + msg["id"], + { + "themes": hass.data[DATA_THEMES], + "default_theme": hass.data[DATA_DEFAULT_THEME], + }, + ) + ) @websocket_api.async_response @@ -502,9 +535,7 @@ async def websocket_get_translations(hass, connection, msg): Async friendly. """ - resources = await async_get_translations(hass, msg['language']) - connection.send_message(websocket_api.result_message( - msg['id'], { - 'resources': resources, - } - )) + resources = await async_get_translations(hass, msg["language"]) + connection.send_message( + websocket_api.result_message(msg["id"], {"resources": resources}) + ) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 45d6e49e399..b6a996afc98 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190717.1" + "home-assistant-frontend==20190801.0" ], "dependencies": [ "api", diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index 17aae14c820..56f23da5253 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -4,24 +4,21 @@ import voluptuous as vol from homeassistant.components import websocket_api -DATA_STORAGE = 'frontend_storage' +DATA_STORAGE = "frontend_storage" STORAGE_VERSION_USER_DATA = 1 -STORAGE_KEY_USER_DATA = 'frontend.user_data_{}' +STORAGE_KEY_USER_DATA = "frontend.user_data_{}" async def async_setup_frontend_storage(hass): """Set up frontend storage.""" hass.data[DATA_STORAGE] = ({}, {}) - hass.components.websocket_api.async_register_command( - websocket_set_user_data - ) - hass.components.websocket_api.async_register_command( - websocket_get_user_data - ) + hass.components.websocket_api.async_register_command(websocket_set_user_data) + hass.components.websocket_api.async_register_command(websocket_get_user_data) def with_store(orig_func): """Decorate function to provide data.""" + @wraps(orig_func) async def with_store_func(hass, connection, msg): """Provide user specific data and store to function.""" @@ -32,25 +29,24 @@ def with_store(orig_func): if store is None: store = stores[user_id] = hass.helpers.storage.Store( STORAGE_VERSION_USER_DATA, - STORAGE_KEY_USER_DATA.format(connection.user.id) + STORAGE_KEY_USER_DATA.format(connection.user.id), ) if user_id not in data: data[user_id] = await store.async_load() or {} - await orig_func( - hass, connection, msg, - store, - data[user_id], - ) + await orig_func(hass, connection, msg, store, data[user_id]) + return with_store_func -@websocket_api.websocket_command({ - vol.Required('type'): 'frontend/set_user_data', - vol.Required('key'): str, - vol.Required('value'): vol.Any(bool, str, int, float, dict, list, None), -}) +@websocket_api.websocket_command( + { + vol.Required("type"): "frontend/set_user_data", + vol.Required("key"): str, + vol.Required("value"): vol.Any(bool, str, int, float, dict, list, None), + } +) @websocket_api.async_response @with_store async def websocket_set_user_data(hass, connection, msg, store, data): @@ -58,17 +54,14 @@ async def websocket_set_user_data(hass, connection, msg, store, data): Async friendly. """ - data[msg['key']] = msg['value'] + data[msg["key"]] = msg["value"] await store.async_save(data) - connection.send_message(websocket_api.result_message( - msg['id'], - )) + connection.send_message(websocket_api.result_message(msg["id"])) -@websocket_api.websocket_command({ - vol.Required('type'): 'frontend/get_user_data', - vol.Optional('key'): str, -}) +@websocket_api.websocket_command( + {vol.Required("type"): "frontend/get_user_data", vol.Optional("key"): str} +) @websocket_api.async_response @with_store async def websocket_get_user_data(hass, connection, msg, store, data): @@ -76,8 +69,8 @@ async def websocket_get_user_data(hass, connection, msg, store, data): Async friendly. """ - connection.send_message(websocket_api.result_message( - msg['id'], { - 'value': data.get(msg['key']) if 'key' in msg else data - } - )) + connection.send_message( + websocket_api.result_message( + msg["id"], {"value": data.get(msg["key"]) if "key" in msg else data} + ) + ) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 64aa1d3a012..8ab379b050b 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -3,46 +3,73 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, - SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) + MEDIA_TYPE_MUSIC, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_PORT, STATE_OFF, STATE_PAUSED, - STATE_PLAYING, STATE_UNKNOWN) + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, + STATE_UNKNOWN, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -SUPPORT_FRONTIER_SILICON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ - SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ - SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_ON | \ - SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE +SUPPORT_FRONTIER_SILICON = ( + SUPPORT_PAUSE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_STEP + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_SEEK + | SUPPORT_PLAY_MEDIA + | SUPPORT_PLAY + | SUPPORT_STOP + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE +) DEFAULT_PORT = 80 -DEFAULT_PASSWORD = '1234' -DEVICE_URL = 'http://{0}:{1}/device' +DEFAULT_PASSWORD = "1234" +DEVICE_URL = "http://{0}:{1}/device" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + } +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Frontier Silicon platform.""" import requests if discovery_info is not None: async_add_entities( - [AFSAPIDevice( - discovery_info['ssdp_description'], DEFAULT_PASSWORD)], True) + [AFSAPIDevice(discovery_info["ssdp_description"], DEFAULT_PASSWORD)], True + ) return True host = config.get(CONF_HOST) @@ -51,12 +78,14 @@ async def async_setup_platform( try: async_add_entities( - [AFSAPIDevice(DEVICE_URL.format(host, port), password)], True) + [AFSAPIDevice(DEVICE_URL.format(host, port), password)], True + ) _LOGGER.debug("FSAPI device %s:%s -> %s", host, port, password) return True except requests.exceptions.RequestException: - _LOGGER.error("Could not add the FSAPI device at %s:%s -> %s", - host, port, password) + _LOGGER.error( + "Could not add the FSAPI device at %s:%s -> %s", host, port, password + ) return False @@ -161,10 +190,10 @@ class AFSAPIDevice(MediaPlayerDevice): status = await fs_device.get_play_status() self._state = { - 'playing': STATE_PLAYING, - 'paused': STATE_PAUSED, - 'stopped': STATE_OFF, - 'unknown': STATE_UNKNOWN, + "playing": STATE_PLAYING, + "paused": STATE_PAUSED, + "stopped": STATE_OFF, + "unknown": STATE_UNKNOWN, None: STATE_OFF, }.get(status, STATE_UNKNOWN) @@ -172,7 +201,7 @@ class AFSAPIDevice(MediaPlayerDevice): info_name = await fs_device.get_play_name() info_text = await fs_device.get_play_text() - self._title = ' - '.join(filter(None, [info_name, info_text])) + self._title = " - ".join(filter(None, [info_name, info_text])) self._artist = await fs_device.get_play_artist() self._album_name = await fs_device.get_play_album() @@ -208,7 +237,7 @@ class AFSAPIDevice(MediaPlayerDevice): async def async_media_play_pause(self): """Send play/pause command.""" - if 'playing' in self._state: + if "playing" in self._state: await self.fs_device.pause() else: await self.fs_device.play() @@ -239,12 +268,12 @@ class AFSAPIDevice(MediaPlayerDevice): async def async_volume_up(self): """Send volume up command.""" volume = await self.fs_device.get_volume() - await self.fs_device.set_volume(volume+1) + await self.fs_device.set_volume(volume + 1) async def async_volume_down(self): """Send volume down command.""" volume = await self.fs_device.get_volume() - await self.fs_device.set_volume(volume-1) + await self.fs_device.set_volume(volume - 1) async def async_set_volume_level(self, volume): """Set volume command.""" diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index 91ec8b0794d..eba768f82e3 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -4,31 +4,37 @@ import logging import voluptuous as vol -from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_PORT, CONF_DEVICES) +from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT, CONF_DEVICES from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + Light, + PLATFORM_SCHEMA, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_DRIVER = 'driver' -CONF_DRIVER_FNIP6X10AD = 'FNIP6x10ad' -CONF_DRIVER_FNIP8X10A = 'FNIP8x10a' +CONF_DRIVER = "driver" +CONF_DRIVER_FNIP6X10AD = "FNIP6x10ad" +CONF_DRIVER_FNIP8X10A = "FNIP8x10a" CONF_DRIVER_TYPES = [CONF_DRIVER_FNIP6X10AD, CONF_DRIVER_FNIP8X10A] -DEVICE_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Optional('dimmable', default=False): cv.boolean, -}) +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional("dimmable", default=False): cv.boolean, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES), - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_DEVICES): {cv.string: DEVICE_SCHEMA}, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_DEVICES): {cv.string: DEVICE_SCHEMA}, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -36,12 +42,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): lights = [] for channel, device_config in config[CONF_DEVICES].items(): device = {} - device['name'] = device_config[CONF_NAME] - device['dimmable'] = device_config['dimmable'] - device['channel'] = channel - device['driver'] = config[CONF_DRIVER] - device['host'] = config[CONF_HOST] - device['port'] = config[CONF_PORT] + device["name"] = device_config[CONF_NAME] + device["dimmable"] = device_config["dimmable"] + device["channel"] = channel + device["driver"] = config[CONF_DRIVER] + device["host"] = config[CONF_HOST] + device["port"] = config[CONF_PORT] lights.append(FutureNowLight(device)) add_entities(lights, True) @@ -64,21 +70,21 @@ class FutureNowLight(Light): """Initialize the light.""" import pyfnip - self._name = device['name'] - self._dimmable = device['dimmable'] - self._channel = device['channel'] + self._name = device["name"] + self._dimmable = device["dimmable"] + self._channel = device["channel"] self._brightness = None self._last_brightness = 255 self._state = None - if device['driver'] == CONF_DRIVER_FNIP6X10AD: - self._light = pyfnip.FNIP6x2adOutput(device['host'], - device['port'], - self._channel) - if device['driver'] == CONF_DRIVER_FNIP8X10A: - self._light = pyfnip.FNIP8x10aOutput(device['host'], - device['port'], - self._channel) + if device["driver"] == CONF_DRIVER_FNIP6X10AD: + self._light = pyfnip.FNIP6x2adOutput( + device["host"], device["port"], self._channel + ) + if device["driver"] == CONF_DRIVER_FNIP8X10A: + self._light = pyfnip.FNIP8x10aOutput( + device["host"], device["port"], self._channel + ) @property def name(self): diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index b3c7c7c1215..2e52b49c5f4 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -8,42 +8,51 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( - CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD, CONF_ACCESS_TOKEN, CONF_NAME, - STATE_CLOSED, STATE_OPEN, CONF_COVERS) + CONF_DEVICE, + CONF_USERNAME, + CONF_PASSWORD, + CONF_ACCESS_TOKEN, + CONF_NAME, + STATE_CLOSED, + STATE_OPEN, + CONF_COVERS, +) _LOGGER = logging.getLogger(__name__) -ATTR_AVAILABLE = 'available' -ATTR_SENSOR_STRENGTH = 'sensor_reflection_rate' -ATTR_SIGNAL_STRENGTH = 'wifi_signal_strength' -ATTR_TIME_IN_STATE = 'time_in_state' +ATTR_AVAILABLE = "available" +ATTR_SENSOR_STRENGTH = "sensor_reflection_rate" +ATTR_SIGNAL_STRENGTH = "wifi_signal_strength" +ATTR_TIME_IN_STATE = "time_in_state" -DEFAULT_NAME = 'Garadget' +DEFAULT_NAME = "Garadget" -STATE_CLOSING = 'closing' -STATE_OFFLINE = 'offline' -STATE_OPENING = 'opening' -STATE_STOPPED = 'stopped' +STATE_CLOSING = "closing" +STATE_OFFLINE = "offline" +STATE_OPENING = "opening" +STATE_STOPPED = "stopped" STATES_MAP = { - 'open': STATE_OPEN, - 'opening': STATE_OPENING, - 'closed': STATE_CLOSED, - 'closing': STATE_CLOSING, - 'stopped': STATE_STOPPED + "open": STATE_OPEN, + "opening": STATE_OPENING, + "closed": STATE_CLOSED, + "closing": STATE_CLOSING, + "stopped": STATE_STOPPED, } -COVER_SCHEMA = vol.Schema({ - vol.Optional(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_DEVICE): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME): cv.string, -}) +COVER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_DEVICE): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA)} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -53,11 +62,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for device_id, device_config in devices.items(): args = { - 'name': device_config.get(CONF_NAME), - 'device_id': device_config.get(CONF_DEVICE, device_id), - 'username': device_config.get(CONF_USERNAME), - 'password': device_config.get(CONF_PASSWORD), - 'access_token': device_config.get(CONF_ACCESS_TOKEN) + "name": device_config.get(CONF_NAME), + "device_id": device_config.get(CONF_DEVICE, device_id), + "username": device_config.get(CONF_USERNAME), + "password": device_config.get(CONF_PASSWORD), + "access_token": device_config.get(CONF_ACCESS_TOKEN), } covers.append(GaradgetCover(hass, args)) @@ -70,14 +79,14 @@ class GaradgetCover(CoverDevice): def __init__(self, hass, args): """Initialize the cover.""" - self.particle_url = 'https://api.particle.io' + self.particle_url = "https://api.particle.io" self.hass = hass - self._name = args['name'] - self.device_id = args['device_id'] - self.access_token = args['access_token'] + self._name = args["name"] + self.device_id = args["device_id"] + self.access_token = args["access_token"] self.obtained_token = False - self._username = args['username'] - self._password = args['password'] + self._username = args["username"] + self._password = args["password"] self._state = None self.time_in_state = None self.signal = None @@ -91,19 +100,20 @@ class GaradgetCover(CoverDevice): try: if self._name is None: - doorconfig = self._get_variable('doorConfig') - if doorconfig['nme'] is not None: - self._name = doorconfig['nme'] + doorconfig = self._get_variable("doorConfig") + if doorconfig["nme"] is not None: + self._name = doorconfig["nme"] self.update() except requests.exceptions.ConnectionError as ex: - _LOGGER.error( - "Unable to connect to server: %(reason)s", dict(reason=ex)) + _LOGGER.error("Unable to connect to server: %(reason)s", dict(reason=ex)) self._state = STATE_OFFLINE self._available = False self._name = DEFAULT_NAME except KeyError: - _LOGGER.warning("Garadget device %(device)s seems to be offline", - dict(device=self.device_id)) + _LOGGER.warning( + "Garadget device %(device)s seems to be offline", + dict(device=self.device_id), + ) self._name = DEFAULT_NAME self._state = STATE_OFFLINE self._available = False @@ -158,30 +168,27 @@ class GaradgetCover(CoverDevice): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return 'garage' + return "garage" def get_token(self): """Get new token for usage during this session.""" args = { - 'grant_type': 'password', - 'username': self._username, - 'password': self._password + "grant_type": "password", + "username": self._username, + "password": self._password, } - url = '{}/oauth/token'.format(self.particle_url) - ret = requests.post( - url, auth=('particle', 'particle'), data=args, timeout=10) + url = "{}/oauth/token".format(self.particle_url) + ret = requests.post(url, auth=("particle", "particle"), data=args, timeout=10) try: - return ret.json()['access_token'] + return ret.json()["access_token"] except KeyError: _LOGGER.error("Unable to retrieve access token") def remove_token(self): """Remove authorization token from API.""" - url = '{}/v1/access_tokens/{}'.format( - self.particle_url, self.access_token) - ret = requests.delete( - url, auth=(self._username, self._password), timeout=10) + url = "{}/v1/access_tokens/{}".format(self.particle_url, self.access_token) + ret = requests.delete(url, auth=(self._username, self._password), timeout=10) return ret.text def _start_watcher(self, command): @@ -189,7 +196,8 @@ class GaradgetCover(CoverDevice): _LOGGER.debug("Starting Watcher for command: %s ", command) if self._unsub_listener_cover is None: self._unsub_listener_cover = track_utc_time_change( - self.hass, self._check_state) + self.hass, self._check_state + ) def _check_state(self, now): """Check the state of the service during an operation.""" @@ -197,42 +205,43 @@ class GaradgetCover(CoverDevice): def close_cover(self, **kwargs): """Close the cover.""" - if self._state not in ['close', 'closing']: - ret = self._put_command('setState', 'close') - self._start_watcher('close') - return ret.get('return_value') == 1 + if self._state not in ["close", "closing"]: + ret = self._put_command("setState", "close") + self._start_watcher("close") + return ret.get("return_value") == 1 def open_cover(self, **kwargs): """Open the cover.""" - if self._state not in ['open', 'opening']: - ret = self._put_command('setState', 'open') - self._start_watcher('open') - return ret.get('return_value') == 1 + if self._state not in ["open", "opening"]: + ret = self._put_command("setState", "open") + self._start_watcher("open") + return ret.get("return_value") == 1 def stop_cover(self, **kwargs): """Stop the door where it is.""" - if self._state not in ['stopped']: - ret = self._put_command('setState', 'stop') - self._start_watcher('stop') - return ret['return_value'] == 1 + if self._state not in ["stopped"]: + ret = self._put_command("setState", "stop") + self._start_watcher("stop") + return ret["return_value"] == 1 def update(self): """Get updated status from API.""" try: - status = self._get_variable('doorStatus') - _LOGGER.debug("Current Status: %s", status['status']) - self._state = STATES_MAP.get(status['status'], None) - self.time_in_state = status['time'] - self.signal = status['signal'] - self.sensor = status['sensor'] + status = self._get_variable("doorStatus") + _LOGGER.debug("Current Status: %s", status["status"]) + self._state = STATES_MAP.get(status["status"], None) + self.time_in_state = status["time"] + self.signal = status["signal"] + self.sensor = status["sensor"] self._available = True except requests.exceptions.ConnectionError as ex: - _LOGGER.error( - "Unable to connect to server: %(reason)s", dict(reason=ex)) + _LOGGER.error("Unable to connect to server: %(reason)s", dict(reason=ex)) self._state = STATE_OFFLINE except KeyError: - _LOGGER.warning("Garadget device %(device)s seems to be offline", - dict(device=self.device_id)) + _LOGGER.warning( + "Garadget device %(device)s seems to be offline", + dict(device=self.device_id), + ) self._state = STATE_OFFLINE if self._state not in [STATE_CLOSING, STATE_OPENING]: @@ -242,21 +251,21 @@ class GaradgetCover(CoverDevice): def _get_variable(self, var): """Get latest status.""" - url = '{}/v1/devices/{}/{}?access_token={}'.format( - self.particle_url, self.device_id, var, self.access_token) + url = "{}/v1/devices/{}/{}?access_token={}".format( + self.particle_url, self.device_id, var, self.access_token + ) ret = requests.get(url, timeout=10) result = {} - for pairs in ret.json()['result'].split('|'): - key = pairs.split('=') + for pairs in ret.json()["result"].split("|"): + key = pairs.split("=") result[key[0]] = key[1] return result def _put_command(self, func, arg=None): """Send commands to API.""" - params = {'access_token': self.access_token} + params = {"access_token": self.access_token} if arg: - params['command'] = arg - url = '{}/v1/devices/{}/{}'.format( - self.particle_url, self.device_id, func) + params["command"] = arg + url = "{}/v1/devices/{}/{}".format(self.particle_url, self.device_id, func) ret = requests.post(url, data=params, timeout=10) return ret.json() diff --git a/homeassistant/components/gc100/__init__.py b/homeassistant/components/gc100/__init__.py index b875d045cc0..19303fdc6d2 100644 --- a/homeassistant/components/gc100/__init__.py +++ b/homeassistant/components/gc100/__init__.py @@ -3,25 +3,29 @@ import logging import voluptuous as vol -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_PORTS = 'ports' +CONF_PORTS = "ports" DEFAULT_PORT = 4998 -DOMAIN = 'gc100' +DOMAIN = "gc100" -DATA_GC100 = 'gc100' +DATA_GC100 = "gc100" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) # pylint: disable=no-member diff --git a/homeassistant/components/gc100/binary_sensor.py b/homeassistant/components/gc100/binary_sensor.py index 4ba68a17799..a2f8ba4a0a2 100644 --- a/homeassistant/components/gc100/binary_sensor.py +++ b/homeassistant/components/gc100/binary_sensor.py @@ -1,20 +1,17 @@ """Support for binary sensor using GC100.""" import voluptuous as vol -from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv from . import CONF_PORTS, DATA_GC100 -_SENSORS_SCHEMA = vol.Schema({ - cv.string: cv.string, -}) +_SENSORS_SCHEMA = vol.Schema({cv.string: cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PORTS): vol.All(cv.ensure_list, [_SENSORS_SCHEMA]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_PORTS): vol.All(cv.ensure_list, [_SENSORS_SCHEMA])} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -23,8 +20,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ports = config.get(CONF_PORTS) for port in ports: for port_addr, port_name in port.items(): - binary_sensors.append(GC100BinarySensor( - port_name, port_addr, hass.data[DATA_GC100])) + binary_sensors.append( + GC100BinarySensor(port_name, port_addr, hass.data[DATA_GC100]) + ) add_entities(binary_sensors, True) diff --git a/homeassistant/components/gc100/switch.py b/homeassistant/components/gc100/switch.py index eea98a4dc23..6be42e35deb 100644 --- a/homeassistant/components/gc100/switch.py +++ b/homeassistant/components/gc100/switch.py @@ -8,13 +8,11 @@ from homeassistant.helpers.entity import ToggleEntity from . import CONF_PORTS, DATA_GC100 -_SWITCH_SCHEMA = vol.Schema({ - cv.string: cv.string, -}) +_SWITCH_SCHEMA = vol.Schema({cv.string: cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PORTS): vol.All(cv.ensure_list, [_SWITCH_SCHEMA]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_PORTS): vol.All(cv.ensure_list, [_SWITCH_SCHEMA])} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -23,8 +21,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ports = config.get(CONF_PORTS) for port in ports: for port_addr, port_name in port.items(): - switches.append(GC100Switch( - port_name, port_addr, hass.data[DATA_GC100])) + switches.append(GC100Switch(port_name, port_addr, hass.data[DATA_GC100])) add_entities(switches, True) diff --git a/homeassistant/components/gearbest/sensor.py b/homeassistant/components/gearbest/sensor.py index ee0ee6d4e3b..bad9b335e73 100644 --- a/homeassistant/components/gearbest/sensor.py +++ b/homeassistant/components/gearbest/sensor.py @@ -9,37 +9,40 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval -from homeassistant.const import (CONF_NAME, CONF_ID, CONF_URL, CONF_CURRENCY) +from homeassistant.const import CONF_NAME, CONF_ID, CONF_URL, CONF_CURRENCY _LOGGER = logging.getLogger(__name__) -CONF_ITEMS = 'items' +CONF_ITEMS = "items" -ICON = 'mdi:coin' -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=2*60*60) # 2h -MIN_TIME_BETWEEN_CURRENCY_UPDATES = timedelta(seconds=12*60*60) # 12h +ICON = "mdi:coin" +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=2 * 60 * 60) # 2h +MIN_TIME_BETWEEN_CURRENCY_UPDATES = timedelta(seconds=12 * 60 * 60) # 12h _ITEM_SCHEMA = vol.All( - vol.Schema({ - vol.Exclusive(CONF_URL, 'XOR'): cv.string, - vol.Exclusive(CONF_ID, 'XOR'): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_CURRENCY): cv.string - }), cv.has_at_least_one_key(CONF_URL, CONF_ID) + vol.Schema( + { + vol.Exclusive(CONF_URL, "XOR"): cv.string, + vol.Exclusive(CONF_ID, "XOR"): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_CURRENCY): cv.string, + } + ), + cv.has_at_least_one_key(CONF_URL, CONF_ID), ) _ITEMS_SCHEMA = vol.Schema([_ITEM_SCHEMA]) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ITEMS): _ITEMS_SCHEMA, - vol.Required(CONF_CURRENCY): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_ITEMS): _ITEMS_SCHEMA, vol.Required(CONF_CURRENCY): cv.string} +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gearbest sensor.""" from gearbest_parser import CurrencyConverter + currency = config.get(CONF_CURRENCY) sensors = [] @@ -58,9 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Update currency list.""" converter.update() - track_time_interval(hass, - currency_update, - MIN_TIME_BETWEEN_CURRENCY_UPDATES) + track_time_interval(hass, currency_update, MIN_TIME_BETWEEN_CURRENCY_UPDATES) add_entities(sensors, True) @@ -75,9 +76,9 @@ class GearbestSensor(Entity): self._name = item.get(CONF_NAME) self._parser = GearbestParser() self._parser.set_currency_converter(converter) - self._item = self._parser.load(item.get(CONF_ID), - item.get(CONF_URL), - item.get(CONF_CURRENCY, currency)) + self._item = self._parser.load( + item.get(CONF_ID), item.get(CONF_URL), item.get(CONF_CURRENCY, currency) + ) if self._item is None: raise ValueError("id and url could not be resolved") @@ -109,10 +110,12 @@ class GearbestSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - attrs = {'name': self._item.name, - 'description': self._item.description, - 'currency': self._item.currency, - 'url': self._item.url} + attrs = { + "name": self._item.name, + "description": self._item.description, + "currency": self._item.currency, + "url": self._item.url, + } return attrs @Throttle(MIN_TIME_BETWEEN_UPDATES) diff --git a/homeassistant/components/geizhals/sensor.py b/homeassistant/components/geizhals/sensor.py index 03c263f54ab..28fe10ec5f5 100644 --- a/homeassistant/components/geizhals/sensor.py +++ b/homeassistant/components/geizhals/sensor.py @@ -12,25 +12,22 @@ from homeassistant.const import CONF_NAME _LOGGER = logging.getLogger(__name__) -CONF_DESCRIPTION = 'description' -CONF_PRODUCT_ID = 'product_id' -CONF_LOCALE = 'locale' +CONF_DESCRIPTION = "description" +CONF_PRODUCT_ID = "product_id" +CONF_LOCALE = "locale" -ICON = 'mdi:coin' +ICON = "mdi:coin" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_PRODUCT_ID): cv.positive_int, - vol.Optional(CONF_DESCRIPTION, default='Price'): cv.string, - vol.Optional(CONF_LOCALE, default='DE'): vol.In( - ['AT', - 'EU', - 'DE', - 'UK', - 'PL']), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_PRODUCT_ID): cv.positive_int, + vol.Optional(CONF_DESCRIPTION, default="Price"): cv.string, + vol.Optional(CONF_LOCALE, default="DE"): vol.In(["AT", "EU", "DE", "UK", "PL"]), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -40,8 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): product_id = config.get(CONF_PRODUCT_ID) domain = config.get(CONF_LOCALE) - add_entities([Geizwatch(name, description, product_id, domain)], - True) + add_entities([Geizwatch(name, description, product_id, domain)], True) class Geizwatch(Entity): @@ -82,15 +78,17 @@ class Geizwatch(Entity): def device_state_attributes(self): """Return the state attributes.""" while len(self._device.prices) < 4: - self._device.prices.append('None') - attrs = {'device_name': self._device.name, - 'description': self.description, - 'unit_of_measurement': self._device.price_currency, - 'product_id': self.product_id, - 'price1': self._device.prices[0], - 'price2': self._device.prices[1], - 'price3': self._device.prices[2], - 'price4': self._device.prices[3]} + self._device.prices.append("None") + attrs = { + "device_name": self._device.name, + "description": self.description, + "unit_of_measurement": self._device.price_currency, + "product_id": self.product_id, + "price1": self._device.prices[0], + "price2": self._device.prices[1], + "price3": self._device.prices[2], + "price4": self._device.prices[3], + } return attrs @Throttle(MIN_TIME_BETWEEN_UPDATES) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 8b98d84c06d..307142ed989 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -9,42 +9,54 @@ from requests.auth import HTTPDigestAuth import voluptuous as vol from homeassistant.const import ( - CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION, - HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, CONF_VERIFY_SSL) + CONF_NAME, + CONF_USERNAME, + CONF_PASSWORD, + CONF_AUTHENTICATION, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, + CONF_VERIFY_SSL, +) from homeassistant.exceptions import TemplateError from homeassistant.components.camera import ( - PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, SUPPORT_STREAM, Camera) + PLATFORM_SCHEMA, + DEFAULT_CONTENT_TYPE, + SUPPORT_STREAM, + Camera, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) -CONF_CONTENT_TYPE = 'content_type' -CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change' -CONF_STILL_IMAGE_URL = 'still_image_url' -CONF_STREAM_SOURCE = 'stream_source' -CONF_FRAMERATE = 'framerate' +CONF_CONTENT_TYPE = "content_type" +CONF_LIMIT_REFETCH_TO_URL_CHANGE = "limit_refetch_to_url_change" +CONF_STILL_IMAGE_URL = "still_image_url" +CONF_STREAM_SOURCE = "stream_source" +CONF_FRAMERATE = "framerate" -DEFAULT_NAME = 'Generic Camera' +DEFAULT_NAME = "Generic Camera" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_STILL_IMAGE_URL): cv.template, - vol.Optional(CONF_STREAM_SOURCE, default=None): vol.Any(None, cv.string), - vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): - vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), - vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE, default=False): cv.boolean, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string, - vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STILL_IMAGE_URL): cv.template, + vol.Optional(CONF_STREAM_SOURCE, default=None): vol.Any(None, cv.string), + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In( + [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] + ), + vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE, default=False): cv.boolean, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string, + vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a generic IP Camera.""" async_add_entities([GenericCamera(hass, config)]) @@ -94,15 +106,15 @@ class GenericCamera(Camera): def camera_image(self): """Return bytes of camera image.""" return run_coroutine_threadsafe( - self.async_camera_image(), self.hass.loop).result() + self.async_camera_image(), self.hass.loop + ).result() async def async_camera_image(self): """Return a still image response from the camera.""" try: url = self._still_image_url.async_render() except TemplateError as err: - _LOGGER.error( - "Error parsing template %s: %s", self._still_image_url, err) + _LOGGER.error("Error parsing template %s: %s", self._still_image_url, err) return self._last_image if url == self._last_url and self._limit_refetch: @@ -110,26 +122,27 @@ class GenericCamera(Camera): # aiohttp don't support DigestAuth yet if self._authentication == HTTP_DIGEST_AUTHENTICATION: + def fetch(): """Read image from a URL.""" try: - response = requests.get(url, timeout=10, auth=self._auth, - verify=self.verify_ssl) + response = requests.get( + url, timeout=10, auth=self._auth, verify=self.verify_ssl + ) return response.content except requests.exceptions.RequestException as error: _LOGGER.error("Error getting camera image: %s", error) return self._last_image - self._last_image = await self.hass.async_add_job( - fetch) + self._last_image = await self.hass.async_add_job(fetch) # async else: try: websession = async_get_clientsession( - self.hass, verify_ssl=self.verify_ssl) + self.hass, verify_ssl=self.verify_ssl + ) with async_timeout.timeout(10): - response = await websession.get( - url, auth=self._auth) + response = await websession.get(url, auth=self._auth) self._last_image = await response.read() except asyncio.TimeoutError: _LOGGER.error("Timeout getting image from: %s", self._name) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index ba18d9de936..13e23b962c9 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -6,65 +6,86 @@ import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - ATTR_PRESET_MODE, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - PRESET_AWAY, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE) + ATTR_PRESET_MODE, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_AWAY, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + PRESET_NONE, +) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_NAME, EVENT_HOMEASSISTANT_START, - PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, SERVICE_TURN_OFF, - SERVICE_TURN_ON, STATE_ON, STATE_UNKNOWN) + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + CONF_NAME, + EVENT_HOMEASSISTANT_START, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNKNOWN, +) from homeassistant.core import DOMAIN as HA_DOMAIN, callback from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( - async_track_state_change, async_track_time_interval) + async_track_state_change, + async_track_time_interval, +) from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) DEFAULT_TOLERANCE = 0.3 -DEFAULT_NAME = 'Generic Thermostat' +DEFAULT_NAME = "Generic Thermostat" -CONF_HEATER = 'heater' -CONF_SENSOR = 'target_sensor' -CONF_MIN_TEMP = 'min_temp' -CONF_MAX_TEMP = 'max_temp' -CONF_TARGET_TEMP = 'target_temp' -CONF_AC_MODE = 'ac_mode' -CONF_MIN_DUR = 'min_cycle_duration' -CONF_COLD_TOLERANCE = 'cold_tolerance' -CONF_HOT_TOLERANCE = 'hot_tolerance' -CONF_KEEP_ALIVE = 'keep_alive' -CONF_INITIAL_HVAC_MODE = 'initial_hvac_mode' -CONF_AWAY_TEMP = 'away_temp' -CONF_PRECISION = 'precision' +CONF_HEATER = "heater" +CONF_SENSOR = "target_sensor" +CONF_MIN_TEMP = "min_temp" +CONF_MAX_TEMP = "max_temp" +CONF_TARGET_TEMP = "target_temp" +CONF_AC_MODE = "ac_mode" +CONF_MIN_DUR = "min_cycle_duration" +CONF_COLD_TOLERANCE = "cold_tolerance" +CONF_HOT_TOLERANCE = "hot_tolerance" +CONF_KEEP_ALIVE = "keep_alive" +CONF_INITIAL_HVAC_MODE = "initial_hvac_mode" +CONF_AWAY_TEMP = "away_temp" +CONF_PRECISION = "precision" SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HEATER): cv.entity_id, - vol.Required(CONF_SENSOR): cv.entity_id, - vol.Optional(CONF_AC_MODE): cv.boolean, - vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), - vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta), - vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_COLD_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce( - float), - vol.Optional(CONF_HOT_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce( - float), - vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), - vol.Optional(CONF_KEEP_ALIVE): vol.All( - cv.time_period, cv.positive_timedelta), - vol.Optional(CONF_INITIAL_HVAC_MODE): - vol.In([HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF]), - vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float), - vol.Optional(CONF_PRECISION): vol.In( - [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HEATER): cv.entity_id, + vol.Required(CONF_SENSOR): cv.entity_id, + vol.Optional(CONF_AC_MODE): cv.boolean, + vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_COLD_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float), + vol.Optional(CONF_HOT_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float), + vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), + vol.Optional(CONF_KEEP_ALIVE): vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_INITIAL_HVAC_MODE): vol.In( + [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF] + ), + vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float), + vol.Optional(CONF_PRECISION): vol.In( + [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] + ), + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the generic thermostat platform.""" name = config.get(CONF_NAME) heater_entity_id = config.get(CONF_HEATER) @@ -82,20 +103,50 @@ async def async_setup_platform(hass, config, async_add_entities, precision = config.get(CONF_PRECISION) unit = hass.config.units.temperature_unit - async_add_entities([GenericThermostat( - name, heater_entity_id, sensor_entity_id, min_temp, max_temp, - target_temp, ac_mode, min_cycle_duration, cold_tolerance, - hot_tolerance, keep_alive, initial_hvac_mode, away_temp, - precision, unit)]) + async_add_entities( + [ + GenericThermostat( + name, + heater_entity_id, + sensor_entity_id, + min_temp, + max_temp, + target_temp, + ac_mode, + min_cycle_duration, + cold_tolerance, + hot_tolerance, + keep_alive, + initial_hvac_mode, + away_temp, + precision, + unit, + ) + ] + ) class GenericThermostat(ClimateDevice, RestoreEntity): """Representation of a Generic Thermostat device.""" - def __init__(self, name, heater_entity_id, sensor_entity_id, - min_temp, max_temp, target_temp, ac_mode, min_cycle_duration, - cold_tolerance, hot_tolerance, keep_alive, - initial_hvac_mode, away_temp, precision, unit): + def __init__( + self, + name, + heater_entity_id, + sensor_entity_id, + min_temp, + max_temp, + target_temp, + ac_mode, + min_cycle_duration, + cold_tolerance, + hot_tolerance, + keep_alive, + initial_hvac_mode, + away_temp, + precision, + unit, + ): """Initialize the thermostat.""" self._name = name self.heater_entity_id = heater_entity_id @@ -131,13 +182,16 @@ class GenericThermostat(ClimateDevice, RestoreEntity): # Add listener async_track_state_change( - self.hass, self.sensor_entity_id, self._async_sensor_changed) + self.hass, self.sensor_entity_id, self._async_sensor_changed + ) async_track_state_change( - self.hass, self.heater_entity_id, self._async_switch_changed) + self.hass, self.heater_entity_id, self._async_switch_changed + ) if self._keep_alive: async_track_time_interval( - self.hass, self._async_control_heating, self._keep_alive) + self.hass, self._async_control_heating, self._keep_alive + ) @callback def _async_startup(event): @@ -146,8 +200,7 @@ class GenericThermostat(ClimateDevice, RestoreEntity): if sensor_state and sensor_state.state != STATE_UNKNOWN: self._async_update_temp(sensor_state) - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, _async_startup) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) # Check If we have an old state old_state = await self.async_get_last_state() @@ -160,11 +213,12 @@ class GenericThermostat(ClimateDevice, RestoreEntity): self._target_temp = self.max_temp else: self._target_temp = self.min_temp - _LOGGER.warning("Undefined target temperature," - "falling back to %s", self._target_temp) + _LOGGER.warning( + "Undefined target temperature," "falling back to %s", + self._target_temp, + ) else: - self._target_temp = float( - old_state.attributes[ATTR_TEMPERATURE]) + self._target_temp = float(old_state.attributes[ATTR_TEMPERATURE]) if old_state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY: self._is_away = True if not self._hvac_mode and old_state.state: @@ -177,8 +231,9 @@ class GenericThermostat(ClimateDevice, RestoreEntity): self._target_temp = self.max_temp else: self._target_temp = self.min_temp - _LOGGER.warning("No previously saved temperature, setting to %s", - self._target_temp) + _LOGGER.warning( + "No previously saved temperature, setting to %s", self._target_temp + ) # Set default state to off if not self._hvac_mode: @@ -251,7 +306,7 @@ class GenericThermostat(ClimateDevice, RestoreEntity): def preset_modes(self): """Return a list of available preset modes.""" if self._away_temp: - return [PRESET_AWAY] + return [PRESET_NONE, PRESET_AWAY] return None async def async_set_hvac_mode(self, hvac_mode): @@ -326,12 +381,14 @@ class GenericThermostat(ClimateDevice, RestoreEntity): async def _async_control_heating(self, time=None, force=False): """Check if we need to turn heating on or off.""" async with self._temp_lock: - if not self._active and None not in (self._cur_temp, - self._target_temp): + if not self._active and None not in (self._cur_temp, self._target_temp): self._active = True - _LOGGER.info("Obtained current and target temperature. " - "Generic thermostat active. %s, %s", - self._cur_temp, self._target_temp) + _LOGGER.info( + "Obtained current and target temperature. " + "Generic thermostat active. %s, %s", + self._cur_temp, + self._target_temp, + ) if not self._active or self._hvac_mode == HVAC_MODE_OFF: return @@ -347,27 +404,25 @@ class GenericThermostat(ClimateDevice, RestoreEntity): else: current_state = HVAC_MODE_OFF long_enough = condition.state( - self.hass, self.heater_entity_id, current_state, - self.min_cycle_duration) + self.hass, + self.heater_entity_id, + current_state, + self.min_cycle_duration, + ) if not long_enough: return - too_cold = \ - self._target_temp - self._cur_temp >= self._cold_tolerance - too_hot = \ - self._cur_temp - self._target_temp >= self._hot_tolerance + too_cold = self._target_temp - self._cur_temp >= self._cold_tolerance + too_hot = self._cur_temp - self._target_temp >= self._hot_tolerance if self._is_device_active: - if (self.ac_mode and too_cold) or \ - (not self.ac_mode and too_hot): - _LOGGER.info("Turning off heater %s", - self.heater_entity_id) + if (self.ac_mode and too_cold) or (not self.ac_mode and too_hot): + _LOGGER.info("Turning off heater %s", self.heater_entity_id) await self._async_heater_turn_off() elif time is not None: # The time argument is passed only in keep-alive case await self._async_heater_turn_on() else: - if (self.ac_mode and too_hot) or \ - (not self.ac_mode and too_cold): + if (self.ac_mode and too_hot) or (not self.ac_mode and too_cold): _LOGGER.info("Turning on heater %s", self.heater_entity_id) await self._async_heater_turn_on() elif time is not None: @@ -404,7 +459,7 @@ class GenericThermostat(ClimateDevice, RestoreEntity): self._saved_target_temp = self._target_temp self._target_temp = self._away_temp await self._async_control_heating(force=True) - elif not preset_mode and self._is_away: + elif preset_mode == PRESET_NONE and self._is_away: self._is_away = False self._target_temp = self._saved_target_temp await self._async_control_heating(force=True) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index b9ab1515d32..3bbec4258bc 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -6,8 +6,7 @@ import voluptuous as vol from geniushubclient import GeniusHubClient -from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform @@ -16,54 +15,62 @@ from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) -DOMAIN = 'geniushub' +DOMAIN = "geniushub" SCAN_INTERVAL = timedelta(seconds=60) -_V1_API_SCHEMA = vol.Schema({ - vol.Required(CONF_TOKEN): cv.string, -}) -_V3_API_SCHEMA = vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, -}) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Any( - _V3_API_SCHEMA, - _V1_API_SCHEMA, - ) -}, extra=vol.ALLOW_EXTRA) +_V1_API_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): cv.string}) +_V3_API_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } +) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Any(_V3_API_SCHEMA, _V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA +) async def async_setup(hass, hass_config): """Create a Genius Hub system.""" kwargs = dict(hass_config[DOMAIN]) if CONF_HOST in kwargs: - args = (kwargs.pop(CONF_HOST), ) + args = (kwargs.pop(CONF_HOST),) else: - args = (kwargs.pop(CONF_TOKEN), ) + args = (kwargs.pop(CONF_TOKEN),) hass.data[DOMAIN] = {} - data = hass.data[DOMAIN]['data'] = GeniusData(hass, args, kwargs) + data = hass.data[DOMAIN]["data"] = GeniusData(hass, args, kwargs) try: await data._client.hub.update() # pylint: disable=protected-access except AssertionError: # assert response.status == HTTP_OK - _LOGGER.warning( - "Setup failed, check your configuration.", - exc_info=True) + _LOGGER.warning("Setup failed, check your configuration.", exc_info=True) return False + _LOGGER.debug( + # noqa; pylint: disable=protected-access + "zones_raw = %s", + data._client.hub._zones_raw, + ) + _LOGGER.debug( + # noqa; pylint: disable=protected-access + "devices_raw = %s", + data._client.hub._devices_raw, + ) + async_track_time_interval(hass, data.async_update, SCAN_INTERVAL) - for platform in ['climate', 'water_heater']: - hass.async_create_task(async_load_platform( - hass, platform, DOMAIN, {}, hass_config)) + for platform in ["climate", "water_heater"]: + hass.async_create_task( + async_load_platform(hass, platform, DOMAIN, {}, hass_config) + ) - if not data._client._api_v1: # pylint: disable=protected-access - for platform in ['sensor', 'binary_sensor']: - hass.async_create_task(async_load_platform( - hass, platform, DOMAIN, {}, hass_config)) + if data._client.api_version == 3: # pylint: disable=protected-access + for platform in ["sensor", "binary_sensor"]: + hass.async_create_task( + async_load_platform(hass, platform, DOMAIN, {}, hass_config) + ) return True @@ -74,8 +81,9 @@ class GeniusData: def __init__(self, hass, args, kwargs): """Initialize the geniushub client.""" self._hass = hass - self._client = hass.data[DOMAIN]['client'] = GeniusHubClient( - *args, **kwargs, session=async_get_clientsession(hass)) + self._client = hass.data[DOMAIN]["client"] = GeniusHubClient( + *args, **kwargs, session=async_get_clientsession(hass) + ) async def async_update(self, now, **kwargs): """Update the geniushub client's data.""" @@ -84,4 +92,16 @@ class GeniusData: except AssertionError: # assert response.status == HTTP_OK _LOGGER.warning("Update failed.", exc_info=True) return + + _LOGGER.debug( + # noqa; pylint: disable=protected-access + "zones_raw = %s", + self._client.hub._zones_raw, + ) + _LOGGER.debug( + # noqa; pylint: disable=protected-access + "devices_raw = %s", + self._client.hub._devices_raw, + ) + async_dispatcher_send(self._hass, DOMAIN) diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index c0f0d90028d..afdc0ef5f89 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,26 +1,26 @@ """Support for Genius Hub binary_sensor devices.""" -from datetime import datetime import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.dt import utc_from_timestamp from . import DOMAIN _LOGGER = logging.getLogger(__name__) -GH_IS_SWITCH = ['Dual Channel Receiver', 'Electric Switch', 'Smart Plug'] +GH_IS_SWITCH = ["Dual Channel Receiver", "Electric Switch", "Smart Plug"] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Genius Hub sensor entities.""" - client = hass.data[DOMAIN]['client'] + client = hass.data[DOMAIN]["client"] devices = [d for d in client.hub.device_objs if d.type is not None] - switches = [GeniusBinarySensor(client, d) - for d in devices if d.type[:21] in GH_IS_SWITCH] + switches = [ + GeniusBinarySensor(client, d) for d in devices if d.type[:21] in GH_IS_SWITCH + ] async_add_entities(switches) @@ -33,10 +33,10 @@ class GeniusBinarySensor(BinarySensorDevice): self._client = client self._device = device - if device.type[:21] == 'Dual Channel Receiver': - self._name = 'Dual Channel Receiver {}'.format(device.id) + if device.type[:21] == "Dual Channel Receiver": + self._name = "Dual Channel Receiver {}".format(device.id) else: - self._name = '{} {}'.format(device.type, device.id) + self._name = "{} {}".format(device.type, device.id) async def async_added_to_hass(self): """Set up a listener when this entity is added to HA.""" @@ -59,17 +59,17 @@ class GeniusBinarySensor(BinarySensorDevice): @property def is_on(self): """Return the status of the sensor.""" - return self._device.state['outputOnOff'] + return self._device.state["outputOnOff"] @property def device_state_attributes(self): """Return the device state attributes.""" attrs = {} - attrs['assigned_zone'] = self._device.assignedZones[0]['name'] + attrs["assigned_zone"] = self._device.assignedZones[0]["name"] - last_comms = self._device._info_raw['childValues']['lastComms']['val'] # noqa; pylint: disable=protected-access + # noqa; pylint: disable=protected-access + last_comms = self._device._raw_json["childValues"]["lastComms"]["val"] if last_comms != 0: - attrs['last_comms'] = datetime.utcfromtimestamp( - last_comms).isoformat() + attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() return {**attrs} diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 18155f7e114..ae1d714dd2b 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -4,8 +4,13 @@ from typing import Any, Awaitable, Dict, Optional, List from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_OFF, HVAC_MODE_HEAT, PRESET_BOOST, PRESET_ACTIVITY, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE) + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + PRESET_BOOST, + PRESET_ACTIVITY, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_PRESET_MODE, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -14,34 +19,34 @@ from . import DOMAIN _LOGGER = logging.getLogger(__name__) -ATTR_DURATION = 'duration' +ATTR_DURATION = "duration" -GH_ZONES = ['radiator'] +GH_ZONES = ["radiator"] # temperature is repeated here, as it gives access to high-precision temps -GH_STATE_ATTRS = ['mode', 'temperature', 'type', 'occupied', 'override'] +GH_STATE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] # GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes -HA_HVAC_TO_GH = { - HVAC_MODE_OFF: 'off', - HVAC_MODE_HEAT: 'timer' -} +HA_HVAC_TO_GH = {HVAC_MODE_OFF: "off", HVAC_MODE_HEAT: "timer"} GH_HVAC_TO_HA = {v: k for k, v in HA_HVAC_TO_GH.items()} -HA_PRESET_TO_GH = { - PRESET_ACTIVITY: 'footprint', - PRESET_BOOST: 'override' -} +HA_PRESET_TO_GH = {PRESET_ACTIVITY: "footprint", PRESET_BOOST: "override"} GH_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_GH.items()} -async def async_setup_platform(hass, hass_config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, hass_config, async_add_entities, discovery_info=None +): """Set up the Genius Hub climate entities.""" - client = hass.data[DOMAIN]['client'] + client = hass.data[DOMAIN]["client"] - async_add_entities([GeniusClimateZone(client, z) - for z in client.hub.zone_objs if z.type in GH_ZONES]) + async_add_entities( + [ + GeniusClimateZone(client, z) + for z in client.hub.zone_objs + if z.type in GH_ZONES + ] + ) class GeniusClimateZone(ClimateDevice): @@ -52,7 +57,7 @@ class GeniusClimateZone(ClimateDevice): self._client = client self._zone = zone - if hasattr(self._zone, 'occupied'): # has a movement sensor + if hasattr(self._zone, "occupied"): # has a movement sensor self._preset_modes = list(HA_PRESET_TO_GH) else: self._preset_modes = [PRESET_BOOST] @@ -74,7 +79,7 @@ class GeniusClimateZone(ClimateDevice): def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" tmp = self._zone.__dict__.items() - return {'status': {k: v for k, v in tmp if k in GH_STATE_ATTRS}} + return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} @property def should_poll(self) -> bool: @@ -138,8 +143,9 @@ class GeniusClimateZone(ClimateDevice): async def async_set_temperature(self, **kwargs) -> Awaitable[None]: """Set a new target temperature for this zone.""" - await self._zone.set_override(kwargs[ATTR_TEMPERATURE], - kwargs.get(ATTR_DURATION, 3600)) + await self._zone.set_override( + kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600) + ) async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: """Set a new hvac mode.""" @@ -147,4 +153,4 @@ class GeniusClimateZone(ClimateDevice): async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: """Set a new preset mode.""" - await self._zone.set_mode(HA_PRESET_TO_GH.get(preset_mode, 'timer')) + await self._zone.set_mode(HA_PRESET_TO_GH.get(preset_mode, "timer")) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 7c82ceeca44..98145ea0944 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/components/geniushub", "requirements": [ - "geniushub-client==0.4.12" + "geniushub-client==0.5.4" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index a52bd2d692f..f87c957d3da 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -12,26 +12,26 @@ from . import DOMAIN _LOGGER = logging.getLogger(__name__) -GH_HAS_BATTERY = [ - 'Room Thermostat', 'Genius Valve', 'Room Sensor', 'Radiator Valve'] +GH_HAS_BATTERY = ["Room Thermostat", "Genius Valve", "Room Sensor", "Radiator Valve"] GH_LEVEL_MAPPING = { - 'error': 'Errors', - 'warning': 'Warnings', - 'information': 'Information' + "error": "Errors", + "warning": "Warnings", + "information": "Information", } -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Genius Hub sensor entities.""" - client = hass.data[DOMAIN]['client'] + client = hass.data[DOMAIN]["client"] - sensors = [GeniusDevice(client, d) - for d in client.hub.device_objs if d.type in GH_HAS_BATTERY] + sensors = [ + GeniusDevice(client, d) + for d in client.hub.device_objs + if d.type in GH_HAS_BATTERY + ] - issues = [GeniusIssue(client, i) - for i in list(GH_LEVEL_MAPPING)] + issues = [GeniusIssue(client, i) for i in list(GH_LEVEL_MAPPING)] async_add_entities(sensors + issues, update_before_add=True) @@ -44,7 +44,7 @@ class GeniusDevice(Entity): self._client = client self._device = device - self._name = '{} {}'.format(device.type, device.id) + self._name = "{} {}".format(device.type, device.id) async def async_added_to_hass(self): """Set up a listener when this entity is added to HA.""" @@ -62,23 +62,27 @@ class GeniusDevice(Entity): @property def icon(self): """Return the icon of the sensor.""" - values = self._device._info_raw['childValues'] # noqa; pylint: disable=protected-access + # noqa; pylint: disable=protected-access + values = self._device._raw_json["childValues"] - last_comms = utc_from_timestamp(values['lastComms']['val']) - interval = timedelta(seconds=values['WakeUp_Interval']['val']) + last_comms = utc_from_timestamp(values["lastComms"]["val"]) + if "WakeUp_Interval" in values: + interval = timedelta(seconds=values["WakeUp_Interval"]["val"]) + else: + interval = timedelta(minutes=20) if last_comms < utcnow() - interval * 3: - return 'mdi:battery-unknown' + return "mdi:battery-unknown" - battery_level = self._device.state['batteryLevel'] + battery_level = self._device.state["batteryLevel"] if battery_level == 255: - return 'mdi:battery-unknown' + return "mdi:battery-unknown" if battery_level < 40: - return 'mdi:battery-alert' + return "mdi:battery-alert" - icon = 'mdi:battery' + icon = "mdi:battery" if battery_level <= 95: - icon += '-{}'.format(int(round(battery_level / 10 - .01)) * 10) + icon += "-{}".format(int(round(battery_level / 10 - 0.01)) * 10) return icon @@ -90,7 +94,7 @@ class GeniusDevice(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of the sensor.""" - return '%' + return "%" @property def should_poll(self) -> bool: @@ -100,17 +104,18 @@ class GeniusDevice(Entity): @property def state(self): """Return the state of the sensor.""" - level = self._device.state['batteryLevel'] + level = self._device.state.get("batteryLevel", 255) return level if level != 255 else 0 @property def device_state_attributes(self): """Return the device state attributes.""" attrs = {} - attrs['assigned_zone'] = self._device.assignedZones[0]['name'] + attrs["assigned_zone"] = self._device.assignedZones[0]["name"] - last_comms = self._device._info_raw['childValues']['lastComms']['val'] # noqa; pylint: disable=protected-access - attrs['last_comms'] = utc_from_timestamp(last_comms).isoformat() + # noqa; pylint: disable=protected-access + last_comms = self._device._raw_json["childValues"]["lastComms"]["val"] + attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() return {**attrs} @@ -151,9 +156,10 @@ class GeniusIssue(Entity): @property def device_state_attributes(self): """Return the device state attributes.""" - return {'{}_list'.format(self._level): self._issues} + return {"{}_list".format(self._level): self._issues} async def async_update(self): """Process the sensor's state data.""" - self._issues = [i['description'] - for i in self._hub.issues if i['level'] == self._level] + self._issues = [ + i["description"] for i in self._hub.issues if i["level"] == self._level + ] diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 3b40bafa699..9e27ec5f190 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -3,56 +3,55 @@ import logging from homeassistant.components.water_heater import ( WaterHeaterDevice, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) -from homeassistant.const import ( - ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS) + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_OPERATION_MODE, +) +from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN -STATE_AUTO = 'auto' -STATE_MANUAL = 'manual' +STATE_AUTO = "auto" +STATE_MANUAL = "manual" _LOGGER = logging.getLogger(__name__) -GH_HEATERS = ['hot water temperature'] +GH_HEATERS = ["hot water temperature"] -GH_SUPPORT_FLAGS = \ - SUPPORT_TARGET_TEMPERATURE | \ - SUPPORT_OPERATION_MODE +GH_SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE # HA does not have SUPPORT_ON_OFF for water_heater GH_MAX_TEMP = 80.0 GH_MIN_TEMP = 30.0 # Genius Hub HW supports only Off, Override/Boost & Timer modes -HA_OPMODE_TO_GH = { - STATE_OFF: 'off', - STATE_AUTO: 'timer', - STATE_MANUAL: 'override', -} +HA_OPMODE_TO_GH = {STATE_OFF: "off", STATE_AUTO: "timer", STATE_MANUAL: "override"} GH_STATE_TO_HA = { - 'off': STATE_OFF, - 'timer': STATE_AUTO, - 'footprint': None, - 'away': None, - 'override': STATE_MANUAL, - 'early': None, - 'test': None, - 'linked': None, - 'other': None, + "off": STATE_OFF, + "timer": STATE_AUTO, + "footprint": None, + "away": None, + "override": STATE_MANUAL, + "early": None, + "test": None, + "linked": None, + "other": None, } -GH_STATE_ATTRS = ['type', 'override'] +GH_STATE_ATTRS = ["type", "override"] -async def async_setup_platform(hass, hass_config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, hass_config, async_add_entities, discovery_info=None +): """Set up the Genius Hub water_heater entities.""" - client = hass.data[DOMAIN]['client'] + client = hass.data[DOMAIN]["client"] - entities = [GeniusWaterHeater(client, z) - for z in client.hub.zone_objs if z.type in GH_HEATERS] + entities = [ + GeniusWaterHeater(client, z) + for z in client.hub.zone_objs + if z.type in GH_HEATERS + ] async_add_entities(entities) @@ -84,7 +83,7 @@ class GeniusWaterHeater(WaterHeaterDevice): def device_state_attributes(self): """Return the device state attributes.""" tmp = self._boiler.__dict__.items() - return {'status': {k: v for k, v in tmp if k in GH_STATE_ATTRS}} + return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} @property def should_poll(self) -> bool: diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index f7d79ae7145..2bf309e2450 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -5,49 +5,57 @@ from typing import Optional import voluptuous as vol -from homeassistant.components.geo_location import ( - PLATFORM_SCHEMA, GeolocationEvent) +from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, - EVENT_HOMEASSISTANT_START) + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_URL, + EVENT_HOMEASSISTANT_START, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) -ATTR_EXTERNAL_ID = 'external_id' +ATTR_EXTERNAL_ID = "external_id" DEFAULT_RADIUS_IN_KM = 20.0 -DEFAULT_UNIT_OF_MEASUREMENT = 'km' +DEFAULT_UNIT_OF_MEASUREMENT = "km" SCAN_INTERVAL = timedelta(minutes=5) -SIGNAL_DELETE_ENTITY = 'geo_json_events_delete_{}' -SIGNAL_UPDATE_ENTITY = 'geo_json_events_update_{}' +SIGNAL_DELETE_ENTITY = "geo_json_events_delete_{}" +SIGNAL_UPDATE_ENTITY = "geo_json_events_update_{}" -SOURCE = 'geo_json_events' +SOURCE = "geo_json_events" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_URL): cv.string, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the GeoJSON Events platform.""" url = config[CONF_URL] scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = (config.get(CONF_LATITUDE, hass.config.latitude), - config.get(CONF_LONGITUDE, hass.config.longitude)) + coordinates = ( + config.get(CONF_LATITUDE, hass.config.latitude), + config.get(CONF_LONGITUDE, hass.config.longitude), + ) radius_in_km = config[CONF_RADIUS] # Initialize the entity manager. feed = GeoJsonFeedEntityManager( - hass, add_entities, scan_interval, coordinates, url, radius_in_km) + hass, add_entities, scan_interval, coordinates, url, radius_in_km + ) def start_feed_manager(event): """Start feed manager.""" @@ -59,15 +67,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class GeoJsonFeedEntityManager: """Feed Entity Manager for GeoJSON feeds.""" - def __init__(self, hass, add_entities, scan_interval, coordinates, url, - radius_in_km): + def __init__( + self, hass, add_entities, scan_interval, coordinates, url, radius_in_km + ): """Initialize the GeoJSON Feed Manager.""" from geojson_client.generic_feed import GenericFeedManager self._hass = hass self._feed_manager = GenericFeedManager( - self._generate_entity, self._update_entity, self._remove_entity, - coordinates, url, filter_radius=radius_in_km) + self._generate_entity, + self._update_entity, + self._remove_entity, + coordinates, + url, + filter_radius=radius_in_km, + ) self._add_entities = add_entities self._scan_interval = scan_interval @@ -79,8 +93,8 @@ class GeoJsonFeedEntityManager: def _init_regular_updates(self): """Schedule regular updates at the specified interval.""" track_time_interval( - self._hass, lambda now: self._feed_manager.update(), - self._scan_interval) + self._hass, lambda now: self._feed_manager.update(), self._scan_interval + ) def get_entry(self, external_id): """Get feed entry by external id.""" @@ -118,11 +132,15 @@ class GeoJsonLocationEvent(GeolocationEvent): async def async_added_to_hass(self): """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( - self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id), - self._delete_callback) + self.hass, + SIGNAL_DELETE_ENTITY.format(self._external_id), + self._delete_callback, + ) self._remove_signal_update = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id), - self._update_callback) + self.hass, + SIGNAL_UPDATE_ENTITY.format(self._external_id), + self._update_callback, + ) @callback def _delete_callback(self): diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 23792e32a2b..869f96901c1 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -5,18 +5,20 @@ from typing import Optional from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.helpers.config_validation import ( # noqa - PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent _LOGGER = logging.getLogger(__name__) -ATTR_DISTANCE = 'distance' -ATTR_SOURCE = 'source' +ATTR_DISTANCE = "distance" +ATTR_SOURCE = "source" -DOMAIN = 'geo_location' +DOMAIN = "geo_location" -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = timedelta(seconds=60) @@ -24,7 +26,8 @@ SCAN_INTERVAL = timedelta(seconds=60) async def async_setup(hass, config): """Set up the Geolocation component.""" component = hass.data[DOMAIN] = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL) + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) await component.async_setup(config) return True diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index f900812385b..a4d13bdef9d 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -16,38 +16,45 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_UNIT_OF_MEASUREMENT, CONF_NAME, - CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_URL) + CONF_UNIT_OF_MEASUREMENT, + CONF_NAME, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_URL, +) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_CATEGORY = 'category' -ATTR_DISTANCE = 'distance' -ATTR_TITLE = 'title' +ATTR_CATEGORY = "category" +ATTR_DISTANCE = "distance" +ATTR_TITLE = "title" -CONF_CATEGORIES = 'categories' +CONF_CATEGORIES = "categories" -DEFAULT_ICON = 'mdi:alert' +DEFAULT_ICON = "mdi:alert" DEFAULT_NAME = "Event Service" DEFAULT_RADIUS_IN_KM = 20.0 -DEFAULT_UNIT_OF_MEASUREMENT = 'Events' +DEFAULT_UNIT_OF_MEASUREMENT = "Events" -DOMAIN = 'geo_rss_events' +DOMAIN = "geo_rss_events" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_URL): cv.string, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_CATEGORIES, default=[]): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_UNIT_OF_MEASUREMENT, - default=DEFAULT_UNIT_OF_MEASUREMENT): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CATEGORIES, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional( + CONF_UNIT_OF_MEASUREMENT, default=DEFAULT_UNIT_OF_MEASUREMENT + ): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -60,21 +67,31 @@ def setup_platform(hass, config, add_entities, discovery_info=None): categories = config.get(CONF_CATEGORIES) unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) - _LOGGER.debug("latitude=%s, longitude=%s, url=%s, radius=%s", - latitude, longitude, url, radius_in_km) + _LOGGER.debug( + "latitude=%s, longitude=%s, url=%s, radius=%s", + latitude, + longitude, + url, + radius_in_km, + ) # Create all sensors based on categories. devices = [] if not categories: - device = GeoRssServiceSensor((latitude, longitude), url, - radius_in_km, None, name, - unit_of_measurement) + device = GeoRssServiceSensor( + (latitude, longitude), url, radius_in_km, None, name, unit_of_measurement + ) devices.append(device) else: for category in categories: - device = GeoRssServiceSensor((latitude, longitude), url, - radius_in_km, category, name, - unit_of_measurement) + device = GeoRssServiceSensor( + (latitude, longitude), + url, + radius_in_km, + category, + name, + unit_of_measurement, + ) devices.append(device) add_entities(devices, True) @@ -82,8 +99,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class GeoRssServiceSensor(Entity): """Representation of a Sensor.""" - def __init__(self, coordinates, url, radius, category, service_name, - unit_of_measurement): + def __init__( + self, coordinates, url, radius, category, service_name, unit_of_measurement + ): """Initialize the sensor.""" self._category = category self._service_name = service_name @@ -91,16 +109,20 @@ class GeoRssServiceSensor(Entity): self._state_attributes = None self._unit_of_measurement = unit_of_measurement from georss_client.generic_feed import GenericFeed - self._feed = GenericFeed(coordinates, url, filter_radius=radius, - filter_categories=None if not category - else [category]) + + self._feed = GenericFeed( + coordinates, + url, + filter_radius=radius, + filter_categories=None if not category else [category], + ) @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self._service_name, - 'Any' if self._category is None - else self._category) + return "{} {}".format( + self._service_name, "Any" if self._category is None else self._category + ) @property def state(self): @@ -125,24 +147,25 @@ class GeoRssServiceSensor(Entity): def update(self): """Update this sensor from the GeoRSS service.""" import georss_client + status, feed_entries = self._feed.update() if status == georss_client.UPDATE_OK: - _LOGGER.debug("Adding events to sensor %s: %s", self.entity_id, - feed_entries) + _LOGGER.debug( + "Adding events to sensor %s: %s", self.entity_id, feed_entries + ) self._state = len(feed_entries) # And now compute the attributes from the filtered events. matrix = {} for entry in feed_entries: - matrix[entry.title] = '{:.0f}km'.format( - entry.distance_to_home) + matrix[entry.title] = "{:.0f}km".format(entry.distance_to_home) self._state_attributes = matrix elif status == georss_client.UPDATE_OK_NO_DATA: - _LOGGER.debug("Update successful, but no data received from %s", - self._feed) + _LOGGER.debug("Update successful, but no data received from %s", self._feed) # Don't change the state or state attributes. else: - _LOGGER.warning("Update not successful, no data received from %s", - self._feed) + _LOGGER.warning( + "Update not successful, no data received from %s", self._feed + ) # If no events were found due to an error then just set state to # zero. self._state = 0 diff --git a/homeassistant/components/geofency/.translations/bg.json b/homeassistant/components/geofency/.translations/bg.json index 6f06d5c00c6..b9bfa2a3b41 100644 --- a/homeassistant/components/geofency/.translations/bg.json +++ b/homeassistant/components/geofency/.translations/bg.json @@ -1,7 +1,18 @@ { "config": { "abort": { + "not_internet_accessible": "Home Assistant \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u0435\u043d \u043e\u0442 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0437\u0430 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430 \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e\u0442 Geofency.", "one_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." - } + }, + "create_entry": { + "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 Geofency. \n\n \u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." + }, + "step": { + "user": { + "description": "\u0421\u0438\u0433\u0443\u0440\u043d\u0438 \u043b\u0438 \u0441\u0442\u0435, \u0447\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Geofency Webhook?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435 \u043d\u0430 Geofency Webhook" + } + }, + "title": "Geofency Webhook" } } \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/ko.json b/homeassistant/components/geofency/.translations/ko.json index db60ec18fe1..42ff061a151 100644 --- a/homeassistant/components/geofency/.translations/ko.json +++ b/homeassistant/components/geofency/.translations/ko.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Geofency \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Geofency \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/geofency/.translations/pt-BR.json b/homeassistant/components/geofency/.translations/pt-BR.json new file mode 100644 index 00000000000..20f21df5ac8 --- /dev/null +++ b/homeassistant/components/geofency/.translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel na Internet para receber mensagens da Geofency.", + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso webhook no Geofency. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n\n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) para mais detalhes." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o Geofency Webhook?", + "title": "Configurar o Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 944879788de..6835103968a 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -6,8 +6,14 @@ import voluptuous as vol from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.const import ( - ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_NAME, CONF_WEBHOOK_ID, HTTP_OK, - HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME) + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_NAME, + CONF_WEBHOOK_ID, + HTTP_OK, + HTTP_UNPROCESSABLE_ENTITY, + STATE_NOT_HOME, +) from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -17,46 +23,55 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -CONF_MOBILE_BEACONS = 'mobile_beacons' +CONF_MOBILE_BEACONS = "mobile_beacons" -CONFIG_SCHEMA = vol.Schema({ - vol.Optional(DOMAIN): vol.Schema({ - vol.Optional(CONF_MOBILE_BEACONS, default=[]): vol.All( - cv.ensure_list, [cv.string]), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + vol.Optional(DOMAIN): vol.Schema( + { + vol.Optional(CONF_MOBILE_BEACONS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -ATTR_ADDRESS = 'address' -ATTR_BEACON_ID = 'beaconUUID' -ATTR_CURRENT_LATITUDE = 'currentLatitude' -ATTR_CURRENT_LONGITUDE = 'currentLongitude' -ATTR_DEVICE = 'device' -ATTR_ENTRY = 'entry' +ATTR_ADDRESS = "address" +ATTR_BEACON_ID = "beaconUUID" +ATTR_CURRENT_LATITUDE = "currentLatitude" +ATTR_CURRENT_LONGITUDE = "currentLongitude" +ATTR_DEVICE = "device" +ATTR_ENTRY = "entry" -BEACON_DEV_PREFIX = 'beacon' +BEACON_DEV_PREFIX = "beacon" -LOCATION_ENTRY = '1' -LOCATION_EXIT = '0' +LOCATION_ENTRY = "1" +LOCATION_EXIT = "0" -TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) +TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) def _address(value: str) -> str: r"""Coerce address by replacing '\n' with ' '.""" - return value.replace('\n', ' ') + return value.replace("\n", " ") -WEBHOOK_SCHEMA = vol.Schema({ - vol.Required(ATTR_ADDRESS): vol.All(cv.string, _address), - vol.Required(ATTR_DEVICE): vol.All(cv.string, slugify), - vol.Required(ATTR_ENTRY): vol.Any(LOCATION_ENTRY, LOCATION_EXIT), - vol.Required(ATTR_LATITUDE): cv.latitude, - vol.Required(ATTR_LONGITUDE): cv.longitude, - vol.Required(ATTR_NAME): vol.All(cv.string, slugify), - vol.Optional(ATTR_CURRENT_LATITUDE): cv.latitude, - vol.Optional(ATTR_CURRENT_LONGITUDE): cv.longitude, - vol.Optional(ATTR_BEACON_ID): cv.string, -}, extra=vol.ALLOW_EXTRA) +WEBHOOK_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ADDRESS): vol.All(cv.string, _address), + vol.Required(ATTR_DEVICE): vol.All(cv.string, slugify), + vol.Required(ATTR_ENTRY): vol.Any(LOCATION_ENTRY, LOCATION_EXIT), + vol.Required(ATTR_LATITUDE): cv.latitude, + vol.Required(ATTR_LONGITUDE): cv.longitude, + vol.Required(ATTR_NAME): vol.All(cv.string, slugify), + vol.Optional(ATTR_CURRENT_LATITUDE): cv.latitude, + vol.Optional(ATTR_CURRENT_LONGITUDE): cv.longitude, + vol.Optional(ATTR_BEACON_ID): cv.string, + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, hass_config): @@ -64,9 +79,9 @@ async def async_setup(hass, hass_config): config = hass_config.get(DOMAIN, {}) mobile_beacons = config.get(CONF_MOBILE_BEACONS, []) hass.data[DOMAIN] = { - 'beacons': [slugify(beacon) for beacon in mobile_beacons], - 'devices': set(), - 'unsub_device_tracker': {} + "beacons": [slugify(beacon) for beacon in mobile_beacons], + "devices": set(), + "unsub_device_tracker": {}, } return True @@ -76,15 +91,12 @@ async def handle_webhook(hass, webhook_id, request): try: data = WEBHOOK_SCHEMA(dict(await request.post())) except vol.MultipleInvalid as error: - return web.Response( - text=error.error_message, - status=HTTP_UNPROCESSABLE_ENTITY - ) + return web.Response(text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY) - if _is_mobile_beacon(data, hass.data[DOMAIN]['beacons']): + if _is_mobile_beacon(data, hass.data[DOMAIN]["beacons"]): return _set_location(hass, data, None) - if data['entry'] == LOCATION_ENTRY: - location_name = data['name'] + if data["entry"] == LOCATION_ENTRY: + location_name = data["name"] else: location_name = STATE_NOT_HOME if ATTR_CURRENT_LATITUDE in data: @@ -96,14 +108,14 @@ async def handle_webhook(hass, webhook_id, request): def _is_mobile_beacon(data, mobile_beacons): """Check if we have a mobile beacon.""" - return ATTR_BEACON_ID in data and data['name'] in mobile_beacons + return ATTR_BEACON_ID in data and data["name"] in mobile_beacons def _device_name(data): """Return name of device tracker.""" if ATTR_BEACON_ID in data: - return "{}_{}".format(BEACON_DEV_PREFIX, data['name']) - return data['device'] + return "{}_{}".format(BEACON_DEV_PREFIX, data["name"]) + return data["device"] def _set_location(hass, data, location_name): @@ -111,17 +123,22 @@ def _set_location(hass, data, location_name): device = _device_name(data) async_dispatcher_send( - hass, TRACKER_UPDATE, device, - (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), location_name, data) + hass, + TRACKER_UPDATE, + device, + (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), + location_name, + data, + ) - return web.Response( - text="Setting location for {}".format(device), status=HTTP_OK) + return web.Response(text="Setting location for {}".format(device), status=HTTP_OK) async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( - DOMAIN, 'Geofency', entry.data[CONF_WEBHOOK_ID], handle_webhook) + DOMAIN, "Geofency", entry.data[CONF_WEBHOOK_ID], handle_webhook + ) hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) @@ -132,7 +149,7 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) - hass.data[DOMAIN]['unsub_device_tracker'].pop(entry.entry_id)() + hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) return True diff --git a/homeassistant/components/geofency/config_flow.py b/homeassistant/components/geofency/config_flow.py index 422343b16bb..d354e6e245d 100644 --- a/homeassistant/components/geofency/config_flow.py +++ b/homeassistant/components/geofency/config_flow.py @@ -5,8 +5,6 @@ from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, - 'Geofency Webhook', - { - 'docs_url': 'https://www.home-assistant.io/components/geofency/' - } + "Geofency Webhook", + {"docs_url": "https://www.home-assistant.io/components/geofency/"}, ) diff --git a/homeassistant/components/geofency/const.py b/homeassistant/components/geofency/const.py index f42fb97f168..b0c54a4d407 100644 --- a/homeassistant/components/geofency/const.py +++ b/homeassistant/components/geofency/const.py @@ -1,3 +1,3 @@ """Const for Geofency.""" -DOMAIN = 'geofency' +DOMAIN = "geofency" diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 3400e7ea35d..09e9d46ce6d 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,15 +1,10 @@ """Support for the Geofency device tracker platform.""" import logging -from homeassistant.const import ( - ATTR_LATITUDE, - ATTR_LONGITUDE, -) +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import callback from homeassistant.components.device_tracker import SOURCE_TYPE_GPS -from homeassistant.components.device_tracker.config_entry import ( - TrackerEntity -) +from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers import device_registry @@ -21,20 +16,20 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Geofency config entry.""" + @callback def _receive_data(device, gps, location_name, attributes): """Fire HA event to set location.""" - if device in hass.data[GF_DOMAIN]['devices']: + if device in hass.data[GF_DOMAIN]["devices"]: return - hass.data[GF_DOMAIN]['devices'].add(device) + hass.data[GF_DOMAIN]["devices"].add(device) - async_add_entities([GeofencyEntity( - device, gps, location_name, attributes - )]) + async_add_entities([GeofencyEntity(device, gps, location_name, attributes)]) - hass.data[GF_DOMAIN]['unsub_device_tracker'][config_entry.entry_id] = \ - async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + hass.data[GF_DOMAIN]["unsub_device_tracker"][ + config_entry.entry_id + ] = async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) # Restore previously loaded devices dev_reg = await device_registry.async_get_registry(hass) @@ -46,7 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): } if dev_ids: - hass.data[GF_DOMAIN]['devices'].update(dev_ids) + hass.data[GF_DOMAIN]["devices"].update(dev_ids) async_add_entities(GeofencyEntity(dev_id) for dev_id in dev_ids) return True @@ -102,10 +97,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): @property def device_info(self): """Return the device info.""" - return { - 'name': self._name, - 'identifiers': {(GF_DOMAIN, self._unique_id)}, - } + return {"name": self._name, "identifiers": {(GF_DOMAIN, self._unique_id)}} @property def source_type(self): @@ -116,7 +108,8 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): """Register state update callback.""" await super().async_added_to_hass() self._unsub_dispatcher = async_dispatcher_connect( - self.hass, TRACKER_UPDATE, self._async_receive_data) + self.hass, TRACKER_UPDATE, self._async_receive_data + ) if self._attributes: return @@ -134,7 +127,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): """Clean up after entity before removal.""" await super().async_will_remove_from_hass() self._unsub_dispatcher() - self.hass.data[GF_DOMAIN]['devices'].remove(self._unique_id) + self.hass.data[GF_DOMAIN]["devices"].remove(self._unique_id) @callback def _async_receive_data(self, device, gps, location_name, attributes): diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index d552d2c65cc..a85364ebeca 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -5,39 +5,44 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_NAME, CONF_ACCESS_TOKEN, CONF_NAME, CONF_PATH, CONF_URL) + ATTR_NAME, + CONF_ACCESS_TOKEN, + CONF_NAME, + CONF_PATH, + CONF_URL, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_REPOS = 'repositories' +CONF_REPOS = "repositories" -ATTR_LATEST_COMMIT_MESSAGE = 'latest_commit_message' -ATTR_LATEST_COMMIT_SHA = 'latest_commit_sha' -ATTR_LATEST_RELEASE_URL = 'latest_release_url' -ATTR_LATEST_OPEN_ISSUE_URL = 'latest_open_issue_url' -ATTR_OPEN_ISSUES = 'open_issues' -ATTR_LATEST_OPEN_PULL_REQUEST_URL = 'latest_open_pull_request_url' -ATTR_OPEN_PULL_REQUESTS = 'open_pull_requests' -ATTR_PATH = 'path' -ATTR_STARGAZERS = 'stargazers' +ATTR_LATEST_COMMIT_MESSAGE = "latest_commit_message" +ATTR_LATEST_COMMIT_SHA = "latest_commit_sha" +ATTR_LATEST_RELEASE_URL = "latest_release_url" +ATTR_LATEST_OPEN_ISSUE_URL = "latest_open_issue_url" +ATTR_OPEN_ISSUES = "open_issues" +ATTR_LATEST_OPEN_PULL_REQUEST_URL = "latest_open_pull_request_url" +ATTR_OPEN_PULL_REQUESTS = "open_pull_requests" +ATTR_PATH = "path" +ATTR_STARGAZERS = "stargazers" -DEFAULT_NAME = 'GitHub' +DEFAULT_NAME = "GitHub" SCAN_INTERVAL = timedelta(seconds=300) -REPO_SCHEMA = vol.Schema({ - vol.Required(CONF_PATH): cv.string, - vol.Optional(CONF_NAME): cv.string -}) +REPO_SCHEMA = vol.Schema( + {vol.Required(CONF_PATH): cv.string, vol.Optional(CONF_NAME): cv.string} +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_URL): cv.url, - vol.Required(CONF_REPOS): - vol.All(cv.ensure_list, [REPO_SCHEMA]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_URL): cv.url, + vol.Required(CONF_REPOS): vol.All(cv.ensure_list, [REPO_SCHEMA]), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -47,11 +52,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = GitHubData( repository=repository, access_token=config.get(CONF_ACCESS_TOKEN), - server_url=config.get(CONF_URL) + server_url=config.get(CONF_URL), ) if data.setup_error is True: - _LOGGER.error("Error setting up GitHub platform. %s", - "Check previous errors for details") + _LOGGER.error( + "Error setting up GitHub platform. %s", + "Check previous errors for details", + ) return sensors.append(GitHubSensor(data)) add_entities(sensors, True) @@ -110,13 +117,13 @@ class GitHubSensor(Entity): ATTR_OPEN_ISSUES: self._open_issue_count, ATTR_LATEST_OPEN_PULL_REQUEST_URL: self._latest_open_pr_url, ATTR_OPEN_PULL_REQUESTS: self._pull_request_count, - ATTR_STARGAZERS: self._stargazers + ATTR_STARGAZERS: self._stargazers, } @property def icon(self): """Return the icon to use in the frontend.""" - return 'mdi:github-circle' + return "mdi:github-circle" def update(self): """Collect updated data from GitHub API.""" @@ -136,7 +143,7 @@ class GitHubSensor(Entity): self._stargazers = self._github_data.stargazers -class GitHubData(): +class GitHubData: """GitHub Data object.""" def __init__(self, repository, access_token=None, server_url=None): @@ -150,8 +157,7 @@ class GitHubData(): try: if server_url is not None: server_url += "/api/v3" - self._github_obj = github.Github( - access_token, base_url=server_url) + self._github_obj = github.Github(access_token, base_url=server_url) else: self._github_obj = github.Github(access_token) @@ -182,13 +188,13 @@ class GitHubData(): self.stargazers = repo.stargazers_count - open_issues = repo.get_issues(state='open', sort='created') + open_issues = repo.get_issues(state="open", sort="created") if open_issues is not None: self.open_issue_count = open_issues.totalCount if open_issues.totalCount > 0: self.latest_open_issue_url = open_issues[0].html_url - open_pull_requests = repo.get_pulls(state='open', sort='created') + open_pull_requests = repo.get_pulls(state="open", sort="created") if open_pull_requests is not None: self.pull_request_count = open_pull_requests.totalCount if open_pull_requests.totalCount > 0: diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index 1d59a5e4f21..d8055c88f30 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -6,40 +6,47 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_NAME, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_URL) + ATTR_ATTRIBUTION, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_TOKEN, + CONF_URL, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTR_BUILD_BRANCH = 'build branch' -ATTR_BUILD_COMMIT_DATE = 'commit date' -ATTR_BUILD_COMMIT_ID = 'commit id' -ATTR_BUILD_DURATION = 'build_duration' -ATTR_BUILD_FINISHED = 'build_finished' -ATTR_BUILD_ID = 'build id' -ATTR_BUILD_STARTED = 'build_started' -ATTR_BUILD_STATUS = 'build_status' +ATTR_BUILD_BRANCH = "build branch" +ATTR_BUILD_COMMIT_DATE = "commit date" +ATTR_BUILD_COMMIT_ID = "commit id" +ATTR_BUILD_DURATION = "build_duration" +ATTR_BUILD_FINISHED = "build_finished" +ATTR_BUILD_ID = "build id" +ATTR_BUILD_STARTED = "build_started" +ATTR_BUILD_STATUS = "build_status" ATTRIBUTION = "Information provided by https://gitlab.com/" -CONF_GITLAB_ID = 'gitlab_id' +CONF_GITLAB_ID = "gitlab_id" -DEFAULT_NAME = 'GitLab CI Status' -DEFAULT_URL = 'https://gitlab.com' +DEFAULT_NAME = "GitLab CI Status" +DEFAULT_URL = "https://gitlab.com" -ICON_HAPPY = 'mdi:emoticon-happy' -ICON_OTHER = 'mdi:git' -ICON_SAD = 'mdi:emoticon-sad' +ICON_HAPPY = "mdi:emoticon-happy" +ICON_OTHER = "mdi:git" +ICON_SAD = "mdi:emoticon-sad" SCAN_INTERVAL = timedelta(seconds=300) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_GITLAB_ID): cv.string, - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_GITLAB_ID): cv.string, + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -52,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): priv_token=config[CONF_TOKEN], gitlab_id=config[CONF_GITLAB_ID], interval=_interval, - url=_url + url=_url, ) add_entities([GitLabSensor(_gitlab_data, _name)], True) @@ -102,15 +109,15 @@ class GitLabSensor(Entity): ATTR_BUILD_COMMIT_ID: self._commit_id, ATTR_BUILD_COMMIT_DATE: self._commit_date, ATTR_BUILD_ID: self._build_id, - ATTR_BUILD_BRANCH: self._branch + ATTR_BUILD_BRANCH: self._branch, } @property def icon(self): """Return the icon to use in the frontend.""" - if self._state == 'success': + if self._state == "success": return ICON_HAPPY - if self._state == 'failed': + if self._state == "failed": return ICON_SAD return ICON_OTHER @@ -129,15 +136,15 @@ class GitLabSensor(Entity): self._available = self._gitlab_data.available -class GitLabData(): +class GitLabData: """GitLab Data object.""" def __init__(self, gitlab_id, priv_token, interval, url): """Fetch data from GitLab API for most recent CI job.""" import gitlab + self._gitlab_id = gitlab_id - self._gitlab = gitlab.Gitlab( - url, private_token=priv_token, per_page=1) + self._gitlab = gitlab.Gitlab(url, private_token=priv_token, per_page=1) self._gitlab.auth() self._gitlab_exceptions = gitlab.exceptions self.update = Throttle(interval)(self._update) @@ -157,15 +164,15 @@ class GitLabData(): _projects = self._gitlab.projects.get(self._gitlab_id) _last_pipeline = _projects.pipelines.list(page=1)[0] _last_job = _last_pipeline.jobs.list(page=1)[0] - self.status = _last_pipeline.attributes.get('status') - self.started_at = _last_job.attributes.get('started_at') - self.finished_at = _last_job.attributes.get('finished_at') - self.duration = _last_job.attributes.get('duration') - _commit = _last_job.attributes.get('commit') - self.commit_id = _commit.get('id') - self.commit_date = _commit.get('committed_date') - self.build_id = _last_job.attributes.get('id') - self.branch = _last_job.attributes.get('ref') + self.status = _last_pipeline.attributes.get("status") + self.started_at = _last_job.attributes.get("started_at") + self.finished_at = _last_job.attributes.get("finished_at") + self.duration = _last_job.attributes.get("duration") + _commit = _last_job.attributes.get("commit") + self.commit_id = _commit.get("id") + self.commit_date = _commit.get("committed_date") + self.build_id = _last_job.attributes.get("id") + self.branch = _last_job.attributes.get("ref") self.available = True except self._gitlab_exceptions.GitlabAuthenticationError as erra: _LOGGER.error("Authentication Error: %s", erra) diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index 06fb6e3a3b5..f124849a193 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -10,20 +10,22 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_MENTION = 'mention' -ATTR_ROOM = 'room' -ATTR_USERNAME = 'username' +ATTR_MENTION = "mention" +ATTR_ROOM = "room" +ATTR_USERNAME = "username" -DEFAULT_NAME = 'Gitter messages' -DEFAULT_ROOM = 'home-assistant/home-assistant' +DEFAULT_NAME = "Gitter messages" +DEFAULT_ROOM = "home-assistant/home-assistant" -ICON = 'mdi:message-settings-variant' +ICON = "mdi:message-settings-variant" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ROOM, default=DEFAULT_ROOM): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ROOM, default=DEFAULT_ROOM): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -37,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): gitter = GitterClient(api_key) try: - username = gitter.auth.get_my_id['name'] + username = gitter.auth.get_my_id["name"] except GitterTokenError: _LOGGER.error("Token is not valid") return @@ -56,7 +58,7 @@ class GitterSensor(Entity): self._username = username self._state = None self._mention = 0 - self._unit_of_measurement = 'Msg' + self._unit_of_measurement = "Msg" @property def name(self): @@ -97,8 +99,8 @@ class GitterSensor(Entity): _LOGGER.error(error) return - if 'error' not in data.keys(): - self._mention = len(data['mention']) - self._state = len(data['chat']) + if "error" not in data.keys(): + self._mention = len(data["mention"]) + self._state = len(data["chat"]) else: _LOGGER.error("Not joined: %s", self._room) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 81a6b900e76..3d630378e42 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -6,8 +6,17 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, CONF_SSL, - CONF_VERIFY_SSL, CONF_RESOURCES, STATE_UNAVAILABLE, TEMP_CELSIUS) + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_USERNAME, + CONF_PASSWORD, + CONF_SSL, + CONF_VERIFY_SSL, + CONF_RESOURCES, + STATE_UNAVAILABLE, + TEMP_CELSIUS, +) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -16,53 +25,55 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -CONF_VERSION = 'version' +CONF_VERSION = "version" -DEFAULT_HOST = 'localhost' -DEFAULT_NAME = 'Glances' -DEFAULT_PORT = '61208' +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "Glances" +DEFAULT_PORT = "61208" DEFAULT_VERSION = 2 MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) SENSOR_TYPES = { - 'disk_use_percent': ['Disk used percent', '%', 'mdi:harddisk'], - 'disk_use': ['Disk used', 'GiB', 'mdi:harddisk'], - 'disk_free': ['Disk free', 'GiB', 'mdi:harddisk'], - 'memory_use_percent': ['RAM used percent', '%', 'mdi:memory'], - 'memory_use': ['RAM used', 'MiB', 'mdi:memory'], - 'memory_free': ['RAM free', 'MiB', 'mdi:memory'], - 'swap_use_percent': ['Swap used percent', '%', 'mdi:memory'], - 'swap_use': ['Swap used', 'GiB', 'mdi:memory'], - 'swap_free': ['Swap free', 'GiB', 'mdi:memory'], - 'processor_load': ['CPU load', '15 min', 'mdi:memory'], - 'process_running': ['Running', 'Count', 'mdi:memory'], - 'process_total': ['Total', 'Count', 'mdi:memory'], - 'process_thread': ['Thread', 'Count', 'mdi:memory'], - 'process_sleeping': ['Sleeping', 'Count', 'mdi:memory'], - 'cpu_use_percent': ['CPU used', '%', 'mdi:memory'], - 'cpu_temp': ['CPU Temp', TEMP_CELSIUS, 'mdi:thermometer'], - 'docker_active': ['Containers active', '', 'mdi:docker'], - 'docker_cpu_use': ['Containers CPU used', '%', 'mdi:docker'], - 'docker_memory_use': ['Containers RAM used', 'MiB', 'mdi:docker'], + "disk_use_percent": ["Disk used percent", "%", "mdi:harddisk"], + "disk_use": ["Disk used", "GiB", "mdi:harddisk"], + "disk_free": ["Disk free", "GiB", "mdi:harddisk"], + "memory_use_percent": ["RAM used percent", "%", "mdi:memory"], + "memory_use": ["RAM used", "MiB", "mdi:memory"], + "memory_free": ["RAM free", "MiB", "mdi:memory"], + "swap_use_percent": ["Swap used percent", "%", "mdi:memory"], + "swap_use": ["Swap used", "GiB", "mdi:memory"], + "swap_free": ["Swap free", "GiB", "mdi:memory"], + "processor_load": ["CPU load", "15 min", "mdi:memory"], + "process_running": ["Running", "Count", "mdi:memory"], + "process_total": ["Total", "Count", "mdi:memory"], + "process_thread": ["Thread", "Count", "mdi:memory"], + "process_sleeping": ["Sleeping", "Count", "mdi:memory"], + "cpu_use_percent": ["CPU used", "%", "mdi:memory"], + "cpu_temp": ["CPU Temp", TEMP_CELSIUS, "mdi:thermometer"], + "docker_active": ["Containers active", "", "mdi:docker"], + "docker_cpu_use": ["Containers CPU used", "%", "mdi:docker"], + "docker_memory_use": ["Containers RAM used", "MiB", "mdi:docker"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_RESOURCES, default=['disk_use']): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In([2, 3]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(CONF_RESOURCES, default=["disk_use"]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In([2, 3]), + } +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Glances sensors.""" from glances_api import Glances @@ -78,8 +89,17 @@ async def async_setup_platform( session = async_get_clientsession(hass, verify_ssl) glances = GlancesData( - Glances(hass.loop, session, host=host, port=port, version=version, - username=username, password=password, ssl=ssl)) + Glances( + hass.loop, + session, + host=host, + port=port, + version=version, + username=username, + password=password, + ssl=ssl, + ) + ) await glances.async_update() @@ -107,7 +127,7 @@ class GlancesSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self._name, SENSOR_TYPES[self.type][0]) + return "{} {}".format(self._name, SENSOR_TYPES[self.type][0]) @property def icon(self): @@ -135,80 +155,94 @@ class GlancesSensor(Entity): value = self.glances.api.data if value is not None: - if self.type == 'disk_use_percent': - self._state = value['fs'][0]['percent'] - elif self.type == 'disk_use': - self._state = round(value['fs'][0]['used'] / 1024**3, 1) - elif self.type == 'disk_free': + if self.type == "disk_use_percent": + self._state = value["fs"][0]["percent"] + elif self.type == "disk_use": + self._state = round(value["fs"][0]["used"] / 1024 ** 3, 1) + elif self.type == "disk_free": try: - self._state = round(value['fs'][0]['free'] / 1024**3, 1) + self._state = round(value["fs"][0]["free"] / 1024 ** 3, 1) except KeyError: - self._state = round((value['fs'][0]['size'] - - value['fs'][0]['used']) / 1024**3, 1) - elif self.type == 'memory_use_percent': - self._state = value['mem']['percent'] - elif self.type == 'memory_use': - self._state = round(value['mem']['used'] / 1024**2, 1) - elif self.type == 'memory_free': - self._state = round(value['mem']['free'] / 1024**2, 1) - elif self.type == 'swap_use_percent': - self._state = value['memswap']['percent'] - elif self.type == 'swap_use': - self._state = round(value['memswap']['used'] / 1024**3, 1) - elif self.type == 'swap_free': - self._state = round(value['memswap']['free'] / 1024**3, 1) - elif self.type == 'processor_load': + self._state = round( + (value["fs"][0]["size"] - value["fs"][0]["used"]) / 1024 ** 3, 1 + ) + elif self.type == "memory_use_percent": + self._state = value["mem"]["percent"] + elif self.type == "memory_use": + self._state = round(value["mem"]["used"] / 1024 ** 2, 1) + elif self.type == "memory_free": + self._state = round(value["mem"]["free"] / 1024 ** 2, 1) + elif self.type == "swap_use_percent": + self._state = value["memswap"]["percent"] + elif self.type == "swap_use": + self._state = round(value["memswap"]["used"] / 1024 ** 3, 1) + elif self.type == "swap_free": + self._state = round(value["memswap"]["free"] / 1024 ** 3, 1) + elif self.type == "processor_load": # Windows systems don't provide load details try: - self._state = value['load']['min15'] + self._state = value["load"]["min15"] except KeyError: - self._state = value['cpu']['total'] - elif self.type == 'process_running': - self._state = value['processcount']['running'] - elif self.type == 'process_total': - self._state = value['processcount']['total'] - elif self.type == 'process_thread': - self._state = value['processcount']['thread'] - elif self.type == 'process_sleeping': - self._state = value['processcount']['sleeping'] - elif self.type == 'cpu_use_percent': - self._state = value['quicklook']['cpu'] - elif self.type == 'cpu_temp': - for sensor in value['sensors']: - if sensor['label'] in ['CPU', "CPU Temperature", - "Package id 0", "Physical id 0", - "cpu_thermal 1", "cpu-thermal 1", - "exynos-therm 1", "soc_thermal 1", - "soc-thermal 1", "aml_thermal"]: - self._state = sensor['value'] - elif self.type == 'docker_active': + self._state = value["cpu"]["total"] + elif self.type == "process_running": + self._state = value["processcount"]["running"] + elif self.type == "process_total": + self._state = value["processcount"]["total"] + elif self.type == "process_thread": + self._state = value["processcount"]["thread"] + elif self.type == "process_sleeping": + self._state = value["processcount"]["sleeping"] + elif self.type == "cpu_use_percent": + self._state = value["quicklook"]["cpu"] + elif self.type == "cpu_temp": + for sensor in value["sensors"]: + if sensor["label"] in [ + "CPU", + "CPU Temperature", + "Package id 0", + "Physical id 0", + "cpu_thermal 1", + "cpu-thermal 1", + "exynos-therm 1", + "soc_thermal 1", + "soc-thermal 1", + "aml_thermal", + ]: + self._state = sensor["value"] + elif self.type == "docker_active": count = 0 try: - for container in value['docker']['containers']: - if container['Status'] == 'running' or \ - 'Up' in container['Status']: + for container in value["docker"]["containers"]: + if ( + container["Status"] == "running" + or "Up" in container["Status"] + ): count += 1 self._state = count except KeyError: self._state = count - elif self.type == 'docker_cpu_use': + elif self.type == "docker_cpu_use": cpu_use = 0.0 try: - for container in value['docker']['containers']: - if container['Status'] == 'running' or \ - 'Up' in container['Status']: - cpu_use += container['cpu']['total'] + for container in value["docker"]["containers"]: + if ( + container["Status"] == "running" + or "Up" in container["Status"] + ): + cpu_use += container["cpu"]["total"] self._state = round(cpu_use, 1) except KeyError: self._state = STATE_UNAVAILABLE - elif self.type == 'docker_memory_use': + elif self.type == "docker_memory_use": mem_use = 0.0 try: - for container in value['docker']['containers']: - if container['Status'] == 'running' or \ - 'Up' in container['Status']: - mem_use += container['memory']['usage'] - self._state = round(mem_use / 1024**2, 1) + for container in value["docker"]["containers"]: + if ( + container["Status"] == "running" + or "Up" in container["Status"] + ): + mem_use += container["memory"]["usage"] + self._state = round(mem_use / 1024 ** 2, 1) except KeyError: self._state = STATE_UNAVAILABLE diff --git a/homeassistant/components/gntp/notify.py b/homeassistant/components/gntp/notify.py index 005043c1384..48c02cf0ba8 100644 --- a/homeassistant/components/gntp/notify.py +++ b/homeassistant/components/gntp/notify.py @@ -8,46 +8,60 @@ from homeassistant.const import CONF_PASSWORD, CONF_PORT import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) _LOGGER = logging.getLogger(__name__) -_GNTP_LOGGER = logging.getLogger('gntp') +_GNTP_LOGGER = logging.getLogger("gntp") _GNTP_LOGGER.setLevel(logging.ERROR) -CONF_APP_NAME = 'app_name' -CONF_APP_ICON = 'app_icon' -CONF_HOSTNAME = 'hostname' +CONF_APP_NAME = "app_name" +CONF_APP_ICON = "app_icon" +CONF_HOSTNAME = "hostname" -DEFAULT_APP_NAME = 'HomeAssistant' -DEFAULT_HOST = 'localhost' +DEFAULT_APP_NAME = "HomeAssistant" +DEFAULT_HOST = "localhost" DEFAULT_PORT = 23053 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_APP_NAME, default=DEFAULT_APP_NAME): cv.string, - vol.Optional(CONF_APP_ICON): vol.Url, - vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_APP_NAME, default=DEFAULT_APP_NAME): cv.string, + vol.Optional(CONF_APP_ICON): vol.Url, + vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) def get_service(hass, config, discovery_info=None): """Get the GNTP notification service.""" if config.get(CONF_APP_ICON) is None: - icon_file = os.path.join(os.path.dirname(__file__), "..", "frontend", - "www_static", "icons", "favicon-192x192.png") - with open(icon_file, 'rb') as file: + icon_file = os.path.join( + os.path.dirname(__file__), + "..", + "frontend", + "www_static", + "icons", + "favicon-192x192.png", + ) + with open(icon_file, "rb") as file: app_icon = file.read() else: app_icon = config.get(CONF_APP_ICON) - return GNTPNotificationService(config.get(CONF_APP_NAME), - app_icon, - config.get(CONF_HOSTNAME), - config.get(CONF_PASSWORD), - config.get(CONF_PORT)) + return GNTPNotificationService( + config.get(CONF_APP_NAME), + app_icon, + config.get(CONF_HOSTNAME), + config.get(CONF_PASSWORD), + config.get(CONF_PORT), + ) class GNTPNotificationService(BaseNotificationService): @@ -57,13 +71,14 @@ class GNTPNotificationService(BaseNotificationService): """Initialize the service.""" import gntp.notifier import gntp.errors + self.gntp = gntp.notifier.GrowlNotifier( applicationName=app_name, notifications=["Notification"], applicationIcon=app_icon, hostname=hostname, password=password, - port=port + port=port, ) try: self.gntp.register() @@ -73,6 +88,8 @@ class GNTPNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - self.gntp.notify(noteType="Notification", - title=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), - description=message) + self.gntp.notify( + noteType="Notification", + title=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), + description=message, + ) diff --git a/homeassistant/components/goalfeed/__init__.py b/homeassistant/components/goalfeed/__init__.py index 4a7e4ea980a..3a14eb2831d 100644 --- a/homeassistant/components/goalfeed/__init__.py +++ b/homeassistant/components/goalfeed/__init__.py @@ -9,23 +9,29 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME # Version downgraded due to regression in library # For details: https://github.com/nlsdfnbch/Pysher/issues/38 -DOMAIN = 'goalfeed' +DOMAIN = "goalfeed" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -GOALFEED_HOST = 'feed.goalfeed.ca' -GOALFEED_AUTH_ENDPOINT = 'https://goalfeed.ca/feed/auth' -GOALFEED_APP_ID = 'bfd4ed98c1ff22c04074' +GOALFEED_HOST = "feed.goalfeed.ca" +GOALFEED_AUTH_ENDPOINT = "https://goalfeed.ca/feed/auth" +GOALFEED_APP_ID = "bfd4ed98c1ff22c04074" def setup(hass, config): """Set up the Goalfeed component.""" import pysher + conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) @@ -34,24 +40,25 @@ def setup(hass, config): """Handle goal events.""" goal = json.loads(json.loads(data)) - hass.bus.fire('goal', event_data=goal) + hass.bus.fire("goal", event_data=goal) def connect_handler(data): """Handle connection.""" post_data = { - 'username': username, - 'password': password, - 'connection_info': data} - resp = requests.post( - GOALFEED_AUTH_ENDPOINT, post_data, timeout=30).json() + "username": username, + "password": password, + "connection_info": data, + } + resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data, timeout=30).json() - channel = pusher.subscribe('private-goals', resp['auth']) - channel.bind('goal', goal_handler) + channel = pusher.subscribe("private-goals", resp["auth"]) + channel.bind("goal", goal_handler) - pusher = pysher.Pusher(GOALFEED_APP_ID, secure=False, port=8080, - custom_host=GOALFEED_HOST) + pusher = pysher.Pusher( + GOALFEED_APP_ID, secure=False, port=8080, custom_host=GOALFEED_HOST + ) - pusher.connection.bind('pusher:connection_established', connect_handler) + pusher.connection.bind("pusher:connection_established", connect_handler) pusher.connect() return True diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 610c131bda5..26aecee2504 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -3,26 +3,31 @@ import logging import voluptuous as vol -from homeassistant.components.cover import ( - CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE) +from homeassistant.components.cover import CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, - CONF_IP_ADDRESS, CONF_NAME) + CONF_USERNAME, + CONF_PASSWORD, + STATE_CLOSED, + CONF_IP_ADDRESS, + CONF_NAME, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'gogogate2' +DEFAULT_NAME = "gogogate2" -NOTIFICATION_ID = 'gogogate2_notification' -NOTIFICATION_TITLE = 'Gogogate2 Cover Setup' +NOTIFICATION_ID = "gogogate2_notification" +NOTIFICATION_TITLE = "Gogogate2 Cover Setup" -COVER_SCHEMA = vol.Schema({ - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +COVER_SCHEMA = vol.Schema( + { + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -39,20 +44,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: devices = mygogogate2.get_devices() if devices is False: - raise ValueError( - "Username or Password is incorrect or no devices found") + raise ValueError("Username or Password is incorrect or no devices found") - add_entities(MyGogogate2Device( - mygogogate2, door, name) for door in devices) + add_entities(MyGogogate2Device(mygogogate2, door, name) for door in devices) except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) hass.components.persistent_notification.create( - 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), + "Error: {}
" + "You will need to restart hass after fixing." + "".format(ex), title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) + notification_id=NOTIFICATION_ID, + ) class MyGogogate2Device(CoverDevice): @@ -61,9 +65,9 @@ class MyGogogate2Device(CoverDevice): def __init__(self, mygogogate2, device, name): """Initialize with API object, device id.""" self.mygogogate2 = mygogogate2 - self.device_id = device['door'] - self._name = name or device['name'] - self._status = device['status'] + self.device_id = device["door"] + self._name = name or device["name"] + self._status = device["status"] self._available = None @property @@ -79,7 +83,7 @@ class MyGogogate2Device(CoverDevice): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return 'garage' + return "garage" @property def supported_features(self): diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index dc1c7b1d5e5..41901a71704 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -15,78 +15,89 @@ from homeassistant.util import convert, dt _LOGGER = logging.getLogger(__name__) -DOMAIN = 'google' -ENTITY_ID_FORMAT = DOMAIN + '.{}' +DOMAIN = "google" +ENTITY_ID_FORMAT = DOMAIN + ".{}" -CONF_CLIENT_ID = 'client_id' -CONF_CLIENT_SECRET = 'client_secret' -CONF_TRACK_NEW = 'track_new_calendar' +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" +CONF_TRACK_NEW = "track_new_calendar" -CONF_CAL_ID = 'cal_id' -CONF_DEVICE_ID = 'device_id' -CONF_NAME = 'name' -CONF_ENTITIES = 'entities' -CONF_TRACK = 'track' -CONF_SEARCH = 'search' -CONF_OFFSET = 'offset' -CONF_IGNORE_AVAILABILITY = 'ignore_availability' -CONF_MAX_RESULTS = 'max_results' +CONF_CAL_ID = "cal_id" +CONF_DEVICE_ID = "device_id" +CONF_NAME = "name" +CONF_ENTITIES = "entities" +CONF_TRACK = "track" +CONF_SEARCH = "search" +CONF_OFFSET = "offset" +CONF_IGNORE_AVAILABILITY = "ignore_availability" +CONF_MAX_RESULTS = "max_results" DEFAULT_CONF_TRACK_NEW = True -DEFAULT_CONF_OFFSET = '!!' +DEFAULT_CONF_OFFSET = "!!" -EVENT_CALENDAR_ID = 'calendar_id' -EVENT_DESCRIPTION = 'description' -EVENT_END_CONF = 'end' -EVENT_END_DATE = 'end_date' -EVENT_END_DATETIME = 'end_date_time' -EVENT_IN = 'in' -EVENT_IN_DAYS = 'days' -EVENT_IN_WEEKS = 'weeks' -EVENT_START_CONF = 'start' -EVENT_START_DATE = 'start_date' -EVENT_START_DATETIME = 'start_date_time' -EVENT_SUMMARY = 'summary' -EVENT_TYPES_CONF = 'event_types' +EVENT_CALENDAR_ID = "calendar_id" +EVENT_DESCRIPTION = "description" +EVENT_END_CONF = "end" +EVENT_END_DATE = "end_date" +EVENT_END_DATETIME = "end_date_time" +EVENT_IN = "in" +EVENT_IN_DAYS = "days" +EVENT_IN_WEEKS = "weeks" +EVENT_START_CONF = "start" +EVENT_START_DATE = "start_date" +EVENT_START_DATETIME = "start_date_time" +EVENT_SUMMARY = "summary" +EVENT_TYPES_CONF = "event_types" -NOTIFICATION_ID = 'google_calendar_notification' +NOTIFICATION_ID = "google_calendar_notification" NOTIFICATION_TITLE = "Google Calendar Setup" GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors" -SERVICE_SCAN_CALENDARS = 'scan_for_calendars' -SERVICE_FOUND_CALENDARS = 'found_calendar' -SERVICE_ADD_EVENT = 'add_event' +SERVICE_SCAN_CALENDARS = "scan_for_calendars" +SERVICE_FOUND_CALENDARS = "found_calendar" +SERVICE_ADD_EVENT = "add_event" -DATA_INDEX = 'google_calendars' +DATA_INDEX = "google_calendars" -YAML_DEVICES = '{}_calendars.yaml'.format(DOMAIN) -SCOPES = 'https://www.googleapis.com/auth/calendar' +YAML_DEVICES = "{}_calendars.yaml".format(DOMAIN) +SCOPES = "https://www.googleapis.com/auth/calendar" -TOKEN_FILE = '.{}.token'.format(DOMAIN) +TOKEN_FILE = ".{}.token".format(DOMAIN) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_TRACK_NEW): cv.boolean, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_TRACK_NEW): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -_SINGLE_CALSEARCH_CONFIG = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean, - vol.Optional(CONF_OFFSET): cv.string, - vol.Optional(CONF_SEARCH): cv.string, - vol.Optional(CONF_TRACK): cv.boolean, - vol.Optional(CONF_MAX_RESULTS): cv.positive_int, -}) +_SINGLE_CALSEARCH_CONFIG = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean, + vol.Optional(CONF_OFFSET): cv.string, + vol.Optional(CONF_SEARCH): cv.string, + vol.Optional(CONF_TRACK): cv.boolean, + vol.Optional(CONF_MAX_RESULTS): cv.positive_int, + } +) -DEVICE_SCHEMA = vol.Schema({ - vol.Required(CONF_CAL_ID): cv.string, - vol.Required(CONF_ENTITIES, None): - vol.All(cv.ensure_list, [_SINGLE_CALSEARCH_CONFIG]), -}, extra=vol.ALLOW_EXTRA) +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_CAL_ID): cv.string, + vol.Required(CONF_ENTITIES, None): vol.All( + cv.ensure_list, [_SINGLE_CALSEARCH_CONFIG] + ), + }, + extra=vol.ALLOW_EXTRA, +) _EVENT_IN_TYPES = vol.Schema( { @@ -104,8 +115,7 @@ ADD_EVENT_SERVICE_SCHEMA = vol.Schema( vol.Exclusive(EVENT_END_DATE, EVENT_END_CONF): cv.date, vol.Exclusive(EVENT_START_DATETIME, EVENT_START_CONF): cv.datetime, vol.Exclusive(EVENT_END_DATETIME, EVENT_END_CONF): cv.datetime, - vol.Exclusive(EVENT_IN, EVENT_START_CONF, EVENT_END_CONF): - _EVENT_IN_TYPES + vol.Exclusive(EVENT_IN, EVENT_START_CONF, EVENT_END_CONF): _EVENT_IN_TYPES, } ) @@ -117,42 +127,47 @@ def do_authentication(hass, hass_config, config): until we have an access token. """ from oauth2client.client import ( - OAuth2WebServerFlow, OAuth2DeviceCodeError, FlowExchangeError) + OAuth2WebServerFlow, + OAuth2DeviceCodeError, + FlowExchangeError, + ) from oauth2client.file import Storage oauth = OAuth2WebServerFlow( client_id=config[CONF_CLIENT_ID], client_secret=config[CONF_CLIENT_SECRET], - scope='https://www.googleapis.com/auth/calendar', - redirect_uri='Home-Assistant.io', + scope="https://www.googleapis.com/auth/calendar", + redirect_uri="Home-Assistant.io", ) try: dev_flow = oauth.step1_get_device_and_user_codes() except OAuth2DeviceCodeError as err: hass.components.persistent_notification.create( - 'Error: {}
You will need to restart hass after fixing.' - ''.format(err), + "Error: {}
You will need to restart hass after fixing." "".format(err), title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) + notification_id=NOTIFICATION_ID, + ) return False hass.components.persistent_notification.create( - 'In order to authorize Home-Assistant to view your calendars ' + "In order to authorize Home-Assistant to view your calendars " 'you must visit:
{} and enter ' - 'code: {}'.format(dev_flow.verification_url, - dev_flow.verification_url, - dev_flow.user_code), - title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID + "code: {}".format( + dev_flow.verification_url, dev_flow.verification_url, dev_flow.user_code + ), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID, ) def step2_exchange(now): """Keep trying to validate the user_code until it expires.""" if now >= dt.as_local(dev_flow.user_code_expiry): hass.components.persistent_notification.create( - 'Authentication code expired, please restart ' - 'Home-Assistant and try again', + "Authentication code expired, please restart " + "Home-Assistant and try again", title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) + notification_id=NOTIFICATION_ID, + ) listener() try: @@ -166,12 +181,15 @@ def do_authentication(hass, hass_config, config): do_setup(hass, hass_config, config) listener() hass.components.persistent_notification.create( - 'We are all setup now. Check {} for calendars that have ' - 'been found'.format(YAML_DEVICES), - title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) + "We are all setup now. Check {} for calendars that have " + "been found".format(YAML_DEVICES), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID, + ) listener = track_time_change( - hass, step2_exchange, second=range(0, 60, dev_flow.interval)) + hass, step2_exchange, second=range(0, 60, dev_flow.interval) + ) return True @@ -207,9 +225,9 @@ def check_correct_scopes(token_file): return True -def setup_services(hass, hass_config, track_new_found_calendars, - calendar_service): +def setup_services(hass, hass_config, track_new_found_calendars, calendar_service): """Set up the service listeners.""" + def _found_calendar(call): """Check if we know about a calendar and generate PLATFORM_DISCOVER.""" calendar = get_calendar_info(hass, call.data) @@ -219,29 +237,29 @@ def setup_services(hass, hass_config, track_new_found_calendars, hass.data[DATA_INDEX].update({calendar[CONF_CAL_ID]: calendar}) update_config( - hass.config.path(YAML_DEVICES), - hass.data[DATA_INDEX][calendar[CONF_CAL_ID]] + hass.config.path(YAML_DEVICES), hass.data[DATA_INDEX][calendar[CONF_CAL_ID]] ) - discovery.load_platform(hass, 'calendar', DOMAIN, - hass.data[DATA_INDEX][calendar[CONF_CAL_ID]], - hass_config) + discovery.load_platform( + hass, + "calendar", + DOMAIN, + hass.data[DATA_INDEX][calendar[CONF_CAL_ID]], + hass_config, + ) - hass.services.register( - DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar) + hass.services.register(DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar) def _scan_for_calendars(service): """Scan for new calendars.""" service = calendar_service.get() cal_list = service.calendarList() - calendars = cal_list.list().execute()['items'] + calendars = cal_list.list().execute()["items"] for calendar in calendars: - calendar['track'] = track_new_found_calendars - hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS, - calendar) + calendar["track"] = track_new_found_calendars + hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar) - hass.services.register( - DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) + hass.services.register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) def _add_event(call): """Add a new event to calendar.""" @@ -253,45 +271,40 @@ def setup_services(hass, hass_config, track_new_found_calendars, if EVENT_IN_DAYS in call.data[EVENT_IN]: now = datetime.now() - start_in = now + timedelta( - days=call.data[EVENT_IN][EVENT_IN_DAYS]) + start_in = now + timedelta(days=call.data[EVENT_IN][EVENT_IN_DAYS]) end_in = start_in + timedelta(days=1) - start = {'date': start_in.strftime('%Y-%m-%d')} - end = {'date': end_in.strftime('%Y-%m-%d')} + start = {"date": start_in.strftime("%Y-%m-%d")} + end = {"date": end_in.strftime("%Y-%m-%d")} elif EVENT_IN_WEEKS in call.data[EVENT_IN]: now = datetime.now() - start_in = now + timedelta( - weeks=call.data[EVENT_IN][EVENT_IN_WEEKS]) + start_in = now + timedelta(weeks=call.data[EVENT_IN][EVENT_IN_WEEKS]) end_in = start_in + timedelta(days=1) - start = {'date': start_in.strftime('%Y-%m-%d')} - end = {'date': end_in.strftime('%Y-%m-%d')} + start = {"date": start_in.strftime("%Y-%m-%d")} + end = {"date": end_in.strftime("%Y-%m-%d")} elif EVENT_START_DATE in call.data: - start = {'date': str(call.data[EVENT_START_DATE])} - end = {'date': str(call.data[EVENT_END_DATE])} + start = {"date": str(call.data[EVENT_START_DATE])} + end = {"date": str(call.data[EVENT_END_DATE])} elif EVENT_START_DATETIME in call.data: - start_dt = str(call.data[EVENT_START_DATETIME] - .strftime('%Y-%m-%dT%H:%M:%S')) - end_dt = str(call.data[EVENT_END_DATETIME] - .strftime('%Y-%m-%dT%H:%M:%S')) - start = {'dateTime': start_dt, - 'timeZone': str(hass.config.time_zone)} - end = {'dateTime': end_dt, - 'timeZone': str(hass.config.time_zone)} + start_dt = str( + call.data[EVENT_START_DATETIME].strftime("%Y-%m-%dT%H:%M:%S") + ) + end_dt = str(call.data[EVENT_END_DATETIME].strftime("%Y-%m-%dT%H:%M:%S")) + start = {"dateTime": start_dt, "timeZone": str(hass.config.time_zone)} + end = {"dateTime": end_dt, "timeZone": str(hass.config.time_zone)} event = { - 'summary': call.data[EVENT_SUMMARY], - 'description': call.data[EVENT_DESCRIPTION], - 'start': start, - 'end': end, + "summary": call.data[EVENT_SUMMARY], + "description": call.data[EVENT_DESCRIPTION], + "start": start, + "end": end, } - service_data = {'calendarId': call.data[EVENT_CALENDAR_ID], - 'body': event} + service_data = {"calendarId": call.data[EVENT_CALENDAR_ID], "body": event} event = service.events().insert(**service_data).execute() hass.services.register( @@ -306,14 +319,13 @@ def do_setup(hass, hass_config, config): hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES)) calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) - track_new_found_calendars = convert(config.get(CONF_TRACK_NEW), - bool, DEFAULT_CONF_TRACK_NEW) - setup_services(hass, hass_config, track_new_found_calendars, - calendar_service) + track_new_found_calendars = convert( + config.get(CONF_TRACK_NEW), bool, DEFAULT_CONF_TRACK_NEW + ) + setup_services(hass, hass_config, track_new_found_calendars, calendar_service) for calendar in hass.data[DATA_INDEX].values(): - discovery.load_platform(hass, 'calendar', DOMAIN, calendar, - hass_config) + discovery.load_platform(hass, "calendar", DOMAIN, calendar, hass_config) # Look for any new calendars hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None) @@ -332,24 +344,31 @@ class GoogleCalendarService: import httplib2 from oauth2client.file import Storage from googleapiclient import discovery as google_discovery + credentials = Storage(self.token_file).get() http = credentials.authorize(httplib2.Http()) service = google_discovery.build( - 'calendar', 'v3', http=http, cache_discovery=False) + "calendar", "v3", http=http, cache_discovery=False + ) return service def get_calendar_info(hass, calendar): """Convert data from Google into DEVICE_SCHEMA.""" - calendar_info = DEVICE_SCHEMA({ - CONF_CAL_ID: calendar['id'], - CONF_ENTITIES: [{ - CONF_TRACK: calendar['track'], - CONF_NAME: calendar['summary'], - CONF_DEVICE_ID: generate_entity_id( - '{}', calendar['summary'], hass=hass), - }] - }) + calendar_info = DEVICE_SCHEMA( + { + CONF_CAL_ID: calendar["id"], + CONF_ENTITIES: [ + { + CONF_TRACK: calendar["track"], + CONF_NAME: calendar["summary"], + CONF_DEVICE_ID: generate_entity_id( + "{}", calendar["summary"], hass=hass + ), + } + ], + } + ) return calendar_info @@ -361,8 +380,7 @@ def load_config(path): data = yaml.safe_load(file) for calendar in data: try: - calendars.update({calendar[CONF_CAL_ID]: - DEVICE_SCHEMA(calendar)}) + calendars.update({calendar[CONF_CAL_ID]: DEVICE_SCHEMA(calendar)}) except VoluptuousError as exception: # keep going _LOGGER.warning("Calendar Invalid Data: %s", exception) @@ -375,6 +393,6 @@ def load_config(path): def update_config(path, calendar): """Write the google_calendar_devices.yaml.""" - with open(path, 'a') as out: - out.write('\n') + with open(path, "a") as out: + out.write("\n") yaml.dump([calendar], out, default_flow_style=False) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 66d6b61f75b..31e9f186a4e 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -4,21 +4,35 @@ from datetime import timedelta import logging from homeassistant.components.calendar import ( - ENTITY_ID_FORMAT, CalendarEventDevice, calculate_offset, is_offset_reached) + ENTITY_ID_FORMAT, + CalendarEventDevice, + calculate_offset, + is_offset_reached, +) from homeassistant.helpers.entity import generate_entity_id from homeassistant.util import Throttle, dt from . import ( - CONF_CAL_ID, CONF_DEVICE_ID, CONF_ENTITIES, CONF_IGNORE_AVAILABILITY, - CONF_MAX_RESULTS, CONF_NAME, CONF_OFFSET, CONF_SEARCH, CONF_TRACK, - DEFAULT_CONF_OFFSET, TOKEN_FILE, GoogleCalendarService) + CONF_CAL_ID, + CONF_DEVICE_ID, + CONF_ENTITIES, + CONF_IGNORE_AVAILABILITY, + CONF_MAX_RESULTS, + CONF_NAME, + CONF_OFFSET, + CONF_SEARCH, + CONF_TRACK, + DEFAULT_CONF_OFFSET, + TOKEN_FILE, + GoogleCalendarService, +) _LOGGER = logging.getLogger(__name__) DEFAULT_GOOGLE_SEARCH_PARAMS = { - 'orderBy': 'startTime', - 'maxResults': 5, - 'singleEvents': True, + "orderBy": "startTime", + "maxResults": 5, + "singleEvents": True, } MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) @@ -38,9 +52,11 @@ def setup_platform(hass, config, add_entities, disc_info=None): if not data[CONF_TRACK]: continue entity_id = generate_entity_id( - ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass) + ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass + ) entity = GoogleCalendarEventDevice( - calendar_service, disc_info[CONF_CAL_ID], data, entity_id) + calendar_service, disc_info[CONF_CAL_ID], data, entity_id + ) entities.append(entity) add_entities(entities, True) @@ -52,9 +68,12 @@ class GoogleCalendarEventDevice(CalendarEventDevice): def __init__(self, calendar_service, calendar, data, entity_id): """Create the Calendar event device.""" self.data = GoogleCalendarData( - calendar_service, calendar, - data.get(CONF_SEARCH), data.get(CONF_IGNORE_AVAILABILITY), - data.get(CONF_MAX_RESULTS)) + calendar_service, + calendar, + data.get(CONF_SEARCH), + data.get(CONF_IGNORE_AVAILABILITY), + data.get(CONF_MAX_RESULTS), + ) self._event = None self._name = data[CONF_NAME] self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) @@ -64,9 +83,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice): @property def device_state_attributes(self): """Return the device state attributes.""" - return { - 'offset_reached': self._offset_reached, - } + return {"offset_reached": self._offset_reached} @property def event(self): @@ -97,8 +114,9 @@ class GoogleCalendarEventDevice(CalendarEventDevice): class GoogleCalendarData: """Class to utilize calendar service object to get next event.""" - def __init__(self, calendar_service, calendar_id, search, - ignore_availability, max_results): + def __init__( + self, calendar_service, calendar_id, search, ignore_availability, max_results + ): """Set up how we are going to search the google calendar.""" self.calendar_service = calendar_service self.calendar_id = calendar_id @@ -117,33 +135,30 @@ class GoogleCalendarData: _LOGGER.error("Unable to connect to Google") return None, None params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) - params['calendarId'] = self.calendar_id + params["calendarId"] = self.calendar_id if self.max_results: - params['maxResults'] = self.max_results + params["maxResults"] = self.max_results if self.search: - params['q'] = self.search + params["q"] = self.search return service, params async def async_get_events(self, hass, start_date, end_date): """Get all events in a specific time frame.""" - service, params = await hass.async_add_executor_job( - self._prepare_query) + service, params = await hass.async_add_executor_job(self._prepare_query) if service is None: return [] - params['timeMin'] = start_date.isoformat('T') - params['timeMax'] = end_date.isoformat('T') + params["timeMin"] = start_date.isoformat("T") + params["timeMax"] = end_date.isoformat("T") events = await hass.async_add_executor_job(service.events) - result = await hass.async_add_executor_job( - events.list(**params).execute) + result = await hass.async_add_executor_job(events.list(**params).execute) - items = result.get('items', []) + items = result.get("items", []) event_list = [] for item in items: - if (not self.ignore_availability - and 'transparency' in item.keys()): - if item['transparency'] == 'opaque': + if not self.ignore_availability and "transparency" in item.keys(): + if item["transparency"] == "opaque": event_list.append(item) else: event_list.append(item) @@ -155,18 +170,17 @@ class GoogleCalendarData: service, params = self._prepare_query() if service is None: return - params['timeMin'] = dt.now().isoformat('T') + params["timeMin"] = dt.now().isoformat("T") events = service.events() result = events.list(**params).execute() - items = result.get('items', []) + items = result.get("items", []) new_event = None for item in items: - if (not self.ignore_availability - and 'transparency' in item.keys()): - if item['transparency'] == 'opaque': + if not self.ignore_availability and "transparency" in item.keys(): + if item["transparency"] == "opaque": new_event = item break else: diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 1e0ac6d9363..61e0c70b6b3 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -16,11 +16,21 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - DOMAIN, CONF_PROJECT_ID, CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, - CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS, CONF_API_KEY, - SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG, - CONF_EXPOSE, CONF_ALIASES, CONF_ROOM_HINT, CONF_ALLOW_UNLOCK, - CONF_SECURE_DEVICES_PIN + DOMAIN, + CONF_PROJECT_ID, + CONF_EXPOSE_BY_DEFAULT, + DEFAULT_EXPOSE_BY_DEFAULT, + CONF_EXPOSED_DOMAINS, + DEFAULT_EXPOSED_DOMAINS, + CONF_API_KEY, + SERVICE_REQUEST_SYNC, + REQUEST_SYNC_BASE_URL, + CONF_ENTITY_CONFIG, + CONF_EXPOSE, + CONF_ALIASES, + CONF_ROOM_HINT, + CONF_ALLOW_UNLOCK, + CONF_SECURE_DEVICES_PIN, ) from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 from .const import EVENT_QUERY_RECEIVED # noqa: F401 @@ -28,31 +38,37 @@ from .http import async_register_http _LOGGER = logging.getLogger(__name__) -ENTITY_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_EXPOSE): cv.boolean, - vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_ROOM_HINT): cv.string, -}) +ENTITY_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_EXPOSE): cv.boolean, + vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_ROOM_HINT): cv.string, + } +) GOOGLE_ASSISTANT_SCHEMA = vol.All( - cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version='0.95'), - vol.Schema({ - vol.Required(CONF_PROJECT_ID): cv.string, - vol.Optional(CONF_EXPOSE_BY_DEFAULT, - default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean, - vol.Optional(CONF_EXPOSED_DOMAINS, - default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}, - vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean, - # str on purpose, makes sure it is configured correctly. - vol.Optional(CONF_SECURE_DEVICES_PIN): str, - }, extra=vol.PREVENT_EXTRA)) + cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version="0.95"), + vol.Schema( + { + vol.Required(CONF_PROJECT_ID): cv.string, + vol.Optional( + CONF_EXPOSE_BY_DEFAULT, default=DEFAULT_EXPOSE_BY_DEFAULT + ): cv.boolean, + vol.Optional( + CONF_EXPOSED_DOMAINS, default=DEFAULT_EXPOSED_DOMAINS + ): cv.ensure_list, + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}, + vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean, + # str on purpose, makes sure it is configured correctly. + vol.Optional(CONF_SECURE_DEVICES_PIN): str, + }, + extra=vol.PREVENT_EXTRA, + ), +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: GOOGLE_ASSISTANT_SCHEMA -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema({DOMAIN: GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA) async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): @@ -66,24 +82,24 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): websession = async_get_clientsession(hass) try: with async_timeout.timeout(15): - agent_user_id = call.data.get('agent_user_id') or \ - call.context.user_id + agent_user_id = call.data.get("agent_user_id") or call.context.user_id res = await websession.post( REQUEST_SYNC_BASE_URL, - params={'key': api_key}, - json={'agent_user_id': agent_user_id}) + params={"key": api_key}, + json={"agent_user_id": agent_user_id}, + ) _LOGGER.info("Submitted request_sync request to Google") res.raise_for_status() except aiohttp.ClientResponseError: body = await res.read() - _LOGGER.error( - 'request_sync request failed: %d %s', res.status, body) + _LOGGER.error("request_sync request failed: %d %s", res.status, body) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Could not contact Google for request_sync") # Register service only if api key is provided if api_key is not None: hass.services.async_register( - DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler) + DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler + ) return True diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index ebded79447e..1d266d23d3f 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -16,48 +16,60 @@ from homeassistant.components import ( switch, vacuum, ) -DOMAIN = 'google_assistant' -GOOGLE_ASSISTANT_API_ENDPOINT = '/api/google_assistant' +DOMAIN = "google_assistant" -CONF_EXPOSE = 'expose' -CONF_ENTITY_CONFIG = 'entity_config' -CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' -CONF_EXPOSED_DOMAINS = 'exposed_domains' -CONF_PROJECT_ID = 'project_id' -CONF_ALIASES = 'aliases' -CONF_API_KEY = 'api_key' -CONF_ROOM_HINT = 'room' -CONF_ALLOW_UNLOCK = 'allow_unlock' -CONF_SECURE_DEVICES_PIN = 'secure_devices_pin' +GOOGLE_ASSISTANT_API_ENDPOINT = "/api/google_assistant" + +CONF_EXPOSE = "expose" +CONF_ENTITY_CONFIG = "entity_config" +CONF_EXPOSE_BY_DEFAULT = "expose_by_default" +CONF_EXPOSED_DOMAINS = "exposed_domains" +CONF_PROJECT_ID = "project_id" +CONF_ALIASES = "aliases" +CONF_API_KEY = "api_key" +CONF_ROOM_HINT = "room" +CONF_ALLOW_UNLOCK = "allow_unlock" +CONF_SECURE_DEVICES_PIN = "secure_devices_pin" DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ - 'climate', 'cover', 'fan', 'group', 'input_boolean', 'light', - 'media_player', 'scene', 'script', 'switch', 'vacuum', 'lock', - 'binary_sensor', 'sensor' + "climate", + "cover", + "fan", + "group", + "input_boolean", + "light", + "media_player", + "scene", + "script", + "switch", + "vacuum", + "lock", + "binary_sensor", + "sensor", ] -PREFIX_TYPES = 'action.devices.types.' -TYPE_CAMERA = PREFIX_TYPES + 'CAMERA' -TYPE_LIGHT = PREFIX_TYPES + 'LIGHT' -TYPE_SWITCH = PREFIX_TYPES + 'SWITCH' -TYPE_VACUUM = PREFIX_TYPES + 'VACUUM' -TYPE_SCENE = PREFIX_TYPES + 'SCENE' -TYPE_FAN = PREFIX_TYPES + 'FAN' -TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT' -TYPE_LOCK = PREFIX_TYPES + 'LOCK' -TYPE_BLINDS = PREFIX_TYPES + 'BLINDS' -TYPE_GARAGE = PREFIX_TYPES + 'GARAGE' -TYPE_OUTLET = PREFIX_TYPES + 'OUTLET' -TYPE_SENSOR = PREFIX_TYPES + 'SENSOR' -TYPE_DOOR = PREFIX_TYPES + 'DOOR' -TYPE_TV = PREFIX_TYPES + 'TV' -TYPE_SPEAKER = PREFIX_TYPES + 'SPEAKER' +PREFIX_TYPES = "action.devices.types." +TYPE_CAMERA = PREFIX_TYPES + "CAMERA" +TYPE_LIGHT = PREFIX_TYPES + "LIGHT" +TYPE_SWITCH = PREFIX_TYPES + "SWITCH" +TYPE_VACUUM = PREFIX_TYPES + "VACUUM" +TYPE_SCENE = PREFIX_TYPES + "SCENE" +TYPE_FAN = PREFIX_TYPES + "FAN" +TYPE_THERMOSTAT = PREFIX_TYPES + "THERMOSTAT" +TYPE_LOCK = PREFIX_TYPES + "LOCK" +TYPE_BLINDS = PREFIX_TYPES + "BLINDS" +TYPE_GARAGE = PREFIX_TYPES + "GARAGE" +TYPE_OUTLET = PREFIX_TYPES + "OUTLET" +TYPE_SENSOR = PREFIX_TYPES + "SENSOR" +TYPE_DOOR = PREFIX_TYPES + "DOOR" +TYPE_TV = PREFIX_TYPES + "TV" +TYPE_SPEAKER = PREFIX_TYPES + "SPEAKER" -SERVICE_REQUEST_SYNC = 'request_sync' -HOMEGRAPH_URL = 'https://homegraph.googleapis.com/' -REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + 'v1/devices:requestSync' +SERVICE_REQUEST_SYNC = "request_sync" +HOMEGRAPH_URL = "https://homegraph.googleapis.com/" +REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + "v1/devices:requestSync" # Error codes used for SmartHomeError class # https://developers.google.com/actions/reference/smarthome/errors-exceptions @@ -65,20 +77,20 @@ ERR_DEVICE_OFFLINE = "deviceOffline" ERR_DEVICE_NOT_FOUND = "deviceNotFound" ERR_VALUE_OUT_OF_RANGE = "valueOutOfRange" ERR_NOT_SUPPORTED = "notSupported" -ERR_PROTOCOL_ERROR = 'protocolError' -ERR_UNKNOWN_ERROR = 'unknownError' -ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported' +ERR_PROTOCOL_ERROR = "protocolError" +ERR_UNKNOWN_ERROR = "unknownError" +ERR_FUNCTION_NOT_SUPPORTED = "functionNotSupported" -ERR_CHALLENGE_NEEDED = 'challengeNeeded' -ERR_CHALLENGE_NOT_SETUP = 'challengeFailedNotSetup' -ERR_TOO_MANY_FAILED_ATTEMPTS = 'tooManyFailedAttempts' -ERR_PIN_INCORRECT = 'pinIncorrect' -ERR_USER_CANCELLED = 'userCancelled' +ERR_CHALLENGE_NEEDED = "challengeNeeded" +ERR_CHALLENGE_NOT_SETUP = "challengeFailedNotSetup" +ERR_TOO_MANY_FAILED_ATTEMPTS = "tooManyFailedAttempts" +ERR_PIN_INCORRECT = "pinIncorrect" +ERR_USER_CANCELLED = "userCancelled" # Event types -EVENT_COMMAND_RECEIVED = 'google_assistant_command' -EVENT_QUERY_RECEIVED = 'google_assistant_query' -EVENT_SYNC_RECEIVED = 'google_assistant_sync' +EVENT_COMMAND_RECEIVED = "google_assistant_command" +EVENT_QUERY_RECEIVED = "google_assistant_query" +EVENT_SYNC_RECEIVED = "google_assistant_sync" DOMAIN_TO_GOOGLE_TYPES = { camera.DOMAIN: TYPE_CAMERA, @@ -102,8 +114,7 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (switch.DOMAIN, switch.DEVICE_CLASS_SWITCH): TYPE_SWITCH, (switch.DOMAIN, switch.DEVICE_CLASS_OUTLET): TYPE_OUTLET, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_DOOR): TYPE_DOOR, - (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_GARAGE_DOOR): - TYPE_GARAGE, + (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_GARAGE_DOOR): TYPE_GARAGE, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_LOCK): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR, @@ -112,6 +123,6 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR, } -CHALLENGE_ACK_NEEDED = 'ackNeeded' -CHALLENGE_PIN_NEEDED = 'pinNeeded' -CHALLENGE_FAILED_PIN_NEEDED = 'challengeFailedPinNeeded' +CHALLENGE_ACK_NEEDED = "ackNeeded" +CHALLENGE_PIN_NEEDED = "pinNeeded" +CHALLENGE_FAILED_PIN_NEEDED = "challengeFailedPinNeeded" diff --git a/homeassistant/components/google_assistant/error.py b/homeassistant/components/google_assistant/error.py index 3aef1e9408d..a2ff72511c7 100644 --- a/homeassistant/components/google_assistant/error.py +++ b/homeassistant/components/google_assistant/error.py @@ -15,9 +15,7 @@ class SmartHomeError(Exception): def to_response(self): """Convert to a response format.""" - return { - 'errorCode': self.code - } + return {"errorCode": self.code} class ChallengeNeeded(SmartHomeError): @@ -28,15 +26,14 @@ class ChallengeNeeded(SmartHomeError): def __init__(self, challenge_type): """Initialize challenge needed error.""" - super().__init__(ERR_CHALLENGE_NEEDED, - 'Challenge needed: {}'.format(challenge_type)) + super().__init__( + ERR_CHALLENGE_NEEDED, "Challenge needed: {}".format(challenge_type) + ) self.challenge_type = challenge_type def to_response(self): """Convert to a response format.""" return { - 'errorCode': self.code, - 'challengeNeeded': { - 'type': self.challenge_type - } + "errorCode": self.code, + "challengeNeeded": {"type": self.challenge_type}, } diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 87c4fb78f3a..066ed0057ac 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -5,14 +5,20 @@ from typing import List from homeassistant.core import Context, callback from homeassistant.const import ( - CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES, - ATTR_DEVICE_CLASS, CLOUD_NEVER_EXPOSED_ENTITIES + CONF_NAME, + STATE_UNAVAILABLE, + ATTR_SUPPORTED_FEATURES, + ATTR_DEVICE_CLASS, + CLOUD_NEVER_EXPOSED_ENTITIES, ) from . import trait from .const import ( - DOMAIN_TO_GOOGLE_TYPES, CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED, - DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT + DOMAIN_TO_GOOGLE_TYPES, + CONF_ALIASES, + ERR_FUNCTION_NOT_SUPPORTED, + DEVICE_CLASS_TO_GOOGLE_TYPES, + CONF_ROOM_HINT, ) from .error import SmartHomeError @@ -88,9 +94,11 @@ class GoogleEntity: features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) device_class = state.attributes.get(ATTR_DEVICE_CLASS) - self._traits = [Trait(self.hass, state, self.config) - for Trait in trait.TRAITS - if Trait.supported(domain, features, device_class)] + self._traits = [ + Trait(self.hass, state, self.config) + for Trait in trait.TRAITS + if Trait.supported(domain, features, device_class) + ] return self._traits @callback @@ -106,8 +114,9 @@ class GoogleEntity: features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) device_class = state.attributes.get(ATTR_DEVICE_CLASS) - return any(trait.might_2fa(domain, features, device_class) - for trait in self.traits()) + return any( + trait.might_2fa(domain, features, device_class) for trait in self.traits() + ) async def sync_serialize(self): """Serialize entity for a SYNC response. @@ -123,31 +132,28 @@ class GoogleEntity: traits = self.traits() - device_type = get_google_type(domain, - device_class) + device_type = get_google_type(domain, device_class) device = { - 'id': state.entity_id, - 'name': { - 'name': name - }, - 'attributes': {}, - 'traits': [trait.name for trait in traits], - 'willReportState': False, - 'type': device_type, + "id": state.entity_id, + "name": {"name": name}, + "attributes": {}, + "traits": [trait.name for trait in traits], + "willReportState": False, + "type": device_type, } # use aliases aliases = entity_config.get(CONF_ALIASES) if aliases: - device['name']['nicknames'] = aliases + device["name"]["nicknames"] = aliases for trt in traits: - device['attributes'].update(trt.sync_attributes()) + device["attributes"].update(trt.sync_attributes()) room = entity_config.get(CONF_ROOM_HINT) if room: - device['roomHint'] = room + device["roomHint"] = room return device dev_reg, ent_reg, area_reg = await gather( @@ -166,7 +172,7 @@ class GoogleEntity: area_entry = area_reg.areas.get(device_entry.area_id) if area_entry and area_entry.name: - device['roomHint'] = area_entry.name + device["roomHint"] = area_entry.name return device @@ -179,9 +185,9 @@ class GoogleEntity: state = self.state if state.state == STATE_UNAVAILABLE: - return {'online': False} + return {"online": False} - attrs = {'online': True} + attrs = {"online": True} for trt in self.traits(): deep_update(attrs, trt.query_attributes()) @@ -193,9 +199,9 @@ class GoogleEntity: https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute """ - command = command_payload['command'] - params = command_payload.get('params', {}) - challenge = command_payload.get('challenge', {}) + command = command_payload["command"] + params = command_payload.get("params", {}) + challenge = command_payload.get("challenge", {}) executed = False for trt in self.traits(): if trt.can_execute(command, params): @@ -206,8 +212,8 @@ class GoogleEntity: if not executed: raise SmartHomeError( ERR_FUNCTION_NOT_SUPPORTED, - 'Unable to execute {} for {}'.format(command, - self.state.entity_id)) + "Unable to execute {} for {}".format(command, self.state.entity_id), + ) @callback def async_update(self): diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 8f6d441d498..24502462512 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -49,24 +49,23 @@ class GoogleConfig(AbstractConfig): expose_by_default = self._config.get(CONF_EXPOSE_BY_DEFAULT) exposed_domains = self._config.get(CONF_EXPOSED_DOMAINS) - if state.attributes.get('view') is not None: + if state.attributes.get("view") is not None: # Ignore entities that are views return False if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False - explicit_expose = \ - self.entity_config.get(state.entity_id, {}).get(CONF_EXPOSE) + explicit_expose = self.entity_config.get(state.entity_id, {}).get(CONF_EXPOSE) - domain_exposed_by_default = \ + domain_exposed_by_default = ( expose_by_default and state.domain in exposed_domains + ) # Expose an entity if the entity's domain is exposed by default and # the configuration doesn't explicitly exclude it from being # exposed, or if the entity is explicitly exposed - is_default_exposed = \ - domain_exposed_by_default and explicit_expose is not False + is_default_exposed = domain_exposed_by_default and explicit_expose is not False return is_default_exposed or explicit_expose @@ -85,7 +84,7 @@ class GoogleAssistantView(HomeAssistantView): """Handle Google Assistant requests.""" url = GOOGLE_ASSISTANT_API_ENDPOINT - name = 'api:google_assistant' + name = "api:google_assistant" requires_auth = True def __init__(self, config): @@ -96,8 +95,6 @@ class GoogleAssistantView(HomeAssistantView): """Handle Google Assistant requests.""" message = await request.json() # type: dict result = await async_handle_message( - request.app['hass'], - self.config, - request['hass_user'].id, - message) + request.app["hass"], self.config, request["hass_user"].id, message + ) return self.json(result) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 07548ee95eb..2cb440f9181 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -8,8 +8,12 @@ from homeassistant.util.decorator import Registry from homeassistant.const import ATTR_ENTITY_ID from .const import ( - ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, ERR_UNKNOWN_ERROR, - EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED + ERR_PROTOCOL_ERROR, + ERR_DEVICE_OFFLINE, + ERR_UNKNOWN_ERROR, + EVENT_COMMAND_RECEIVED, + EVENT_SYNC_RECEIVED, + EVENT_QUERY_RECEIVED, ) from .helpers import RequestData, GoogleEntity, async_get_entities from .error import SmartHomeError @@ -20,112 +24,107 @@ _LOGGER = logging.getLogger(__name__) async def async_handle_message(hass, config, user_id, message): """Handle incoming API messages.""" - request_id = message.get('requestId') # type: str + request_id = message.get("requestId") # type: str data = RequestData(config, user_id, request_id) response = await _process(hass, data, message) - if response and 'errorCode' in response['payload']: - _LOGGER.error('Error handling message %s: %s', - message, response['payload']) + if response and "errorCode" in response["payload"]: + _LOGGER.error("Error handling message %s: %s", message, response["payload"]) return response async def _process(hass, data, message): """Process a message.""" - inputs = message.get('inputs') # type: list + inputs = message.get("inputs") # type: list if len(inputs) != 1: return { - 'requestId': data.request_id, - 'payload': {'errorCode': ERR_PROTOCOL_ERROR} + "requestId": data.request_id, + "payload": {"errorCode": ERR_PROTOCOL_ERROR}, } - handler = HANDLERS.get(inputs[0].get('intent')) + handler = HANDLERS.get(inputs[0].get("intent")) if handler is None: return { - 'requestId': data.request_id, - 'payload': {'errorCode': ERR_PROTOCOL_ERROR} + "requestId": data.request_id, + "payload": {"errorCode": ERR_PROTOCOL_ERROR}, } try: - result = await handler(hass, data, inputs[0].get('payload')) + result = await handler(hass, data, inputs[0].get("payload")) except SmartHomeError as err: - return { - 'requestId': data.request_id, - 'payload': {'errorCode': err.code} - } + return {"requestId": data.request_id, "payload": {"errorCode": err.code}} except Exception: # pylint: disable=broad-except - _LOGGER.exception('Unexpected error') + _LOGGER.exception("Unexpected error") return { - 'requestId': data.request_id, - 'payload': {'errorCode': ERR_UNKNOWN_ERROR} + "requestId": data.request_id, + "payload": {"errorCode": ERR_UNKNOWN_ERROR}, } if result is None: return None - return {'requestId': data.request_id, 'payload': result} + return {"requestId": data.request_id, "payload": result} -@HANDLERS.register('action.devices.SYNC') +@HANDLERS.register("action.devices.SYNC") async def async_devices_sync(hass, data, payload): """Handle action.devices.SYNC request. https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ hass.bus.async_fire( - EVENT_SYNC_RECEIVED, - {'request_id': data.request_id}, - context=data.context) + EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context + ) - devices = await asyncio.gather(*[ - entity.sync_serialize() for entity in - async_get_entities(hass, data.config) - if data.config.should_expose(entity.state) - ]) + devices = await asyncio.gather( + *( + entity.sync_serialize() + for entity in async_get_entities(hass, data.config) + if data.config.should_expose(entity.state) + ) + ) response = { - 'agentUserId': data.config.agent_user_id or data.context.user_id, - 'devices': devices, + "agentUserId": data.config.agent_user_id or data.context.user_id, + "devices": devices, } return response -@HANDLERS.register('action.devices.QUERY') +@HANDLERS.register("action.devices.QUERY") async def async_devices_query(hass, data, payload): """Handle action.devices.QUERY request. https://developers.google.com/actions/smarthome/create-app#actiondevicesquery """ devices = {} - for device in payload.get('devices', []): - devid = device['id'] + for device in payload.get("devices", []): + devid = device["id"] state = hass.states.get(devid) hass.bus.async_fire( EVENT_QUERY_RECEIVED, - { - 'request_id': data.request_id, - ATTR_ENTITY_ID: devid, - }, - context=data.context) + {"request_id": data.request_id, ATTR_ENTITY_ID: devid}, + context=data.context, + ) if not state: # If we can't find a state, the device is offline - devices[devid] = {'online': False} + devices[devid] = {"online": False} continue entity = GoogleEntity(hass, data.config, state) devices[devid] = entity.query_serialize() - return {'devices': devices} + return {"devices": devices} -@HANDLERS.register('action.devices.EXECUTE') +@HANDLERS.register("action.devices.EXECUTE") async def handle_devices_execute(hass, data, payload): """Handle action.devices.EXECUTE request. @@ -134,19 +133,19 @@ async def handle_devices_execute(hass, data, payload): entities = {} results = {} - for command in payload['commands']: - for device, execution in product(command['devices'], - command['execution']): - entity_id = device['id'] + for command in payload["commands"]: + for device, execution in product(command["devices"], command["execution"]): + entity_id = device["id"] hass.bus.async_fire( EVENT_COMMAND_RECEIVED, { - 'request_id': data.request_id, + "request_id": data.request_id, ATTR_ENTITY_ID: entity_id, - 'execution': execution + "execution": execution, }, - context=data.context) + context=data.context, + ) # Happens if error occurred. Skip entity for further processing if entity_id in results: @@ -157,9 +156,9 @@ async def handle_devices_execute(hass, data, payload): if state is None: results[entity_id] = { - 'ids': [entity_id], - 'status': 'ERROR', - 'errorCode': ERR_DEVICE_OFFLINE + "ids": [entity_id], + "status": "ERROR", + "errorCode": ERR_DEVICE_OFFLINE, } continue @@ -169,9 +168,9 @@ async def handle_devices_execute(hass, data, payload): await entities[entity_id].execute(data, execution) except SmartHomeError as err: results[entity_id] = { - 'ids': [entity_id], - 'status': 'ERROR', - **err.to_response() + "ids": [entity_id], + "status": "ERROR", + **err.to_response(), } final_results = list(results.values()) @@ -182,16 +181,18 @@ async def handle_devices_execute(hass, data, payload): entity.async_update() - final_results.append({ - 'ids': [entity.entity_id], - 'status': 'SUCCESS', - 'states': entity.query_serialize(), - }) + final_results.append( + { + "ids": [entity.entity_id], + "status": "SUCCESS", + "states": entity.query_serialize(), + } + ) - return {'commands': final_results} + return {"commands": final_results} -@HANDLERS.register('action.devices.DISCONNECT') +@HANDLERS.register("action.devices.DISCONNECT") async def async_devices_disconnect(hass, data, payload): """Handle action.devices.DISCONNECT request. @@ -203,6 +204,6 @@ async def async_devices_disconnect(hass, data, payload): def turned_off_response(message): """Return a device turned off response.""" return { - 'requestId': message.get('requestId'), - 'payload': {'errorCode': 'deviceTurnedOff'} + "requestId": message.get("requestId"), + "payload": {"errorCode": "deviceTurnedOff"}, } diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 2d7b7edd6ba..5fa7d49b885 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -48,41 +48,43 @@ from .error import SmartHomeError, ChallengeNeeded _LOGGER = logging.getLogger(__name__) -PREFIX_TRAITS = 'action.devices.traits.' -TRAIT_CAMERA_STREAM = PREFIX_TRAITS + 'CameraStream' -TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff' -TRAIT_DOCK = PREFIX_TRAITS + 'Dock' -TRAIT_STARTSTOP = PREFIX_TRAITS + 'StartStop' -TRAIT_BRIGHTNESS = PREFIX_TRAITS + 'Brightness' -TRAIT_COLOR_SETTING = PREFIX_TRAITS + 'ColorSetting' -TRAIT_SCENE = PREFIX_TRAITS + 'Scene' -TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting' -TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock' -TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed' -TRAIT_MODES = PREFIX_TRAITS + 'Modes' -TRAIT_OPENCLOSE = PREFIX_TRAITS + 'OpenClose' -TRAIT_VOLUME = PREFIX_TRAITS + 'Volume' +PREFIX_TRAITS = "action.devices.traits." +TRAIT_CAMERA_STREAM = PREFIX_TRAITS + "CameraStream" +TRAIT_ONOFF = PREFIX_TRAITS + "OnOff" +TRAIT_DOCK = PREFIX_TRAITS + "Dock" +TRAIT_STARTSTOP = PREFIX_TRAITS + "StartStop" +TRAIT_BRIGHTNESS = PREFIX_TRAITS + "Brightness" +TRAIT_COLOR_SETTING = PREFIX_TRAITS + "ColorSetting" +TRAIT_SCENE = PREFIX_TRAITS + "Scene" +TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + "TemperatureSetting" +TRAIT_LOCKUNLOCK = PREFIX_TRAITS + "LockUnlock" +TRAIT_FANSPEED = PREFIX_TRAITS + "FanSpeed" +TRAIT_MODES = PREFIX_TRAITS + "Modes" +TRAIT_OPENCLOSE = PREFIX_TRAITS + "OpenClose" +TRAIT_VOLUME = PREFIX_TRAITS + "Volume" -PREFIX_COMMANDS = 'action.devices.commands.' -COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff' -COMMAND_GET_CAMERA_STREAM = PREFIX_COMMANDS + 'GetCameraStream' -COMMAND_DOCK = PREFIX_COMMANDS + 'Dock' -COMMAND_STARTSTOP = PREFIX_COMMANDS + 'StartStop' -COMMAND_PAUSEUNPAUSE = PREFIX_COMMANDS + 'PauseUnpause' -COMMAND_BRIGHTNESS_ABSOLUTE = PREFIX_COMMANDS + 'BrightnessAbsolute' -COMMAND_COLOR_ABSOLUTE = PREFIX_COMMANDS + 'ColorAbsolute' -COMMAND_ACTIVATE_SCENE = PREFIX_COMMANDS + 'ActivateScene' +PREFIX_COMMANDS = "action.devices.commands." +COMMAND_ONOFF = PREFIX_COMMANDS + "OnOff" +COMMAND_GET_CAMERA_STREAM = PREFIX_COMMANDS + "GetCameraStream" +COMMAND_DOCK = PREFIX_COMMANDS + "Dock" +COMMAND_STARTSTOP = PREFIX_COMMANDS + "StartStop" +COMMAND_PAUSEUNPAUSE = PREFIX_COMMANDS + "PauseUnpause" +COMMAND_BRIGHTNESS_ABSOLUTE = PREFIX_COMMANDS + "BrightnessAbsolute" +COMMAND_COLOR_ABSOLUTE = PREFIX_COMMANDS + "ColorAbsolute" +COMMAND_ACTIVATE_SCENE = PREFIX_COMMANDS + "ActivateScene" COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT = ( - PREFIX_COMMANDS + 'ThermostatTemperatureSetpoint') + PREFIX_COMMANDS + "ThermostatTemperatureSetpoint" +) COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = ( - PREFIX_COMMANDS + 'ThermostatTemperatureSetRange') -COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode' -COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock' -COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed' -COMMAND_MODES = PREFIX_COMMANDS + 'SetModes' -COMMAND_OPENCLOSE = PREFIX_COMMANDS + 'OpenClose' -COMMAND_SET_VOLUME = PREFIX_COMMANDS + 'setVolume' -COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + 'volumeRelative' + PREFIX_COMMANDS + "ThermostatTemperatureSetRange" +) +COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + "ThermostatSetMode" +COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + "LockUnlock" +COMMAND_FANSPEED = PREFIX_COMMANDS + "SetFanSpeed" +COMMAND_MODES = PREFIX_COMMANDS + "SetModes" +COMMAND_OPENCLOSE = PREFIX_COMMANDS + "OpenClose" +COMMAND_SET_VOLUME = PREFIX_COMMANDS + "setVolume" +COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + "volumeRelative" TRAITS = [] @@ -96,8 +98,8 @@ def register_trait(trait): def _google_temp_unit(units): """Return Google temperature unit.""" if units == TEMP_FAHRENHEIT: - return 'F' - return 'C' + return "F" + return "C" class _Trait: @@ -141,9 +143,7 @@ class BrightnessTrait(_Trait): """ name = TRAIT_BRIGHTNESS - commands = [ - COMMAND_BRIGHTNESS_ABSOLUTE - ] + commands = [COMMAND_BRIGHTNESS_ABSOLUTE] @staticmethod def supported(domain, features, device_class): @@ -165,7 +165,7 @@ class BrightnessTrait(_Trait): if domain == light.DOMAIN: brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS) if brightness is not None: - response['brightness'] = int(100 * (brightness / 255)) + response["brightness"] = int(100 * (brightness / 255)) return response @@ -175,10 +175,15 @@ class BrightnessTrait(_Trait): if domain == light.DOMAIN: await self.hass.services.async_call( - light.DOMAIN, light.SERVICE_TURN_ON, { + light.DOMAIN, + light.SERVICE_TURN_ON, + { ATTR_ENTITY_ID: self.state.entity_id, - light.ATTR_BRIGHTNESS_PCT: params['brightness'] - }, blocking=True, context=data.context) + light.ATTR_BRIGHTNESS_PCT: params["brightness"], + }, + blocking=True, + context=data.context, + ) @register_trait @@ -189,9 +194,7 @@ class CameraStreamTrait(_Trait): """ name = TRAIT_CAMERA_STREAM - commands = [ - COMMAND_GET_CAMERA_STREAM - ] + commands = [COMMAND_GET_CAMERA_STREAM] stream_info = None @@ -206,11 +209,9 @@ class CameraStreamTrait(_Trait): def sync_attributes(self): """Return stream attributes for a sync request.""" return { - 'cameraStreamSupportedProtocols': [ - "hls", - ], - 'cameraStreamNeedAuthToken': False, - 'cameraStreamNeedDrmEncryption': False, + "cameraStreamSupportedProtocols": ["hls"], + "cameraStreamNeedAuthToken": False, + "cameraStreamNeedDrmEncryption": False, } def query_attributes(self): @@ -220,9 +221,10 @@ class CameraStreamTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute a get camera stream command.""" url = await self.hass.components.camera.async_request_stream( - self.state.entity_id, 'hls') + self.state.entity_id, "hls" + ) self.stream_info = { - 'cameraStreamAccessUrl': self.hass.config.api.base_url + url + "cameraStreamAccessUrl": self.hass.config.api.base_url + url } @@ -234,9 +236,7 @@ class OnOffTrait(_Trait): """ name = TRAIT_ONOFF - commands = [ - COMMAND_ONOFF - ] + commands = [COMMAND_ONOFF] @staticmethod def supported(domain, features, device_class): @@ -256,7 +256,7 @@ class OnOffTrait(_Trait): def query_attributes(self): """Return OnOff query attributes.""" - return {'on': self.state.state != STATE_OFF} + return {"on": self.state.state != STATE_OFF} async def execute(self, command, data, params, challenge): """Execute an OnOff command.""" @@ -264,15 +264,19 @@ class OnOffTrait(_Trait): if domain == group.DOMAIN: service_domain = HA_DOMAIN - service = SERVICE_TURN_ON if params['on'] else SERVICE_TURN_OFF + service = SERVICE_TURN_ON if params["on"] else SERVICE_TURN_OFF else: service_domain = domain - service = SERVICE_TURN_ON if params['on'] else SERVICE_TURN_OFF + service = SERVICE_TURN_ON if params["on"] else SERVICE_TURN_OFF - await self.hass.services.async_call(service_domain, service, { - ATTR_ENTITY_ID: self.state.entity_id - }, blocking=True, context=data.context) + await self.hass.services.async_call( + service_domain, + service, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) @register_trait @@ -283,9 +287,7 @@ class ColorSettingTrait(_Trait): """ name = TRAIT_COLOR_SETTING - commands = [ - COMMAND_COLOR_ABSOLUTE - ] + commands = [COMMAND_COLOR_ABSOLUTE] @staticmethod def supported(domain, features, device_class): @@ -293,8 +295,7 @@ class ColorSettingTrait(_Trait): if domain != light.DOMAIN: return False - return (features & light.SUPPORT_COLOR_TEMP or - features & light.SUPPORT_COLOR) + return features & light.SUPPORT_COLOR_TEMP or features & light.SUPPORT_COLOR def sync_attributes(self): """Return color temperature attributes for a sync request.""" @@ -303,18 +304,18 @@ class ColorSettingTrait(_Trait): response = {} if features & light.SUPPORT_COLOR: - response['colorModel'] = 'hsv' + response["colorModel"] = "hsv" if features & light.SUPPORT_COLOR_TEMP: # Max Kelvin is Min Mireds K = 1000000 / mireds # Min Kevin is Max Mireds K = 1000000 / mireds - response['colorTemperatureRange'] = { - 'temperatureMaxK': - color_util.color_temperature_mired_to_kelvin( - attrs.get(light.ATTR_MIN_MIREDS)), - 'temperatureMinK': - color_util.color_temperature_mired_to_kelvin( - attrs.get(light.ATTR_MAX_MIREDS)), + response["colorTemperatureRange"] = { + "temperatureMaxK": color_util.color_temperature_mired_to_kelvin( + attrs.get(light.ATTR_MIN_MIREDS) + ), + "temperatureMinK": color_util.color_temperature_mired_to_kelvin( + attrs.get(light.ATTR_MAX_MIREDS) + ), } return response @@ -328,72 +329,89 @@ class ColorSettingTrait(_Trait): color_hs = self.state.attributes.get(light.ATTR_HS_COLOR) brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS, 1) if color_hs is not None: - color['spectrumHsv'] = { - 'hue': color_hs[0], - 'saturation': color_hs[1]/100, - 'value': brightness/255, + color["spectrumHsv"] = { + "hue": color_hs[0], + "saturation": color_hs[1] / 100, + "value": brightness / 255, } if features & light.SUPPORT_COLOR_TEMP: temp = self.state.attributes.get(light.ATTR_COLOR_TEMP) # Some faulty integrations might put 0 in here, raising exception. if temp == 0: - _LOGGER.warning('Entity %s has incorrect color temperature %s', - self.state.entity_id, temp) + _LOGGER.warning( + "Entity %s has incorrect color temperature %s", + self.state.entity_id, + temp, + ) elif temp is not None: - color['temperatureK'] = \ - color_util.color_temperature_mired_to_kelvin(temp) + color["temperatureK"] = color_util.color_temperature_mired_to_kelvin( + temp + ) response = {} if color: - response['color'] = color + response["color"] = color return response async def execute(self, command, data, params, challenge): """Execute a color temperature command.""" - if 'temperature' in params['color']: + if "temperature" in params["color"]: temp = color_util.color_temperature_kelvin_to_mired( - params['color']['temperature']) + params["color"]["temperature"] + ) min_temp = self.state.attributes[light.ATTR_MIN_MIREDS] max_temp = self.state.attributes[light.ATTR_MAX_MIREDS] if temp < min_temp or temp > max_temp: raise SmartHomeError( ERR_VALUE_OUT_OF_RANGE, - "Temperature should be between {} and {}".format(min_temp, - max_temp)) + "Temperature should be between {} and {}".format( + min_temp, max_temp + ), + ) await self.hass.services.async_call( - light.DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: self.state.entity_id, - light.ATTR_COLOR_TEMP: temp, - }, blocking=True, context=data.context) + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self.state.entity_id, light.ATTR_COLOR_TEMP: temp}, + blocking=True, + context=data.context, + ) - elif 'spectrumRGB' in params['color']: + elif "spectrumRGB" in params["color"]: # Convert integer to hex format and left pad with 0's till length 6 - hex_value = "{0:06x}".format(params['color']['spectrumRGB']) + hex_value = "{0:06x}".format(params["color"]["spectrumRGB"]) color = color_util.color_RGB_to_hs( - *color_util.rgb_hex_to_rgb_list(hex_value)) + *color_util.rgb_hex_to_rgb_list(hex_value) + ) await self.hass.services.async_call( - light.DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: self.state.entity_id, - light.ATTR_HS_COLOR: color - }, blocking=True, context=data.context) + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self.state.entity_id, light.ATTR_HS_COLOR: color}, + blocking=True, + context=data.context, + ) - elif 'spectrumHSV' in params['color']: - color = params['color']['spectrumHSV'] - saturation = color['saturation'] * 100 - brightness = color['value'] * 255 + elif "spectrumHSV" in params["color"]: + color = params["color"]["spectrumHSV"] + saturation = color["saturation"] * 100 + brightness = color["value"] * 255 await self.hass.services.async_call( - light.DOMAIN, SERVICE_TURN_ON, { + light.DOMAIN, + SERVICE_TURN_ON, + { ATTR_ENTITY_ID: self.state.entity_id, - light.ATTR_HS_COLOR: [color['hue'], saturation], - light.ATTR_BRIGHTNESS: brightness - }, blocking=True, context=data.context) + light.ATTR_HS_COLOR: [color["hue"], saturation], + light.ATTR_BRIGHTNESS: brightness, + }, + blocking=True, + context=data.context, + ) @register_trait @@ -404,9 +422,7 @@ class SceneTrait(_Trait): """ name = TRAIT_SCENE - commands = [ - COMMAND_ACTIVATE_SCENE - ] + commands = [COMMAND_ACTIVATE_SCENE] @staticmethod def supported(domain, features, device_class): @@ -426,10 +442,12 @@ class SceneTrait(_Trait): """Execute a scene command.""" # Don't block for scripts as they can be slow. await self.hass.services.async_call( - self.state.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: self.state.entity_id - }, blocking=self.state.domain != script.DOMAIN, - context=data.context) + self.state.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=self.state.domain != script.DOMAIN, + context=data.context, + ) @register_trait @@ -440,9 +458,7 @@ class DockTrait(_Trait): """ name = TRAIT_DOCK - commands = [ - COMMAND_DOCK - ] + commands = [COMMAND_DOCK] @staticmethod def supported(domain, features, device_class): @@ -455,14 +471,17 @@ class DockTrait(_Trait): def query_attributes(self): """Return dock query attributes.""" - return {'isDocked': self.state.state == vacuum.STATE_DOCKED} + return {"isDocked": self.state.state == vacuum.STATE_DOCKED} async def execute(self, command, data, params, challenge): """Execute a dock command.""" await self.hass.services.async_call( - self.state.domain, vacuum.SERVICE_RETURN_TO_BASE, { - ATTR_ENTITY_ID: self.state.entity_id - }, blocking=True, context=data.context) + self.state.domain, + vacuum.SERVICE_RETURN_TO_BASE, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) @register_trait @@ -473,10 +492,7 @@ class StartStopTrait(_Trait): """ name = TRAIT_STARTSTOP - commands = [ - COMMAND_STARTSTOP, - COMMAND_PAUSEUNPAUSE - ] + commands = [COMMAND_STARTSTOP, COMMAND_PAUSEUNPAUSE] @staticmethod def supported(domain, features, device_class): @@ -485,41 +501,55 @@ class StartStopTrait(_Trait): def sync_attributes(self): """Return StartStop attributes for a sync request.""" - return {'pausable': - self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - & vacuum.SUPPORT_PAUSE != 0} + return { + "pausable": self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + & vacuum.SUPPORT_PAUSE + != 0 + } def query_attributes(self): """Return StartStop query attributes.""" return { - 'isRunning': self.state.state == vacuum.STATE_CLEANING, - 'isPaused': self.state.state == vacuum.STATE_PAUSED, + "isRunning": self.state.state == vacuum.STATE_CLEANING, + "isPaused": self.state.state == vacuum.STATE_PAUSED, } async def execute(self, command, data, params, challenge): """Execute a StartStop command.""" if command == COMMAND_STARTSTOP: - if params['start']: + if params["start"]: await self.hass.services.async_call( - self.state.domain, vacuum.SERVICE_START, { - ATTR_ENTITY_ID: self.state.entity_id - }, blocking=True, context=data.context) + self.state.domain, + vacuum.SERVICE_START, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) else: await self.hass.services.async_call( - self.state.domain, vacuum.SERVICE_STOP, { - ATTR_ENTITY_ID: self.state.entity_id - }, blocking=True, context=data.context) + self.state.domain, + vacuum.SERVICE_STOP, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) elif command == COMMAND_PAUSEUNPAUSE: - if params['pause']: + if params["pause"]: await self.hass.services.async_call( - self.state.domain, vacuum.SERVICE_PAUSE, { - ATTR_ENTITY_ID: self.state.entity_id - }, blocking=True, context=data.context) + self.state.domain, + vacuum.SERVICE_PAUSE, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) else: await self.hass.services.async_call( - self.state.domain, vacuum.SERVICE_START, { - ATTR_ENTITY_ID: self.state.entity_id - }, blocking=True, context=data.context) + self.state.domain, + vacuum.SERVICE_START, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) @register_trait @@ -538,19 +568,17 @@ class TemperatureSettingTrait(_Trait): # We do not support "on" as we are unable to know how to restore # the last mode. hvac_to_google = { - climate.HVAC_MODE_HEAT: 'heat', - climate.HVAC_MODE_COOL: 'cool', - climate.HVAC_MODE_OFF: 'off', - climate.HVAC_MODE_AUTO: 'auto', - climate.HVAC_MODE_HEAT_COOL: 'heatcool', - climate.HVAC_MODE_FAN_ONLY: 'fan-only', - climate.HVAC_MODE_DRY: 'dry', + climate.HVAC_MODE_HEAT: "heat", + climate.HVAC_MODE_COOL: "cool", + climate.HVAC_MODE_OFF: "off", + climate.HVAC_MODE_AUTO: "auto", + climate.HVAC_MODE_HEAT_COOL: "heatcool", + climate.HVAC_MODE_FAN_ONLY: "fan-only", + climate.HVAC_MODE_DRY: "dry", } google_to_hvac = {value: key for key, value in hvac_to_google.items()} - preset_to_google = { - climate.PRESET_ECO: 'eco' - } + preset_to_google = {climate.PRESET_ECO: "eco"} google_to_preset = {value: key for key, value in preset_to_google.items()} @staticmethod @@ -559,8 +587,9 @@ class TemperatureSettingTrait(_Trait): if domain == climate.DOMAIN: return True - return (domain == sensor.DOMAIN - and device_class == sensor.DEVICE_CLASS_TEMPERATURE) + return ( + domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_TEMPERATURE + ) @property def climate_google_modes(self): @@ -585,8 +614,9 @@ class TemperatureSettingTrait(_Trait): response = {} attrs = self.state.attributes domain = self.state.domain - response['thermostatTemperatureUnit'] = _google_temp_unit( - self.hass.config.units.temperature_unit) + response["thermostatTemperatureUnit"] = _google_temp_unit( + self.hass.config.units.temperature_unit + ) if domain == sensor.DOMAIN: device_class = attrs.get(ATTR_DEVICE_CLASS) @@ -595,10 +625,11 @@ class TemperatureSettingTrait(_Trait): elif domain == climate.DOMAIN: modes = self.climate_google_modes - if 'off' in modes and any(mode in modes for mode - in ('heatcool', 'heat', 'cool')): - modes.append('on') - response['availableThermostatModes'] = ','.join(modes) + if "off" in modes and any( + mode in modes for mode in ("heatcool", "heat", "cool") + ): + modes.append("on") + response["availableThermostatModes"] = ",".join(modes) return response @@ -613,12 +644,9 @@ class TemperatureSettingTrait(_Trait): if device_class == sensor.DEVICE_CLASS_TEMPERATURE: current_temp = self.state.state if current_temp is not None: - response['thermostatTemperatureAmbient'] = \ - round(temp_util.convert( - float(current_temp), - unit, - TEMP_CELSIUS - ), 1) + response["thermostatTemperatureAmbient"] = round( + temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1 + ) elif domain == climate.DOMAIN: operation = self.state.state @@ -626,52 +654,48 @@ class TemperatureSettingTrait(_Trait): supported = attrs.get(ATTR_SUPPORTED_FEATURES, 0) if preset in self.preset_to_google: - response['thermostatMode'] = self.preset_to_google[preset] + response["thermostatMode"] = self.preset_to_google[preset] else: - response['thermostatMode'] = self.hvac_to_google.get(operation) + response["thermostatMode"] = self.hvac_to_google.get(operation) current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) if current_temp is not None: - response['thermostatTemperatureAmbient'] = \ - round(temp_util.convert( - current_temp, - unit, - TEMP_CELSIUS - ), 1) + response["thermostatTemperatureAmbient"] = round( + temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1 + ) current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY) if current_humidity is not None: - response['thermostatHumidityAmbient'] = current_humidity + response["thermostatHumidityAmbient"] = current_humidity - if operation in (climate.HVAC_MODE_AUTO, - climate.HVAC_MODE_HEAT_COOL): + if operation in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: - response['thermostatTemperatureSetpointHigh'] = \ - round(temp_util.convert( - attrs[climate.ATTR_TARGET_TEMP_HIGH], - unit, TEMP_CELSIUS), 1) - response['thermostatTemperatureSetpointLow'] = \ - round(temp_util.convert( - attrs[climate.ATTR_TARGET_TEMP_LOW], - unit, TEMP_CELSIUS), 1) + response["thermostatTemperatureSetpointHigh"] = round( + temp_util.convert( + attrs[climate.ATTR_TARGET_TEMP_HIGH], unit, TEMP_CELSIUS + ), + 1, + ) + response["thermostatTemperatureSetpointLow"] = round( + temp_util.convert( + attrs[climate.ATTR_TARGET_TEMP_LOW], unit, TEMP_CELSIUS + ), + 1, + ) else: target_temp = attrs.get(ATTR_TEMPERATURE) if target_temp is not None: target_temp = round( - temp_util.convert( - target_temp, - unit, - TEMP_CELSIUS - ), 1) - response['thermostatTemperatureSetpointHigh'] = \ - target_temp - response['thermostatTemperatureSetpointLow'] = \ - target_temp + temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 + ) + response["thermostatTemperatureSetpointHigh"] = target_temp + response["thermostatTemperatureSetpointLow"] = target_temp else: target_temp = attrs.get(ATTR_TEMPERATURE) if target_temp is not None: - response['thermostatTemperatureSetpoint'] = round( - temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1) + response["thermostatTemperatureSetpoint"] = round( + temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 + ) return response @@ -680,8 +704,8 @@ class TemperatureSettingTrait(_Trait): domain = self.state.domain if domain == sensor.DOMAIN: raise SmartHomeError( - ERR_NOT_SUPPORTED, - 'Execute is not supported by sensor') + ERR_NOT_SUPPORTED, "Execute is not supported by sensor" + ) # All sent in temperatures are always in Celsius unit = self.hass.config.units.temperature_unit @@ -690,27 +714,31 @@ class TemperatureSettingTrait(_Trait): if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT: temp = temp_util.convert( - params['thermostatTemperatureSetpoint'], TEMP_CELSIUS, - unit) + params["thermostatTemperatureSetpoint"], TEMP_CELSIUS, unit + ) if unit == TEMP_FAHRENHEIT: temp = round(temp) if temp < min_temp or temp > max_temp: raise SmartHomeError( ERR_VALUE_OUT_OF_RANGE, - "Temperature should be between {} and {}".format(min_temp, - max_temp)) + "Temperature should be between {} and {}".format( + min_temp, max_temp + ), + ) await self.hass.services.async_call( - climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, { - ATTR_ENTITY_ID: self.state.entity_id, - ATTR_TEMPERATURE: temp - }, blocking=True, context=data.context) + climate.DOMAIN, + climate.SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: self.state.entity_id, ATTR_TEMPERATURE: temp}, + blocking=True, + context=data.context, + ) elif command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE: temp_high = temp_util.convert( - params['thermostatTemperatureSetpointHigh'], TEMP_CELSIUS, - unit) + params["thermostatTemperatureSetpointHigh"], TEMP_CELSIUS, unit + ) if unit == TEMP_FAHRENHEIT: temp_high = round(temp_high) @@ -718,11 +746,12 @@ class TemperatureSettingTrait(_Trait): raise SmartHomeError( ERR_VALUE_OUT_OF_RANGE, "Upper bound for temperature range should be between " - "{} and {}".format(min_temp, max_temp)) + "{} and {}".format(min_temp, max_temp), + ) temp_low = temp_util.convert( - params['thermostatTemperatureSetpointLow'], TEMP_CELSIUS, - unit) + params["thermostatTemperatureSetpointLow"], TEMP_CELSIUS, unit + ) if unit == TEMP_FAHRENHEIT: temp_low = round(temp_low) @@ -730,12 +759,11 @@ class TemperatureSettingTrait(_Trait): raise SmartHomeError( ERR_VALUE_OUT_OF_RANGE, "Lower bound for temperature range should be between " - "{} and {}".format(min_temp, max_temp)) + "{} and {}".format(min_temp, max_temp), + ) supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES) - svc_data = { - ATTR_ENTITY_ID: self.state.entity_id, - } + svc_data = {ATTR_ENTITY_ID: self.state.entity_id} if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: svc_data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high @@ -744,51 +772,60 @@ class TemperatureSettingTrait(_Trait): svc_data[ATTR_TEMPERATURE] = (temp_high + temp_low) / 2 await self.hass.services.async_call( - climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, svc_data, - blocking=True, context=data.context) + climate.DOMAIN, + climate.SERVICE_SET_TEMPERATURE, + svc_data, + blocking=True, + context=data.context, + ) elif command == COMMAND_THERMOSTAT_SET_MODE: - target_mode = params['thermostatMode'] + target_mode = params["thermostatMode"] supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES) - if target_mode == 'on': + if target_mode == "on": await self.hass.services.async_call( - climate.DOMAIN, SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: self.state.entity_id - }, - blocking=True, context=data.context + climate.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, ) return - if target_mode == 'off': + if target_mode == "off": await self.hass.services.async_call( - climate.DOMAIN, SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: self.state.entity_id - }, - blocking=True, context=data.context + climate.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, ) return if target_mode in self.google_to_preset: await self.hass.services.async_call( - climate.DOMAIN, climate.SERVICE_SET_PRESET_MODE, + climate.DOMAIN, + climate.SERVICE_SET_PRESET_MODE, { - climate.ATTR_PRESET_MODE: - self.google_to_preset[target_mode], - ATTR_ENTITY_ID: self.state.entity_id + climate.ATTR_PRESET_MODE: self.google_to_preset[target_mode], + ATTR_ENTITY_ID: self.state.entity_id, }, - blocking=True, context=data.context + blocking=True, + context=data.context, ) return await self.hass.services.async_call( - climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE, { + climate.DOMAIN, + climate.SERVICE_SET_HVAC_MODE, + { ATTR_ENTITY_ID: self.state.entity_id, - climate.ATTR_HVAC_MODE: - self.google_to_hvac[target_mode], - }, blocking=True, context=data.context) + climate.ATTR_HVAC_MODE: self.google_to_hvac[target_mode], + }, + blocking=True, + context=data.context, + ) @register_trait @@ -799,9 +836,7 @@ class LockUnlockTrait(_Trait): """ name = TRAIT_LOCKUNLOCK - commands = [ - COMMAND_LOCKUNLOCK - ] + commands = [COMMAND_LOCKUNLOCK] @staticmethod def supported(domain, features, device_class): @@ -819,19 +854,23 @@ class LockUnlockTrait(_Trait): def query_attributes(self): """Return LockUnlock query attributes.""" - return {'isLocked': self.state.state == STATE_LOCKED} + return {"isLocked": self.state.state == STATE_LOCKED} async def execute(self, command, data, params, challenge): """Execute an LockUnlock command.""" - if params['lock']: + if params["lock"]: service = lock.SERVICE_LOCK else: _verify_pin_challenge(data, self.state, challenge) service = lock.SERVICE_UNLOCK - await self.hass.services.async_call(lock.DOMAIN, service, { - ATTR_ENTITY_ID: self.state.entity_id - }, blocking=True, context=data.context) + await self.hass.services.async_call( + lock.DOMAIN, + service, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) @register_trait @@ -842,17 +881,13 @@ class FanSpeedTrait(_Trait): """ name = TRAIT_FANSPEED - commands = [ - COMMAND_FANSPEED - ] + commands = [COMMAND_FANSPEED] speed_synonyms = { - fan.SPEED_OFF: ['stop', 'off'], - fan.SPEED_LOW: ['slow', 'low', 'slowest', 'lowest'], - fan.SPEED_MEDIUM: ['medium', 'mid', 'middle'], - fan.SPEED_HIGH: [ - 'high', 'max', 'fast', 'highest', 'fastest', 'maximum' - ] + fan.SPEED_OFF: ["stop", "off"], + fan.SPEED_LOW: ["slow", "low", "slowest", "lowest"], + fan.SPEED_MEDIUM: ["medium", "mid", "middle"], + fan.SPEED_HIGH: ["high", "max", "fast", "highest", "fastest", "maximum"], } @staticmethod @@ -872,20 +907,18 @@ class FanSpeedTrait(_Trait): continue speed = { "speed_name": mode, - "speed_values": [{ - "speed_synonym": self.speed_synonyms.get(mode), - "lang": 'en' - }] + "speed_values": [ + {"speed_synonym": self.speed_synonyms.get(mode), "lang": "en"} + ], } speeds.append(speed) return { - 'availableFanSpeeds': { - 'speeds': speeds, - 'ordered': True - }, - "reversible": bool(self.state.attributes.get( - ATTR_SUPPORTED_FEATURES, 0) & fan.SUPPORT_DIRECTION) + "availableFanSpeeds": {"speeds": speeds, "ordered": True}, + "reversible": bool( + self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + & fan.SUPPORT_DIRECTION + ), } def query_attributes(self): @@ -895,19 +928,21 @@ class FanSpeedTrait(_Trait): speed = attrs.get(fan.ATTR_SPEED) if speed is not None: - response['on'] = speed != fan.SPEED_OFF - response['online'] = True - response['currentFanSpeedSetting'] = speed + response["on"] = speed != fan.SPEED_OFF + response["online"] = True + response["currentFanSpeedSetting"] = speed return response async def execute(self, command, data, params, challenge): """Execute an SetFanSpeed command.""" await self.hass.services.async_call( - fan.DOMAIN, fan.SERVICE_SET_SPEED, { - ATTR_ENTITY_ID: self.state.entity_id, - fan.ATTR_SPEED: params['fanSpeed'] - }, blocking=True, context=data.context) + fan.DOMAIN, + fan.SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: self.state.entity_id, fan.ATTR_SPEED: params["fanSpeed"]}, + blocking=True, + context=data.context, + ) @register_trait @@ -918,92 +953,100 @@ class ModesTrait(_Trait): """ name = TRAIT_MODES - commands = [ - COMMAND_MODES - ] + commands = [COMMAND_MODES] # Google requires specific mode names and settings. Here is the full list. # https://developers.google.com/actions/reference/smarthome/traits/modes # All settings are mapped here as of 2018-11-28 and can be used for other # entity types. - HA_TO_GOOGLE = { - media_player.ATTR_INPUT_SOURCE: "input source", - } + HA_TO_GOOGLE = {media_player.ATTR_INPUT_SOURCE: "input source"} SUPPORTED_MODE_SETTINGS = { - 'xsmall': [ - 'xsmall', 'extra small', 'min', 'minimum', 'tiny', 'xs'], - 'small': ['small', 'half'], - 'large': ['large', 'big', 'full'], - 'xlarge': ['extra large', 'xlarge', 'xl'], - 'Cool': ['cool', 'rapid cool', 'rapid cooling'], - 'Heat': ['heat'], 'Low': ['low'], - 'Medium': ['medium', 'med', 'mid', 'half'], - 'High': ['high'], - 'Auto': ['auto', 'automatic'], - 'Bake': ['bake'], 'Roast': ['roast'], - 'Convection Bake': ['convection bake', 'convect bake'], - 'Convection Roast': ['convection roast', 'convect roast'], - 'Favorite': ['favorite'], - 'Broil': ['broil'], - 'Warm': ['warm'], - 'Off': ['off'], - 'On': ['on'], - 'Normal': [ - 'normal', 'normal mode', 'normal setting', 'standard', - 'schedule', 'original', 'default', 'old settings' + "xsmall": ["xsmall", "extra small", "min", "minimum", "tiny", "xs"], + "small": ["small", "half"], + "large": ["large", "big", "full"], + "xlarge": ["extra large", "xlarge", "xl"], + "Cool": ["cool", "rapid cool", "rapid cooling"], + "Heat": ["heat"], + "Low": ["low"], + "Medium": ["medium", "med", "mid", "half"], + "High": ["high"], + "Auto": ["auto", "automatic"], + "Bake": ["bake"], + "Roast": ["roast"], + "Convection Bake": ["convection bake", "convect bake"], + "Convection Roast": ["convection roast", "convect roast"], + "Favorite": ["favorite"], + "Broil": ["broil"], + "Warm": ["warm"], + "Off": ["off"], + "On": ["on"], + "Normal": [ + "normal", + "normal mode", + "normal setting", + "standard", + "schedule", + "original", + "default", + "old settings", ], - 'None': ['none'], - 'Tap Cold': ['tap cold'], - 'Cold Warm': ['cold warm'], - 'Hot': ['hot'], - 'Extra Hot': ['extra hot'], - 'Eco': ['eco'], - 'Wool': ['wool', 'fleece'], - 'Turbo': ['turbo'], - 'Rinse': ['rinse', 'rinsing', 'rinse wash'], - 'Away': ['away', 'holiday'], - 'maximum': ['maximum'], - 'media player': ['media player'], - 'chromecast': ['chromecast'], - 'tv': [ - 'tv', 'television', 'tv position', 'television position', - 'watching tv', 'watching tv position', 'entertainment', - 'entertainment position' + "None": ["none"], + "Tap Cold": ["tap cold"], + "Cold Warm": ["cold warm"], + "Hot": ["hot"], + "Extra Hot": ["extra hot"], + "Eco": ["eco"], + "Wool": ["wool", "fleece"], + "Turbo": ["turbo"], + "Rinse": ["rinse", "rinsing", "rinse wash"], + "Away": ["away", "holiday"], + "maximum": ["maximum"], + "media player": ["media player"], + "chromecast": ["chromecast"], + "tv": [ + "tv", + "television", + "tv position", + "television position", + "watching tv", + "watching tv position", + "entertainment", + "entertainment position", ], - 'am fm': ['am fm', 'am radio', 'fm radio'], - 'internet radio': ['internet radio'], - 'satellite': ['satellite'], - 'game console': ['game console'], - 'antifrost': ['antifrost', 'anti-frost'], - 'boost': ['boost'], - 'Clock': ['clock'], - 'Message': ['message'], - 'Messages': ['messages'], - 'News': ['news'], - 'Disco': ['disco'], - 'antifreeze': ['antifreeze', 'anti-freeze', 'anti freeze'], - 'balanced': ['balanced', 'normal'], - 'swing': ['swing'], - 'media': ['media', 'media mode'], - 'panic': ['panic'], - 'ring': ['ring'], - 'frozen': ['frozen', 'rapid frozen', 'rapid freeze'], - 'cotton': ['cotton', 'cottons'], - 'blend': ['blend', 'mix'], - 'baby wash': ['baby wash'], - 'synthetics': ['synthetic', 'synthetics', 'compose'], - 'hygiene': ['hygiene', 'sterilization'], - 'smart': ['smart', 'intelligent', 'intelligence'], - 'comfortable': ['comfortable', 'comfort'], - 'manual': ['manual'], - 'energy saving': ['energy saving'], - 'sleep': ['sleep'], - 'quick wash': ['quick wash', 'fast wash'], - 'cold': ['cold'], - 'airsupply': ['airsupply', 'air supply'], - 'dehumidification': ['dehumidication', 'dehumidify'], - 'game': ['game', 'game mode'] + "am fm": ["am fm", "am radio", "fm radio"], + "internet radio": ["internet radio"], + "satellite": ["satellite"], + "game console": ["game console"], + "antifrost": ["antifrost", "anti-frost"], + "boost": ["boost"], + "Clock": ["clock"], + "Message": ["message"], + "Messages": ["messages"], + "News": ["news"], + "Disco": ["disco"], + "antifreeze": ["antifreeze", "anti-freeze", "anti freeze"], + "balanced": ["balanced", "normal"], + "swing": ["swing"], + "media": ["media", "media mode"], + "panic": ["panic"], + "ring": ["ring"], + "frozen": ["frozen", "rapid frozen", "rapid freeze"], + "cotton": ["cotton", "cottons"], + "blend": ["blend", "mix"], + "baby wash": ["baby wash"], + "synthetics": ["synthetic", "synthetics", "compose"], + "hygiene": ["hygiene", "sterilization"], + "smart": ["smart", "intelligent", "intelligence"], + "comfortable": ["comfortable", "comfort"], + "manual": ["manual"], + "energy saving": ["energy saving"], + "sleep": ["sleep"], + "quick wash": ["quick wash", "fast wash"], + "cold": ["cold"], + "airsupply": ["airsupply", "air supply"], + "dehumidification": ["dehumidication", "dehumidify"], + "game": ["game", "game mode"], } @staticmethod @@ -1017,19 +1060,17 @@ class ModesTrait(_Trait): def sync_attributes(self): """Return mode attributes for a sync request.""" sources_list = self.state.attributes.get( - media_player.ATTR_INPUT_SOURCE_LIST, []) + media_player.ATTR_INPUT_SOURCE_LIST, [] + ) modes = [] sources = {} if sources_list: sources = { "name": self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE), - "name_values": [{ - "name_synonym": ['input source'], - "lang": "en" - }], + "name_values": [{"name_synonym": ["input source"], "lang": "en"}], "settings": [], - "ordered": False + "ordered": False, } for source in sources_list: if source in self.SUPPORTED_MODE_SETTINGS: @@ -1042,18 +1083,15 @@ class ModesTrait(_Trait): else: continue - sources['settings'].append( + sources["settings"].append( { "setting_name": src, - "setting_values": [{ - "setting_synonym": synonyms, - "lang": "en" - }] + "setting_values": [{"setting_synonym": synonyms, "lang": "en"}], } ) if sources: modes.append(sources) - payload = {'availableModes': modes} + payload = {"availableModes": modes} return payload @@ -1064,35 +1102,42 @@ class ModesTrait(_Trait): mode_settings = {} if attrs.get(media_player.ATTR_INPUT_SOURCE_LIST): - mode_settings.update({ - media_player.ATTR_INPUT_SOURCE: attrs.get( - media_player.ATTR_INPUT_SOURCE) - }) + mode_settings.update( + { + media_player.ATTR_INPUT_SOURCE: attrs.get( + media_player.ATTR_INPUT_SOURCE + ) + } + ) if mode_settings: - response['on'] = self.state.state != STATE_OFF - response['online'] = True - response['currentModeSettings'] = mode_settings + response["on"] = self.state.state != STATE_OFF + response["online"] = True + response["currentModeSettings"] = mode_settings return response async def execute(self, command, data, params, challenge): """Execute an SetModes command.""" - settings = params.get('updateModeSettings') + settings = params.get("updateModeSettings") requested_source = settings.get( - self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE)) + self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE) + ) if requested_source: - for src in self.state.attributes.get( - media_player.ATTR_INPUT_SOURCE_LIST): + for src in self.state.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST): if src.lower() == requested_source.lower(): source = src await self.hass.services.async_call( media_player.DOMAIN, - media_player.SERVICE_SELECT_SOURCE, { + media_player.SERVICE_SELECT_SOURCE, + { ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_INPUT_SOURCE: source - }, blocking=True, context=data.context) + media_player.ATTR_INPUT_SOURCE: source, + }, + blocking=True, + context=data.context, + ) @register_trait @@ -1106,9 +1151,7 @@ class OpenCloseTrait(_Trait): COVER_2FA = (cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE) name = TRAIT_OPENCLOSE - commands = [ - COMMAND_OPENCLOSE - ] + commands = [COMMAND_OPENCLOSE] override_position = None @@ -1129,14 +1172,13 @@ class OpenCloseTrait(_Trait): @staticmethod def might_2fa(domain, features, device_class): """Return if the trait might ask for 2FA.""" - return (domain == cover.DOMAIN and - device_class in OpenCloseTrait.COVER_2FA) + return domain == cover.DOMAIN and device_class in OpenCloseTrait.COVER_2FA def sync_attributes(self): """Return opening direction.""" response = {} if self.state.domain == binary_sensor.DOMAIN: - response['queryOnlyOpenClose'] = True + response["queryOnlyOpenClose"] = True return response def query_attributes(self): @@ -1145,37 +1187,37 @@ class OpenCloseTrait(_Trait): response = {} if self.override_position is not None: - response['openPercent'] = self.override_position + response["openPercent"] = self.override_position elif domain == cover.DOMAIN: # When it's an assumed state, we will return that querying state # is not supported. if self.state.attributes.get(ATTR_ASSUMED_STATE): raise SmartHomeError( - ERR_NOT_SUPPORTED, - 'Querying state is not supported') + ERR_NOT_SUPPORTED, "Querying state is not supported" + ) if self.state.state == STATE_UNKNOWN: raise SmartHomeError( - ERR_NOT_SUPPORTED, - 'Querying state is not supported') + ERR_NOT_SUPPORTED, "Querying state is not supported" + ) position = self.override_position or self.state.attributes.get( cover.ATTR_CURRENT_POSITION ) if position is not None: - response['openPercent'] = position + response["openPercent"] = position elif self.state.state != cover.STATE_CLOSED: - response['openPercent'] = 100 + response["openPercent"] = 100 else: - response['openPercent'] = 0 + response["openPercent"] = 0 elif domain == binary_sensor.DOMAIN: if self.state.state == STATE_ON: - response['openPercent'] = 100 + response["openPercent"] = 100 else: - response['openPercent'] = 0 + response["openPercent"] = 0 return response @@ -1186,34 +1228,40 @@ class OpenCloseTrait(_Trait): if domain == cover.DOMAIN: svc_params = {ATTR_ENTITY_ID: self.state.entity_id} - if params['openPercent'] == 0: + if params["openPercent"] == 0: service = cover.SERVICE_CLOSE_COVER should_verify = False - elif params['openPercent'] == 100: + elif params["openPercent"] == 100: service = cover.SERVICE_OPEN_COVER should_verify = True - elif (self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & - cover.SUPPORT_SET_POSITION): + elif ( + self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + & cover.SUPPORT_SET_POSITION + ): service = cover.SERVICE_SET_COVER_POSITION should_verify = True - svc_params[cover.ATTR_POSITION] = params['openPercent'] + svc_params[cover.ATTR_POSITION] = params["openPercent"] else: raise SmartHomeError( - ERR_FUNCTION_NOT_SUPPORTED, - 'Setting a position is not supported') + ERR_FUNCTION_NOT_SUPPORTED, "Setting a position is not supported" + ) - if (should_verify and - self.state.attributes.get(ATTR_DEVICE_CLASS) - in OpenCloseTrait.COVER_2FA): + if ( + should_verify + and self.state.attributes.get(ATTR_DEVICE_CLASS) + in OpenCloseTrait.COVER_2FA + ): _verify_pin_challenge(data, self.state, challenge) await self.hass.services.async_call( - cover.DOMAIN, service, svc_params, - blocking=True, context=data.context) + cover.DOMAIN, service, svc_params, blocking=True, context=data.context + ) - if (self.state.attributes.get(ATTR_ASSUMED_STATE) or - self.state.state == STATE_UNKNOWN): - self.override_position = params['openPercent'] + if ( + self.state.attributes.get(ATTR_ASSUMED_STATE) + or self.state.state == STATE_UNKNOWN + ): + self.override_position = params["openPercent"] @register_trait @@ -1224,10 +1272,7 @@ class VolumeTrait(_Trait): """ name = TRAIT_VOLUME - commands = [ - COMMAND_SET_VOLUME, - COMMAND_VOLUME_RELATIVE, - ] + commands = [COMMAND_SET_VOLUME, COMMAND_VOLUME_RELATIVE] @staticmethod def supported(domain, features, device_class): @@ -1245,40 +1290,44 @@ class VolumeTrait(_Trait): """Return brightness query attributes.""" response = {} - level = self.state.attributes.get( - media_player.ATTR_MEDIA_VOLUME_LEVEL) - muted = self.state.attributes.get( - media_player.ATTR_MEDIA_VOLUME_MUTED) + level = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) + muted = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED) if level is not None: # Convert 0.0-1.0 to 0-100 - response['currentVolume'] = int(level * 100) - response['isMuted'] = bool(muted) + response["currentVolume"] = int(level * 100) + response["isMuted"] = bool(muted) return response async def _execute_set_volume(self, data, params): - level = params['volumeLevel'] + level = params["volumeLevel"] await self.hass.services.async_call( media_player.DOMAIN, - media_player.SERVICE_VOLUME_SET, { + media_player.SERVICE_VOLUME_SET, + { ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_MEDIA_VOLUME_LEVEL: - level / 100 - }, blocking=True, context=data.context) + media_player.ATTR_MEDIA_VOLUME_LEVEL: level / 100, + }, + blocking=True, + context=data.context, + ) async def _execute_volume_relative(self, data, params): # This could also support up/down commands using relativeSteps - relative = params['volumeRelativeLevel'] - current = self.state.attributes.get( - media_player.ATTR_MEDIA_VOLUME_LEVEL) + relative = params["volumeRelativeLevel"] + current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) await self.hass.services.async_call( - media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, { + media_player.DOMAIN, + media_player.SERVICE_VOLUME_SET, + { ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_MEDIA_VOLUME_LEVEL: - current + relative / 100 - }, blocking=True, context=data.context) + media_player.ATTR_MEDIA_VOLUME_LEVEL: current + relative / 100, + }, + blocking=True, + context=data.context, + ) async def execute(self, command, data, params, challenge): """Execute a brightness command.""" @@ -1287,8 +1336,7 @@ class VolumeTrait(_Trait): elif command == COMMAND_VOLUME_RELATIVE: await self._execute_volume_relative(data, params) else: - raise SmartHomeError( - ERR_NOT_SUPPORTED, 'Command not supported') + raise SmartHomeError(ERR_NOT_SUPPORTED, "Command not supported") def _verify_pin_challenge(data, state, challenge): @@ -1297,13 +1345,12 @@ def _verify_pin_challenge(data, state, challenge): return if not data.config.secure_devices_pin: - raise SmartHomeError( - ERR_CHALLENGE_NOT_SETUP, 'Challenge is not set up') + raise SmartHomeError(ERR_CHALLENGE_NOT_SETUP, "Challenge is not set up") if not challenge: raise ChallengeNeeded(CHALLENGE_PIN_NEEDED) - pin = challenge.get('pin') + pin = challenge.get("pin") if pin != data.config.secure_devices_pin: raise ChallengeNeeded(CHALLENGE_FAILED_PIN_NEEDED) @@ -1311,5 +1358,5 @@ def _verify_pin_challenge(data, state, challenge): def _verify_ack_challenge(data, state, challenge): """Verify a pin challenge.""" - if not challenge or not challenge.get('ack'): + if not challenge or not challenge.get("ack"): raise ChallengeNeeded(CHALLENGE_ACK_NEEDED) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 696d4da3223..cf064b3bfa7 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -12,29 +12,55 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_KEY_FILE = 'key_file' -CONF_GENDER = 'gender' -CONF_VOICE = 'voice' -CONF_ENCODING = 'encoding' -CONF_SPEED = 'speed' -CONF_PITCH = 'pitch' -CONF_GAIN = 'gain' -CONF_PROFILES = 'profiles' +CONF_KEY_FILE = "key_file" +CONF_GENDER = "gender" +CONF_VOICE = "voice" +CONF_ENCODING = "encoding" +CONF_SPEED = "speed" +CONF_PITCH = "pitch" +CONF_GAIN = "gain" +CONF_PROFILES = "profiles" SUPPORTED_LANGUAGES = [ - 'cs-CZ', 'da-DK', 'de-DE', 'el-GR', 'en-AU', 'en-GB', 'en-IN', 'en-US', - 'es-ES', 'fi-FI', 'fil-PH', 'fr-CA', 'fr-FR', 'hi-IN', 'hu-HU', 'id-ID', - 'it-IT', 'ja-JP', 'ko-KR', 'nb-NO', 'nl-NL', 'pl-PL', 'pt-BR', 'pt-PT', - 'ru-RU', 'sk-SK', 'sv-SE', 'tr-TR', 'uk-UA', 'vi-VN', + "cs-CZ", + "da-DK", + "de-DE", + "el-GR", + "en-AU", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fi-FI", + "fil-PH", + "fr-CA", + "fr-FR", + "hi-IN", + "hu-HU", + "id-ID", + "it-IT", + "ja-JP", + "ko-KR", + "nb-NO", + "nl-NL", + "pl-PL", + "pt-BR", + "pt-PT", + "ru-RU", + "sk-SK", + "sv-SE", + "tr-TR", + "uk-UA", + "vi-VN", ] -DEFAULT_LANG = 'en-US' +DEFAULT_LANG = "en-US" -DEFAULT_GENDER = 'NEUTRAL' +DEFAULT_GENDER = "NEUTRAL" -VOICE_REGEX = r'[a-z]{2,3}-[A-Z]{2}-(Standard|Wavenet)-[A-Z]|' -DEFAULT_VOICE = '' +VOICE_REGEX = r"[a-z]{2,3}-[A-Z]{2}-(Standard|Wavenet)-[A-Z]|" +DEFAULT_VOICE = "" -DEFAULT_ENCODING = 'MP3' +DEFAULT_ENCODING = "MP3" MIN_SPEED = 0.25 MAX_SPEED = 4.0 @@ -70,42 +96,30 @@ SUPPORTED_OPTIONS = [ ] GENDER_SCHEMA = vol.All( - vol.Upper, - vol.In(texttospeech.enums.SsmlVoiceGender.__members__) + vol.Upper, vol.In(texttospeech.enums.SsmlVoiceGender.__members__) ) VOICE_SCHEMA = cv.matches_regex(VOICE_REGEX) SCHEMA_ENCODING = vol.All( - vol.Upper, - vol.In(texttospeech.enums.AudioEncoding.__members__) -) -SPEED_SCHEMA = vol.All( - vol.Coerce(float), - vol.Clamp(min=MIN_SPEED, max=MAX_SPEED) -) -PITCH_SCHEMA = vol.All( - vol.Coerce(float), - vol.Clamp(min=MIN_PITCH, max=MAX_PITCH) -) -GAIN_SCHEMA = vol.All( - vol.Coerce(float), - vol.Clamp(min=MIN_GAIN, max=MAX_GAIN) -) -PROFILES_SCHEMA = vol.All( - cv.ensure_list, - [vol.In(SUPPORTED_PROFILES)] + vol.Upper, vol.In(texttospeech.enums.AudioEncoding.__members__) ) +SPEED_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_SPEED, max=MAX_SPEED)) +PITCH_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_PITCH, max=MAX_PITCH)) +GAIN_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_GAIN, max=MAX_GAIN)) +PROFILES_SCHEMA = vol.All(cv.ensure_list, [vol.In(SUPPORTED_PROFILES)]) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_KEY_FILE): cv.string, - vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), - vol.Optional(CONF_GENDER, default=DEFAULT_GENDER): GENDER_SCHEMA, - vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): VOICE_SCHEMA, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): SCHEMA_ENCODING, - vol.Optional(CONF_SPEED, default=DEFAULT_SPEED): SPEED_SCHEMA, - vol.Optional(CONF_PITCH, default=DEFAULT_PITCH): PITCH_SCHEMA, - vol.Optional(CONF_GAIN, default=DEFAULT_GAIN): GAIN_SCHEMA, - vol.Optional(CONF_PROFILES, default=[]): PROFILES_SCHEMA, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_KEY_FILE): cv.string, + vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), + vol.Optional(CONF_GENDER, default=DEFAULT_GENDER): GENDER_SCHEMA, + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): VOICE_SCHEMA, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): SCHEMA_ENCODING, + vol.Optional(CONF_SPEED, default=DEFAULT_SPEED): SPEED_SCHEMA, + vol.Optional(CONF_PITCH, default=DEFAULT_PITCH): PITCH_SCHEMA, + vol.Optional(CONF_GAIN, default=DEFAULT_GAIN): GAIN_SCHEMA, + vol.Optional(CONF_PROFILES, default=[]): PROFILES_SCHEMA, + } +) async def async_get_engine(hass, config): @@ -127,7 +141,7 @@ async def async_get_engine(hass, config): config.get(CONF_SPEED), config.get(CONF_PITCH), config.get(CONF_GAIN), - config.get(CONF_PROFILES) + config.get(CONF_PROFILES), ) @@ -135,21 +149,21 @@ class GoogleCloudTTSProvider(Provider): """The Google Cloud TTS API provider.""" def __init__( - self, - hass, - key_file=None, - language=DEFAULT_LANG, - gender=DEFAULT_GENDER, - voice=DEFAULT_VOICE, - encoding=DEFAULT_ENCODING, - speed=1.0, - pitch=0, - gain=0, - profiles=None + self, + hass, + key_file=None, + language=DEFAULT_LANG, + gender=DEFAULT_GENDER, + voice=DEFAULT_VOICE, + encoding=DEFAULT_ENCODING, + speed=1.0, + pitch=0, + gain=0, + profiles=None, ): """Init Google Cloud TTS service.""" self.hass = hass - self.name = 'Google Cloud TTS' + self.name = "Google Cloud TTS" self._language = language self._gender = gender self._voice = voice @@ -160,8 +174,9 @@ class GoogleCloudTTSProvider(Provider): self._profiles = profiles if key_file: - self._client = texttospeech \ - .TextToSpeechClient.from_service_account_json(key_file) + self._client = texttospeech.TextToSpeechClient.from_service_account_json( + key_file + ) else: self._client = texttospeech.TextToSpeechClient() @@ -190,21 +205,22 @@ class GoogleCloudTTSProvider(Provider): CONF_SPEED: self._speed, CONF_PITCH: self._pitch, CONF_GAIN: self._gain, - CONF_PROFILES: self._profiles + CONF_PROFILES: self._profiles, } async def async_get_tts_audio(self, message, language, options=None): """Load TTS from google.""" - options_schema = vol.Schema({ - vol.Optional(CONF_GENDER, default=self._gender): GENDER_SCHEMA, - vol.Optional(CONF_VOICE, default=self._voice): VOICE_SCHEMA, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): - SCHEMA_ENCODING, - vol.Optional(CONF_SPEED, default=self._speed): SPEED_SCHEMA, - vol.Optional(CONF_PITCH, default=self._speed): SPEED_SCHEMA, - vol.Optional(CONF_GAIN, default=DEFAULT_GAIN): GAIN_SCHEMA, - vol.Optional(CONF_PROFILES, default=[]): PROFILES_SCHEMA, - }) + options_schema = vol.Schema( + { + vol.Optional(CONF_GENDER, default=self._gender): GENDER_SCHEMA, + vol.Optional(CONF_VOICE, default=self._voice): VOICE_SCHEMA, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): SCHEMA_ENCODING, + vol.Optional(CONF_SPEED, default=self._speed): SPEED_SCHEMA, + vol.Optional(CONF_PITCH, default=self._speed): SPEED_SCHEMA, + vol.Optional(CONF_GAIN, default=DEFAULT_GAIN): GAIN_SCHEMA, + vol.Optional(CONF_PROFILES, default=[]): PROFILES_SCHEMA, + } + ) options = options_schema(options) _encoding = options[CONF_ENCODING] @@ -214,16 +230,12 @@ class GoogleCloudTTSProvider(Provider): try: # pylint: disable=no-member - synthesis_input = texttospeech.types.SynthesisInput( - text=message - ) + synthesis_input = texttospeech.types.SynthesisInput(text=message) voice = texttospeech.types.VoiceSelectionParams( language_code=language, - ssml_gender=texttospeech.enums.SsmlVoiceGender[ - options[CONF_GENDER] - ], - name=_voice + ssml_gender=texttospeech.enums.SsmlVoiceGender[options[CONF_GENDER]], + name=_voice, ) audio_config = texttospeech.types.AudioConfig( @@ -237,18 +249,13 @@ class GoogleCloudTTSProvider(Provider): with async_timeout.timeout(10, loop=self.hass.loop): response = await self.hass.async_add_executor_job( - self._client.synthesize_speech, - synthesis_input, - voice, - audio_config + self._client.synthesize_speech, synthesis_input, voice, audio_config ) return _encoding, response.audio_content except asyncio.TimeoutError as ex: _LOGGER.error("Timeout for Google Cloud TTS call: %s", ex) except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception( - "Error occured during Google Cloud TTS call: %s", ex - ) + _LOGGER.exception("Error occurred during Google Cloud TTS call: %s", ex) return None, None diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index 2d3736d2ec3..8f975db6fd8 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -8,27 +8,31 @@ import async_timeout import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME) +from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME _LOGGER = logging.getLogger(__name__) -DOMAIN = 'google_domains' +DOMAIN = "google_domains" INTERVAL = timedelta(minutes=5) DEFAULT_TIMEOUT = 10 -UPDATE_URL = 'https://{}:{}@domains.google.com/nic/update' +UPDATE_URL = "https://{}:{}@domains.google.com/nic/update" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DOMAIN): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_DOMAIN): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -41,47 +45,41 @@ async def async_setup(hass, config): session = hass.helpers.aiohttp_client.async_get_clientsession() result = await _update_google_domains( - hass, session, domain, user, password, timeout) + hass, session, domain, user, password, timeout + ) if not result: return False async def update_domain_interval(now): """Update the Google Domains entry.""" - await _update_google_domains( - hass, session, domain, user, password, timeout) + await _update_google_domains(hass, session, domain, user, password, timeout) - hass.helpers.event.async_track_time_interval( - update_domain_interval, INTERVAL) + hass.helpers.event.async_track_time_interval(update_domain_interval, INTERVAL) return True -async def _update_google_domains( - hass, session, domain, user, password, timeout): +async def _update_google_domains(hass, session, domain, user, password, timeout): """Update Google Domains.""" url = UPDATE_URL.format(user, password) - params = { - 'hostname': domain - } + params = {"hostname": domain} try: with async_timeout.timeout(timeout): resp = await session.get(url, params=params) body = await resp.text() - if body.startswith('good') or body.startswith('nochg'): + if body.startswith("good") or body.startswith("nochg"): return True - _LOGGER.warning('Updating Google Domains failed: %s => %s', - domain, body) + _LOGGER.warning("Updating Google Domains failed: %s => %s", domain, body) except aiohttp.ClientError: _LOGGER.warning("Can't connect to Google Domains API") except asyncio.TimeoutError: - _LOGGER.warning("Timeout from Google Domains API for domain: %s", - domain) + _LOGGER.warning("Timeout from Google Domains API for domain: %s", domain) return False diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 5788392190a..0887aa19bfb 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -2,36 +2,40 @@ from datetime import timedelta import logging +from locationsharinglib import Service +from locationsharinglib.locationsharinglibexceptions import InvalidCookies import voluptuous as vol -from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA, SOURCE_TYPE_GPS) +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_GPS from homeassistant.const import ( - ATTR_ID, CONF_PASSWORD, CONF_USERNAME, ATTR_BATTERY_CHARGING, - ATTR_BATTERY_LEVEL) + ATTR_BATTERY_CHARGING, + ATTR_BATTERY_LEVEL, + ATTR_ID, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util import slugify, dt as dt_util +from homeassistant.util import dt as dt_util, slugify _LOGGER = logging.getLogger(__name__) -ATTR_ADDRESS = 'address' -ATTR_FULL_NAME = 'full_name' -ATTR_LAST_SEEN = 'last_seen' -ATTR_NICKNAME = 'nickname' +ATTR_ADDRESS = "address" +ATTR_FULL_NAME = "full_name" +ATTR_LAST_SEEN = "last_seen" +ATTR_NICKNAME = "nickname" -CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' +CONF_MAX_GPS_ACCURACY = "max_gps_accuracy" -CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' +CREDENTIALS_FILE = ".google_maps_location_sharing.cookies" -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_MAX_GPS_ACCURACY, default=100000): vol.Coerce(float), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_MAX_GPS_ACCURACY, default=100000): vol.Coerce(float), + } +) def setup_scanner(hass, config: ConfigType, see, discovery_info=None): @@ -45,43 +49,50 @@ class GoogleMapsScanner: def __init__(self, hass, config: ConfigType, see) -> None: """Initialize the scanner.""" - from locationsharinglib import Service - from locationsharinglib.locationsharinglibexceptions import InvalidUser - self.see = see self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] + self.scan_interval = timedelta(seconds=config.get(CONF_SCAN_INTERVAL, 60)) + credfile = "{}.{}".format( + hass.config.path(CREDENTIALS_FILE), slugify(self.username) + ) try: - credfile = "{}.{}".format(hass.config.path(CREDENTIALS_FILE), - slugify(self.username)) - self.service = Service(self.username, self.password, credfile) + self.service = Service(credfile, self.username) self._update_info() - track_time_interval( - hass, self._update_info, MIN_TIME_BETWEEN_SCANS) + track_time_interval(hass, self._update_info, self.scan_interval) self.success_init = True - except InvalidUser: - _LOGGER.error("You have specified invalid login credentials") + except InvalidCookies: + _LOGGER.error( + "You have specified invalid login credentials. " + "Please make sure you have saved your credentials" + " in the following file: %s", + credfile, + ) self.success_init = False def _update_info(self, now=None): for person in self.service.get_all_people(): try: - dev_id = 'google_maps_{0}'.format(slugify(person.id)) + dev_id = "google_maps_{0}".format(slugify(person.id)) except TypeError: _LOGGER.warning("No location(s) shared with this account") return - if self.max_gps_accuracy is not None and \ - person.accuracy > self.max_gps_accuracy: - _LOGGER.info("Ignoring %s update because expected GPS " - "accuracy %s is not met: %s", - person.nickname, self.max_gps_accuracy, - person.accuracy) + if ( + self.max_gps_accuracy is not None + and person.accuracy > self.max_gps_accuracy + ): + _LOGGER.info( + "Ignoring %s update because expected GPS " + "accuracy %s is not met: %s", + person.nickname, + self.max_gps_accuracy, + person.accuracy, + ) continue attrs = { @@ -91,7 +102,7 @@ class GoogleMapsScanner: ATTR_LAST_SEEN: dt_util.as_utc(person.datetime), ATTR_NICKNAME: person.nickname, ATTR_BATTERY_CHARGING: person.charging, - ATTR_BATTERY_LEVEL: person.battery_level + ATTR_BATTERY_LEVEL: person.battery_level, } self.see( dev_id=dev_id, diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index 7d6aeeef041..5f31f533a38 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -3,7 +3,7 @@ "name": "Google maps", "documentation": "https://www.home-assistant.io/components/google_maps", "requirements": [ - "locationsharinglib==3.0.11" + "locationsharinglib==4.0.2" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 8aaa7a17ac4..e108d249797 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -7,29 +7,33 @@ from typing import Any, Dict import voluptuous as vol -from homeassistant.const import ( - EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN) +from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA _LOGGER = logging.getLogger(__name__) -DOMAIN = 'google_pubsub' +DOMAIN = "google_pubsub" -CONF_PROJECT_ID = 'project_id' -CONF_TOPIC_NAME = 'topic_name' -CONF_SERVICE_PRINCIPAL = 'credentials_json' -CONF_FILTER = 'filter' +CONF_PROJECT_ID = "project_id" +CONF_TOPIC_NAME = "topic_name" +CONF_SERVICE_PRINCIPAL = "credentials_json" +CONF_FILTER = "filter" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_PROJECT_ID): cv.string, - vol.Required(CONF_TOPIC_NAME): cv.string, - vol.Required(CONF_SERVICE_PRINCIPAL): cv.string, - vol.Required(CONF_FILTER): FILTER_SCHEMA, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_PROJECT_ID): cv.string, + vol.Required(CONF_TOPIC_NAME): cv.string, + vol.Required(CONF_SERVICE_PRINCIPAL): cv.string, + vol.Required(CONF_FILTER): FILTER_SCHEMA, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): @@ -40,7 +44,8 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): project_id = config[CONF_PROJECT_ID] topic_name = config[CONF_TOPIC_NAME] service_principal_path = os.path.join( - hass.config.config_dir, config[CONF_SERVICE_PRINCIPAL]) + hass.config.config_dir, config[CONF_SERVICE_PRINCIPAL] + ) if not os.path.isfile(service_principal_path): _LOGGER.error("Path to credentials file cannot be found") @@ -48,29 +53,26 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): entities_filter = config[CONF_FILTER] - publisher = (pubsub_v1 - .PublisherClient - .from_service_account_json(service_principal_path) - ) + publisher = pubsub_v1.PublisherClient.from_service_account_json( + service_principal_path + ) - topic_path = publisher.topic_path(project_id, # pylint: disable=E1101 - topic_name) + topic_path = publisher.topic_path(project_id, topic_name) # pylint: disable=E1101 encoder = DateTimeJSONEncoder() def send_to_pubsub(event: Event): """Send states to Pub/Sub.""" - state = event.data.get('new_state') - if (state is None - or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE) - or not entities_filter(state.entity_id)): + state = event.data.get("new_state") + if ( + state is None + or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) + or not entities_filter(state.entity_id) + ): return as_dict = state.as_dict() - data = json.dumps( - obj=as_dict, - default=encoder.encode - ).encode('utf-8') + data = json.dumps(obj=as_dict, default=encoder.encode).encode("utf-8") publisher.publish(topic_path, data=data) diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index 0f067cf13b9..cdf6dbd402e 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -18,18 +18,67 @@ GOOGLE_SPEECH_URL = "https://translate.google.com/translate_tts" MESSAGE_SIZE = 148 SUPPORT_LANGUAGES = [ - 'af', 'sq', 'ar', 'hy', 'bn', 'ca', 'zh', 'zh-cn', 'zh-tw', 'zh-yue', - 'hr', 'cs', 'da', 'nl', 'en', 'en-au', 'en-uk', 'en-us', 'eo', 'fi', - 'fr', 'de', 'el', 'hi', 'hu', 'is', 'id', 'it', 'ja', 'ko', 'la', 'lv', - 'mk', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru', 'sr', 'sk', 'es', 'es-es', - 'es-mx', 'es-us', 'sw', 'sv', 'ta', 'th', 'tr', 'vi', 'cy', 'uk', 'bg-BG' + "af", + "sq", + "ar", + "hy", + "bn", + "ca", + "zh", + "zh-cn", + "zh-tw", + "zh-yue", + "hr", + "cs", + "da", + "nl", + "en", + "en-au", + "en-uk", + "en-us", + "eo", + "fi", + "fr", + "de", + "el", + "hi", + "hu", + "is", + "id", + "it", + "ja", + "ko", + "la", + "lv", + "mk", + "no", + "pl", + "pt", + "pt-br", + "ro", + "ru", + "sr", + "sk", + "es", + "es-es", + "es-mx", + "es-us", + "sw", + "sv", + "ta", + "th", + "tr", + "vi", + "cy", + "uk", + "bg-BG", ] -DEFAULT_LANG = 'en' +DEFAULT_LANG = "en" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)} +) async def async_get_engine(hass, config): @@ -46,11 +95,13 @@ class GoogleProvider(Provider): self._lang = lang self.headers = { REFERER: "http://translate.google.com/", - USER_AGENT: ("Mozilla/5.0 (Windows NT 10.0; WOW64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/47.0.2526.106 Safari/537.36"), + USER_AGENT: ( + "Mozilla/5.0 (Windows NT 10.0; WOW64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/47.0.2526.106 Safari/537.36" + ), } - self.name = 'Google' + self.name = "Google" @property def default_language(self): @@ -70,32 +121,31 @@ class GoogleProvider(Provider): websession = async_get_clientsession(self.hass) message_parts = self._split_message_to_parts(message) - data = b'' + data = b"" for idx, part in enumerate(message_parts): - part_token = await self.hass.async_add_job( - token.calculate_token, part) + part_token = await self.hass.async_add_job(token.calculate_token, part) url_param = { - 'ie': 'UTF-8', - 'tl': language, - 'q': yarl.URL(part).raw_path, - 'tk': part_token, - 'total': len(message_parts), - 'idx': idx, - 'client': 'tw-ob', - 'textlen': len(part), + "ie": "UTF-8", + "tl": language, + "q": yarl.URL(part).raw_path, + "tk": part_token, + "total": len(message_parts), + "idx": idx, + "client": "tw-ob", + "textlen": len(part), } try: with async_timeout.timeout(10): request = await websession.get( - GOOGLE_SPEECH_URL, params=url_param, - headers=self.headers + GOOGLE_SPEECH_URL, params=url_param, headers=self.headers ) if request.status != 200: - _LOGGER.error("Error %d on load URL %s", - request.status, request.url) + _LOGGER.error( + "Error %d on load URL %s", request.status, request.url + ) return None, None data += await request.read() @@ -103,7 +153,7 @@ class GoogleProvider(Provider): _LOGGER.error("Timeout for google speech") return None, None - return 'mp3', data + return "mp3", data @staticmethod def _split_message_to_parts(message): @@ -113,13 +163,13 @@ class GoogleProvider(Provider): punc = "!()[]?.,;:" punc_list = [re.escape(c) for c in punc] - pattern = '|'.join(punc_list) + pattern = "|".join(punc_list) parts = re.split(pattern, message) def split_by_space(fullstring): """Split a string by space.""" if len(fullstring) > MESSAGE_SIZE: - idx = fullstring.rfind(' ', 0, MESSAGE_SIZE) + idx = fullstring.rfind(" ", 0, MESSAGE_SIZE) return [fullstring[:idx]] + split_by_space(fullstring[idx:]) return [fullstring] diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 1a6d347ad7f..32947867958 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -9,8 +9,14 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, CONF_NAME, EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, - ATTR_LONGITUDE, ATTR_ATTRIBUTION, CONF_MODE) + CONF_API_KEY, + CONF_NAME, + EVENT_HOMEASSISTANT_START, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_ATTRIBUTION, + CONF_MODE, +) from homeassistant.helpers import location from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -19,57 +25,110 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Google" -CONF_DESTINATION = 'destination' -CONF_OPTIONS = 'options' -CONF_ORIGIN = 'origin' -CONF_TRAVEL_MODE = 'travel_mode' +CONF_DESTINATION = "destination" +CONF_OPTIONS = "options" +CONF_ORIGIN = "origin" +CONF_TRAVEL_MODE = "travel_mode" -DEFAULT_NAME = 'Google Travel Time' +DEFAULT_NAME = "Google Travel Time" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) -ALL_LANGUAGES = ['ar', 'bg', 'bn', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', - 'eu', 'fa', 'fi', 'fr', 'gl', 'gu', 'hi', 'hr', 'hu', 'id', - 'it', 'iw', 'ja', 'kn', 'ko', 'lt', 'lv', 'ml', 'mr', 'nl', - 'no', 'pl', 'pt', 'pt-BR', 'pt-PT', 'ro', 'ru', 'sk', 'sl', - 'sr', 'sv', 'ta', 'te', 'th', 'tl', 'tr', 'uk', 'vi', - 'zh-CN', 'zh-TW'] +ALL_LANGUAGES = [ + "ar", + "bg", + "bn", + "ca", + "cs", + "da", + "de", + "el", + "en", + "es", + "eu", + "fa", + "fi", + "fr", + "gl", + "gu", + "hi", + "hr", + "hu", + "id", + "it", + "iw", + "ja", + "kn", + "ko", + "lt", + "lv", + "ml", + "mr", + "nl", + "no", + "pl", + "pt", + "pt-BR", + "pt-PT", + "ro", + "ru", + "sk", + "sl", + "sr", + "sv", + "ta", + "te", + "th", + "tl", + "tr", + "uk", + "vi", + "zh-CN", + "zh-TW", +] -AVOID = ['tolls', 'highways', 'ferries', 'indoor'] -TRANSIT_PREFS = ['less_walking', 'fewer_transfers'] -TRANSPORT_TYPE = ['bus', 'subway', 'train', 'tram', 'rail'] -TRAVEL_MODE = ['driving', 'walking', 'bicycling', 'transit'] -TRAVEL_MODEL = ['best_guess', 'pessimistic', 'optimistic'] -UNITS = ['metric', 'imperial'] +AVOID = ["tolls", "highways", "ferries", "indoor"] +TRANSIT_PREFS = ["less_walking", "fewer_transfers"] +TRANSPORT_TYPE = ["bus", "subway", "train", "tram", "rail"] +TRAVEL_MODE = ["driving", "walking", "bicycling", "transit"] +TRAVEL_MODEL = ["best_guess", "pessimistic", "optimistic"] +UNITS = ["metric", "imperial"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_DESTINATION): cv.string, - vol.Required(CONF_ORIGIN): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_TRAVEL_MODE): vol.In(TRAVEL_MODE), - vol.Optional(CONF_OPTIONS, default={CONF_MODE: 'driving'}): vol.All( - dict, vol.Schema({ - vol.Optional(CONF_MODE, default='driving'): vol.In(TRAVEL_MODE), - vol.Optional('language'): vol.In(ALL_LANGUAGES), - vol.Optional('avoid'): vol.In(AVOID), - vol.Optional('units'): vol.In(UNITS), - vol.Exclusive('arrival_time', 'time'): cv.string, - vol.Exclusive('departure_time', 'time'): cv.string, - vol.Optional('traffic_model'): vol.In(TRAVEL_MODEL), - vol.Optional('transit_mode'): vol.In(TRANSPORT_TYPE), - vol.Optional('transit_routing_preference'): vol.In(TRANSIT_PREFS) - })) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_DESTINATION): cv.string, + vol.Required(CONF_ORIGIN): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_TRAVEL_MODE): vol.In(TRAVEL_MODE), + vol.Optional(CONF_OPTIONS, default={CONF_MODE: "driving"}): vol.All( + dict, + vol.Schema( + { + vol.Optional(CONF_MODE, default="driving"): vol.In(TRAVEL_MODE), + vol.Optional("language"): vol.In(ALL_LANGUAGES), + vol.Optional("avoid"): vol.In(AVOID), + vol.Optional("units"): vol.In(UNITS), + vol.Exclusive("arrival_time", "time"): cv.string, + vol.Exclusive("departure_time", "time"): cv.string, + vol.Optional("traffic_model"): vol.In(TRAVEL_MODEL), + vol.Optional("transit_mode"): vol.In(TRANSPORT_TYPE), + vol.Optional("transit_routing_preference"): vol.In(TRANSIT_PREFS), + } + ), + ), + } +) -TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone', 'person'] -DATA_KEY = 'google_travel_time' +TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] +DATA_KEY = "google_travel_time" def convert_time_to_utc(timestr): """Take a string like 08:00:00 and convert it to a unix timestamp.""" combined = datetime.combine( - dt_util.start_of_local_day(), dt_util.parse_time(timestr)) + dt_util.start_of_local_day(), dt_util.parse_time(timestr) + ) if combined < datetime.now(): combined = combined + timedelta(days=1) return dt_util.as_timestamp(combined) @@ -77,6 +136,7 @@ def convert_time_to_utc(timestr): def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the Google travel time platform.""" + def run_setup(event): """ Delay the setup until Home Assistant is fully initialized. @@ -86,15 +146,17 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): hass.data.setdefault(DATA_KEY, []) options = config.get(CONF_OPTIONS) - if options.get('units') is None: - options['units'] = hass.config.units.name + if options.get("units") is None: + options["units"] = hass.config.units.name travel_mode = config.get(CONF_TRAVEL_MODE) mode = options.get(CONF_MODE) if travel_mode is not None: - wstr = ("Google Travel Time: travel_mode is deprecated, please " - "add mode to the options dictionary instead!") + wstr = ( + "Google Travel Time: travel_mode is deprecated, please " + "add mode to the options dictionary instead!" + ) _LOGGER.warning(wstr) if mode is None: options[CONF_MODE] = travel_mode @@ -107,7 +169,8 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): destination = config.get(CONF_DESTINATION) sensor = GoogleTravelTimeSensor( - hass, name, api_key, origin, destination, options) + hass, name, api_key, origin, destination, options + ) hass.data[DATA_KEY].append(sensor) if sensor.valid_api_connection: @@ -125,27 +188,28 @@ class GoogleTravelTimeSensor(Entity): self._hass = hass self._name = name self._options = options - self._unit_of_measurement = 'min' + self._unit_of_measurement = "min" self._matrix = None self.valid_api_connection = True # Check if location is a trackable entity - if origin.split('.', 1)[0] in TRACKABLE_DOMAINS: + if origin.split(".", 1)[0] in TRACKABLE_DOMAINS: self._origin_entity_id = origin else: self._origin = origin - if destination.split('.', 1)[0] in TRACKABLE_DOMAINS: + if destination.split(".", 1)[0] in TRACKABLE_DOMAINS: self._destination_entity_id = destination else: self._destination = destination import googlemaps + self._client = googlemaps.Client(api_key, timeout=10) try: self.update() except googlemaps.exceptions.ApiError as exp: - _LOGGER .error(exp) + _LOGGER.error(exp) self.valid_api_connection = False return @@ -155,11 +219,11 @@ class GoogleTravelTimeSensor(Entity): if self._matrix is None: return None - _data = self._matrix['rows'][0]['elements'][0] - if 'duration_in_traffic' in _data: - return round(_data['duration_in_traffic']['value']/60) - if 'duration' in _data: - return round(_data['duration']['value']/60) + _data = self._matrix["rows"][0]["elements"][0] + if "duration_in_traffic" in _data: + return round(_data["duration_in_traffic"]["value"] / 60) + if "duration" in _data: + return round(_data["duration"]["value"] / 60) return None @property @@ -175,16 +239,16 @@ class GoogleTravelTimeSensor(Entity): res = self._matrix.copy() res.update(self._options) - del res['rows'] - _data = self._matrix['rows'][0]['elements'][0] - if 'duration_in_traffic' in _data: - res['duration_in_traffic'] = _data['duration_in_traffic']['text'] - if 'duration' in _data: - res['duration'] = _data['duration']['text'] - if 'distance' in _data: - res['distance'] = _data['distance']['text'] - res['origin'] = self._origin - res['destination'] = self._destination + del res["rows"] + _data = self._matrix["rows"][0]["elements"][0] + if "duration_in_traffic" in _data: + res["duration_in_traffic"] = _data["duration_in_traffic"]["text"] + if "duration" in _data: + res["duration"] = _data["duration"]["text"] + if "distance" in _data: + res["distance"] = _data["distance"]["text"] + res["origin"] = self._origin + res["destination"] = self._destination res[ATTR_ATTRIBUTION] = ATTRIBUTION return res @@ -197,27 +261,25 @@ class GoogleTravelTimeSensor(Entity): def update(self): """Get the latest data from Google.""" options_copy = self._options.copy() - dtime = options_copy.get('departure_time') - atime = options_copy.get('arrival_time') - if dtime is not None and ':' in dtime: - options_copy['departure_time'] = convert_time_to_utc(dtime) + dtime = options_copy.get("departure_time") + atime = options_copy.get("arrival_time") + if dtime is not None and ":" in dtime: + options_copy["departure_time"] = convert_time_to_utc(dtime) elif dtime is not None: - options_copy['departure_time'] = dtime + options_copy["departure_time"] = dtime elif atime is None: - options_copy['departure_time'] = 'now' + options_copy["departure_time"] = "now" - if atime is not None and ':' in atime: - options_copy['arrival_time'] = convert_time_to_utc(atime) + if atime is not None and ":" in atime: + options_copy["arrival_time"] = convert_time_to_utc(atime) elif atime is not None: - options_copy['arrival_time'] = atime + options_copy["arrival_time"] = atime # Convert device_trackers to google friendly location - if hasattr(self, '_origin_entity_id'): - self._origin = self._get_location_from_entity( - self._origin_entity_id - ) + if hasattr(self, "_origin_entity_id"): + self._origin = self._get_location_from_entity(self._origin_entity_id) - if hasattr(self, '_destination_entity_id'): + if hasattr(self, "_destination_entity_id"): self._destination = self._get_location_from_entity( self._destination_entity_id ) @@ -227,7 +289,8 @@ class GoogleTravelTimeSensor(Entity): if self._destination is not None and self._origin is not None: self._matrix = self._client.distance_matrix( - self._origin, self._destination, **options_copy) + self._origin, self._destination, **options_copy + ) def _get_location_from_entity(self, entity_id): """Get the location from the entity state or attributes.""" @@ -246,8 +309,7 @@ class GoogleTravelTimeSensor(Entity): zone_entity = self._hass.states.get("zone.%s" % entity.state) if location.has_location(zone_entity): _LOGGER.debug( - "%s is in %s, getting zone location", - entity_id, zone_entity.entity_id + "%s is in %s, getting zone location", entity_id, zone_entity.entity_id ) return self._get_location_from_attributes(zone_entity) @@ -267,7 +329,7 @@ class GoogleTravelTimeSensor(Entity): def _resolve_zone(self, friendly_name): entities = self._hass.states.all() for entity in entities: - if entity.domain == 'zone' and entity.name == friendly_name: + if entity.domain == "zone" and entity.name == friendly_name: return self._get_location_from_attributes(entity) return friendly_name diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index 202e2a0eb46..a910e91b164 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -9,66 +9,52 @@ from homeassistant.util import dt import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_MONITORED_CONDITIONS, STATE_UNKNOWN) + CONF_NAME, + CONF_HOST, + CONF_MONITORED_CONDITIONS, + STATE_UNKNOWN, +) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTR_CURRENT_VERSION = 'current_version' -ATTR_LAST_RESTART = 'last_restart' -ATTR_LOCAL_IP = 'local_ip' -ATTR_NEW_VERSION = 'new_version' -ATTR_STATUS = 'status' -ATTR_UPTIME = 'uptime' +ATTR_CURRENT_VERSION = "current_version" +ATTR_LAST_RESTART = "last_restart" +ATTR_LOCAL_IP = "local_ip" +ATTR_NEW_VERSION = "new_version" +ATTR_STATUS = "status" +ATTR_UPTIME = "uptime" -DEFAULT_HOST = 'testwifi.here' -DEFAULT_NAME = 'google_wifi' +DEFAULT_HOST = "testwifi.here" +DEFAULT_NAME = "google_wifi" -ENDPOINT = '/api/v1/status' +ENDPOINT = "/api/v1/status" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) MONITORED_CONDITIONS = { ATTR_CURRENT_VERSION: [ - ['software', 'softwareVersion'], + ["software", "softwareVersion"], None, - 'mdi:checkbox-marked-circle-outline' + "mdi:checkbox-marked-circle-outline", ], - ATTR_NEW_VERSION: [ - ['software', 'updateNewVersion'], - None, - 'mdi:update' - ], - ATTR_UPTIME: [ - ['system', 'uptime'], - 'days', - 'mdi:timelapse' - ], - ATTR_LAST_RESTART: [ - ['system', 'uptime'], - None, - 'mdi:restart' - ], - ATTR_LOCAL_IP: [ - ['wan', 'localIpAddress'], - None, - 'mdi:access-point-network' - ], - ATTR_STATUS: [ - ['wan', 'online'], - None, - 'mdi:google' - ] + ATTR_NEW_VERSION: [["software", "updateNewVersion"], None, "mdi:update"], + ATTR_UPTIME: [["system", "uptime"], "days", "mdi:timelapse"], + ATTR_LAST_RESTART: [["system", "uptime"], None, "mdi:restart"], + ATTR_LOCAL_IP: [["wan", "localIpAddress"], None, "mdi:access-point-network"], + ATTR_STATUS: [["wan", "online"], None, "mdi:google"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, - default=list(MONITORED_CONDITIONS)): - vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional( + CONF_MONITORED_CONDITIONS, default=list(MONITORED_CONDITIONS) + ): vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -102,7 +88,7 @@ class GoogleWifiSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return '{}_{}'.format(self._name, self._var_name) + return "{}_{}".format(self._name, self._var_name) @property def icon(self): @@ -138,9 +124,9 @@ class GoogleWifiAPI: def __init__(self, host, conditions): """Initialize the data object.""" - uri = 'http://' + uri = "http://" resource = "{}{}{}".format(uri, host, ENDPOINT) - self._request = requests.Request('GET', resource).prepare() + self._request = requests.Request("GET", resource).prepare() self.raw_data = None self.conditions = conditions self.data = { @@ -149,7 +135,7 @@ class GoogleWifiAPI: ATTR_UPTIME: STATE_UNKNOWN, ATTR_LAST_RESTART: STATE_UNKNOWN, ATTR_LOCAL_IP: STATE_UNKNOWN, - ATTR_STATUS: STATE_UNKNOWN + ATTR_STATUS: STATE_UNKNOWN, } self.available = True self.update() @@ -178,28 +164,28 @@ class GoogleWifiAPI: if primary_key in self.raw_data: sensor_value = self.raw_data[primary_key][sensor_key] # Format sensor for better readability - if (attr_key == ATTR_NEW_VERSION and - sensor_value == '0.0.0.0'): - sensor_value = 'Latest' + if attr_key == ATTR_NEW_VERSION and sensor_value == "0.0.0.0": + sensor_value = "Latest" elif attr_key == ATTR_UPTIME: sensor_value = round(sensor_value / (3600 * 24), 2) elif attr_key == ATTR_LAST_RESTART: - last_restart = ( - dt.now() - timedelta(seconds=sensor_value)) - sensor_value = last_restart.strftime( - '%Y-%m-%d %H:%M:%S') + last_restart = dt.now() - timedelta(seconds=sensor_value) + sensor_value = last_restart.strftime("%Y-%m-%d %H:%M:%S") elif attr_key == ATTR_STATUS: if sensor_value: - sensor_value = 'Online' + sensor_value = "Online" else: - sensor_value = 'Offline' + sensor_value = "Offline" elif attr_key == ATTR_LOCAL_IP: - if not self.raw_data['wan']['online']: + if not self.raw_data["wan"]["online"]: sensor_value = STATE_UNKNOWN self.data[attr_key] = sensor_value except KeyError: - _LOGGER.error("Router does not support %s field. " - "Please remove %s from monitored_conditions", - sensor_key, attr_key) + _LOGGER.error( + "Router does not support %s field. " + "Please remove %s from monitored_conditions", + sensor_key, + attr_key, + ) self.data[attr_key] = STATE_UNKNOWN diff --git a/homeassistant/components/googlehome/__init__.py b/homeassistant/components/googlehome/__init__.py index 073081a9634..01e17708fb3 100644 --- a/homeassistant/components/googlehome/__init__.py +++ b/homeassistant/components/googlehome/__init__.py @@ -10,35 +10,42 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) -DOMAIN = 'googlehome' -CLIENT = 'googlehome_client' +DOMAIN = "googlehome" +CLIENT = "googlehome_client" -NAME = 'GoogleHome' +NAME = "GoogleHome" -CONF_DEVICE_TYPES = 'device_types' -CONF_RSSI_THRESHOLD = 'rssi_threshold' -CONF_TRACK_ALARMS = 'track_alarms' -CONF_TRACK_DEVICES = 'track_devices' +CONF_DEVICE_TYPES = "device_types" +CONF_RSSI_THRESHOLD = "rssi_threshold" +CONF_TRACK_ALARMS = "track_alarms" +CONF_TRACK_DEVICES = "track_devices" DEVICE_TYPES = [1, 2, 3] DEFAULT_RSSI_THRESHOLD = -70 -DEVICE_CONFIG = vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DEVICE_TYPES, default=DEVICE_TYPES): - vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]), - vol.Optional(CONF_RSSI_THRESHOLD, default=DEFAULT_RSSI_THRESHOLD): - vol.Coerce(int), - vol.Optional(CONF_TRACK_ALARMS, default=False): cv.boolean, - vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean, -}) +DEVICE_CONFIG = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_DEVICE_TYPES, default=DEVICE_TYPES): vol.All( + cv.ensure_list, [vol.In(DEVICE_TYPES)] + ), + vol.Optional(CONF_RSSI_THRESHOLD, default=DEFAULT_RSSI_THRESHOLD): vol.Coerce( + int + ), + vol.Optional(CONF_TRACK_ALARMS, default=False): cv.boolean, + vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_CONFIG]), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_CONFIG])} + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -47,16 +54,18 @@ async def async_setup(hass, config): hass.data[CLIENT] = GoogleHomeClient(hass) for device in config[DOMAIN][CONF_DEVICES]: - hass.data[DOMAIN][device['host']] = {} + hass.data[DOMAIN][device["host"]] = {} if device[CONF_TRACK_DEVICES]: hass.async_create_task( discovery.async_load_platform( - hass, 'device_tracker', DOMAIN, device, config)) + hass, "device_tracker", DOMAIN, device, config + ) + ) if device[CONF_TRACK_ALARMS]: hass.async_create_task( - discovery.async_load_platform( - hass, 'sensor', DOMAIN, device, config)) + discovery.async_load_platform(hass, "sensor", DOMAIN, device, config) + ) return True @@ -72,6 +81,7 @@ class GoogleHomeClient: async def update_info(self, host): """Update data from Google Home.""" from googledevices.api.connect import Cast + _LOGGER.debug("Updating Google Home info for %s", host) session = async_get_clientsession(self.hass) @@ -79,11 +89,12 @@ class GoogleHomeClient: device_info_data = await device_info.get_device_info() self._connected = bool(device_info_data) - self.hass.data[DOMAIN][host]['info'] = device_info_data + self.hass.data[DOMAIN][host]["info"] = device_info_data async def update_bluetooth(self, host): """Update bluetooth from Google Home.""" from googledevices.api.connect import Cast + _LOGGER.debug("Updating Google Home bluetooth for %s", host) session = async_get_clientsession(self.hass) @@ -92,15 +103,16 @@ class GoogleHomeClient: await asyncio.sleep(5) bluetooth_data = await bluetooth.get_scan_result() - self.hass.data[DOMAIN][host]['bluetooth'] = bluetooth_data + self.hass.data[DOMAIN][host]["bluetooth"] = bluetooth_data async def update_alarms(self, host): """Update alarms from Google Home.""" from googledevices.api.connect import Cast + _LOGGER.debug("Updating Google Home bluetooth for %s", host) session = async_get_clientsession(self.hass) assistant = await Cast(host, self.hass.loop, session).assistant() alarms_data = await assistant.get_alarms() - self.hass.data[DOMAIN][host]['alarms'] = alarms_data + self.hass.data[DOMAIN][host]["alarms"] = alarms_data diff --git a/homeassistant/components/googlehome/device_tracker.py b/homeassistant/components/googlehome/device_tracker.py index 3b6bc5d341c..58350afa430 100644 --- a/homeassistant/components/googlehome/device_tracker.py +++ b/homeassistant/components/googlehome/device_tracker.py @@ -16,11 +16,11 @@ DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Google Home scanner.""" if discovery_info is None: - _LOGGER.warning( - "To use this you need to configure the 'googlehome' component") + _LOGGER.warning("To use this you need to configure the 'googlehome' component") return False - scanner = GoogleHomeDeviceScanner(hass, hass.data[CLIENT], - discovery_info, async_see) + scanner = GoogleHomeDeviceScanner( + hass, hass.data[CLIENT], discovery_info, async_see + ) return await scanner.async_init() @@ -31,49 +31,50 @@ class GoogleHomeDeviceScanner(DeviceScanner): """Initialize the scanner.""" self.async_see = async_see self.hass = hass - self.rssi = config['rssi_threshold'] - self.device_types = config['device_types'] - self.host = config['host'] + self.rssi = config["rssi_threshold"] + self.device_types = config["device_types"] + self.host = config["host"] self.client = client async def async_init(self): """Further initialize connection to Google Home.""" await self.client.update_info(self.host) data = self.hass.data[GOOGLEHOME_DOMAIN][self.host] - info = data.get('info', {}) + info = data.get("info", {}) connected = bool(info) if connected: await self.async_update() - async_track_time_interval(self.hass, - self.async_update, - DEFAULT_SCAN_INTERVAL) + async_track_time_interval( + self.hass, self.async_update, DEFAULT_SCAN_INTERVAL + ) return connected async def async_update(self, now=None): """Ensure the information from Google Home is up to date.""" - _LOGGER.debug('Checking Devices on %s', self.host) + _LOGGER.debug("Checking Devices on %s", self.host) await self.client.update_bluetooth(self.host) data = self.hass.data[GOOGLEHOME_DOMAIN][self.host] - info = data.get('info') - bluetooth = data.get('bluetooth') + info = data.get("info") + bluetooth = data.get("bluetooth") if info is None or bluetooth is None: return - google_home_name = info.get('name', NAME) + google_home_name = info.get("name", NAME) for device in bluetooth: - if (device['device_type'] not in - self.device_types or device['rssi'] < self.rssi): + if ( + device["device_type"] not in self.device_types + or device["rssi"] < self.rssi + ): continue - name = "{} {}".format(self.host, device['mac_address']) + name = "{} {}".format(self.host, device["mac_address"]) attributes = {} - attributes['btle_mac_address'] = device['mac_address'] - attributes['ghname'] = google_home_name - attributes['rssi'] = device['rssi'] - attributes['source_type'] = 'bluetooth' - if device['name']: - attributes['name'] = device['name'] + attributes["btle_mac_address"] = device["mac_address"] + attributes["ghname"] = google_home_name + attributes["rssi"] = device["rssi"] + attributes["source_type"] = "bluetooth" + if device["name"]: + attributes["name"] = device["name"] - await self.async_see(dev_id=slugify(name), - attributes=attributes) + await self.async_see(dev_id=slugify(name), attributes=attributes) diff --git a/homeassistant/components/googlehome/sensor.py b/homeassistant/components/googlehome/sensor.py index 088f4352fa3..6a578e14f5a 100644 --- a/homeassistant/components/googlehome/sensor.py +++ b/homeassistant/components/googlehome/sensor.py @@ -12,30 +12,26 @@ SCAN_INTERVAL = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) -ICON = 'mdi:alarm' +ICON = "mdi:alarm" -SENSOR_TYPES = { - 'timer': 'Timer', - 'alarm': 'Alarm', -} +SENSOR_TYPES = {"timer": "Timer", "alarm": "Alarm"} -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the googlehome sensor platform.""" if discovery_info is None: - _LOGGER.warning( - "To use this you need to configure the 'googlehome' component") + _LOGGER.warning("To use this you need to configure the 'googlehome' component") return - await hass.data[CLIENT].update_info(discovery_info['host']) - data = hass.data[GOOGLEHOME_DOMAIN][discovery_info['host']] - info = data.get('info', {}) + await hass.data[CLIENT].update_info(discovery_info["host"]) + data = hass.data[GOOGLEHOME_DOMAIN][discovery_info["host"]] + info = data.get("info", {}) devices = [] for condition in SENSOR_TYPES: - device = GoogleHomeAlarm(hass.data[CLIENT], condition, - discovery_info, info.get('name', NAME)) + device = GoogleHomeAlarm( + hass.data[CLIENT], condition, discovery_info, info.get("name", NAME) + ) devices.append(device) async_add_entities(devices, True) @@ -46,7 +42,7 @@ class GoogleHomeAlarm(Entity): def __init__(self, client, condition, config, name): """Initialize the GoogleHomeAlarm sensor.""" - self._host = config['host'] + self._host = config["host"] self._client = client self._condition = condition self._name = None @@ -59,14 +55,14 @@ class GoogleHomeAlarm(Entity): await self._client.update_alarms(self._host) data = self.hass.data[GOOGLEHOME_DOMAIN][self._host] - alarms = data.get('alarms')[self._condition] + alarms = data.get("alarms")[self._condition] if not alarms: self._available = False return self._available = True - time_date = dt_util.utc_from_timestamp(min(element['fire_time'] - for element in alarms) - / 1000) + time_date = dt_util.utc_from_timestamp( + min(element["fire_time"] for element in alarms) / 1000 + ) self._state = time_date.isoformat() @property diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py index 76253d32db8..90522a84ce5 100644 --- a/homeassistant/components/gpmdp/media_player.py +++ b/homeassistant/components/gpmdp/media_player.py @@ -6,95 +6,134 @@ import time import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_VOLUME_SET) + MEDIA_TYPE_MUSIC, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_VOLUME_SET, +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) + CONF_HOST, + CONF_NAME, + CONF_PORT, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = 'localhost' -DEFAULT_NAME = 'GPM Desktop Player' +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "GPM Desktop Player" DEFAULT_PORT = 5672 -GPMDP_CONFIG_FILE = 'gpmpd.conf' +GPMDP_CONFIG_FILE = "gpmpd.conf" -SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ - SUPPORT_SEEK | SUPPORT_VOLUME_SET | SUPPORT_PLAY +SUPPORT_GPMDP = ( + SUPPORT_PAUSE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_SEEK + | SUPPORT_VOLUME_SET + | SUPPORT_PLAY +) -PLAYBACK_DICT = {'0': STATE_PAUSED, # Stopped - '1': STATE_PAUSED, - '2': STATE_PLAYING} +PLAYBACK_DICT = {"0": STATE_PAUSED, "1": STATE_PAUSED, "2": STATE_PLAYING} # Stopped -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) def request_configuration(hass, config, url, add_entities_callback): """Request configuration steps from the user.""" configurator = hass.components.configurator - if 'gpmdp' in _CONFIGURING: + if "gpmdp" in _CONFIGURING: configurator.notify_errors( - _CONFIGURING['gpmdp'], "Failed to register, please try again.") + _CONFIGURING["gpmdp"], "Failed to register, please try again." + ) return from websocket import create_connection + websocket = create_connection((url), timeout=1) - websocket.send(json.dumps({ - 'namespace': 'connect', - 'method': 'connect', - 'arguments': ['Home Assistant'] - })) + websocket.send( + json.dumps( + { + "namespace": "connect", + "method": "connect", + "arguments": ["Home Assistant"], + } + ) + ) def gpmdp_configuration_callback(callback_data): """Handle configuration changes.""" while True: from websocket import _exceptions + try: msg = json.loads(websocket.recv()) except _exceptions.WebSocketConnectionClosedException: continue - if msg['channel'] != 'connect': + if msg["channel"] != "connect": continue - if msg['payload'] != "CODE_REQUIRED": + if msg["payload"] != "CODE_REQUIRED": continue - pin = callback_data.get('pin') - websocket.send(json.dumps({'namespace': 'connect', - 'method': 'connect', - 'arguments': ['Home Assistant', pin]})) + pin = callback_data.get("pin") + websocket.send( + json.dumps( + { + "namespace": "connect", + "method": "connect", + "arguments": ["Home Assistant", pin], + } + ) + ) tmpmsg = json.loads(websocket.recv()) - if tmpmsg['channel'] == 'time': - _LOGGER.error("Error setting up GPMDP. Please pause " - "the desktop player and try again") + if tmpmsg["channel"] == "time": + _LOGGER.error( + "Error setting up GPMDP. Please pause " + "the desktop player and try again" + ) break - code = tmpmsg['payload'] - if code == 'CODE_REQUIRED': + code = tmpmsg["payload"] + if code == "CODE_REQUIRED": continue - setup_gpmdp(hass, config, code, - add_entities_callback) + setup_gpmdp(hass, config, code, add_entities_callback) save_json(hass.config.path(GPMDP_CONFIG_FILE), {"CODE": code}) - websocket.send(json.dumps({'namespace': 'connect', - 'method': 'connect', - 'arguments': ['Home Assistant', code]})) + websocket.send( + json.dumps( + { + "namespace": "connect", + "method": "connect", + "arguments": ["Home Assistant", code], + } + ) + ) websocket.close() break - _CONFIGURING['gpmdp'] = configurator.request_config( - DEFAULT_NAME, gpmdp_configuration_callback, + _CONFIGURING["gpmdp"] = configurator.request_config( + DEFAULT_NAME, + gpmdp_configuration_callback, description=( - 'Enter the pin that is displayed in the ' - 'Google Play Music Desktop Player.'), + "Enter the pin that is displayed in the " + "Google Play Music Desktop Player." + ), submit_caption="Submit", - fields=[{'id': 'pin', 'name': 'Pin Code', 'type': 'number'}] + fields=[{"id": "pin", "name": "Pin Code", "type": "number"}], ) @@ -103,15 +142,15 @@ def setup_gpmdp(hass, config, code, add_entities): name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = 'ws://{}:{}'.format(host, port) + url = "ws://{}:{}".format(host, port) if not code: request_configuration(hass, config, url, add_entities) return - if 'gpmdp' in _CONFIGURING: + if "gpmdp" in _CONFIGURING: configurator = hass.components.configurator - configurator.request_done(_CONFIGURING.pop('gpmdp')) + configurator.request_done(_CONFIGURING.pop("gpmdp")) add_entities([GPMDP(name, url, code)], True) @@ -120,9 +159,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the GPMDP platform.""" codeconfig = load_json(hass.config.path(GPMDP_CONFIG_FILE)) if codeconfig: - code = codeconfig.get('CODE') + code = codeconfig.get("CODE") elif discovery_info is not None: - if 'gpmdp' in _CONFIGURING: + if "gpmdp" in _CONFIGURING: return code = None else: @@ -136,6 +175,7 @@ class GPMDP(MediaPlayerDevice): def __init__(self, name, url, code): """Initialize the media player.""" from websocket import create_connection + self._connection = create_connection self._url = url self._authorization_code = code @@ -156,40 +196,52 @@ class GPMDP(MediaPlayerDevice): if self._ws is None: try: self._ws = self._connection((self._url), timeout=1) - msg = json.dumps({'namespace': 'connect', - 'method': 'connect', - 'arguments': ['Home Assistant', - self._authorization_code]}) + msg = json.dumps( + { + "namespace": "connect", + "method": "connect", + "arguments": ["Home Assistant", self._authorization_code], + } + ) self._ws.send(msg) - except (socket.timeout, ConnectionRefusedError, - ConnectionResetError): + except (socket.timeout, ConnectionRefusedError, ConnectionResetError): self._ws = None return self._ws def send_gpmdp_msg(self, namespace, method, with_id=True): """Send ws messages to GPMDP and verify request id in response.""" from websocket import _exceptions + try: websocket = self.get_ws() if websocket is None: self._status = STATE_OFF return self._request_id += 1 - websocket.send(json.dumps({'namespace': namespace, - 'method': method, - 'requestID': self._request_id})) + websocket.send( + json.dumps( + { + "namespace": namespace, + "method": method, + "requestID": self._request_id, + } + ) + ) if not with_id: return while True: msg = json.loads(websocket.recv()) - if 'requestID' in msg: - if msg['requestID'] == self._request_id: + if "requestID" in msg: + if msg["requestID"] == self._request_id: return msg - except (ConnectionRefusedError, ConnectionResetError, - _exceptions.WebSocketTimeoutException, - _exceptions.WebSocketProtocolException, - _exceptions.WebSocketPayloadException, - _exceptions.WebSocketConnectionClosedException): + except ( + ConnectionRefusedError, + ConnectionResetError, + _exceptions.WebSocketTimeoutException, + _exceptions.WebSocketProtocolException, + _exceptions.WebSocketPayloadException, + _exceptions.WebSocketConnectionClosedException, + ): self._ws = None def update(self): @@ -197,22 +249,22 @@ class GPMDP(MediaPlayerDevice): time.sleep(1) try: self._available = True - playstate = self.send_gpmdp_msg('playback', 'getPlaybackState') + playstate = self.send_gpmdp_msg("playback", "getPlaybackState") if playstate is None: return - self._status = PLAYBACK_DICT[str(playstate['value'])] - time_data = self.send_gpmdp_msg('playback', 'getCurrentTime') + self._status = PLAYBACK_DICT[str(playstate["value"])] + time_data = self.send_gpmdp_msg("playback", "getCurrentTime") if time_data is not None: - self._seek_position = int(time_data['value'] / 1000) - track_data = self.send_gpmdp_msg('playback', 'getCurrentTrack') + self._seek_position = int(time_data["value"] / 1000) + track_data = self.send_gpmdp_msg("playback", "getCurrentTrack") if track_data is not None: - self._title = track_data['value']['title'] - self._artist = track_data['value']['artist'] - self._albumart = track_data['value']['albumArt'] - self._duration = int(track_data['value']['duration'] / 1000) - volume_data = self.send_gpmdp_msg('volume', 'getVolume') + self._title = track_data["value"]["title"] + self._artist = track_data["value"]["artist"] + self._albumart = track_data["value"]["albumArt"] + self._duration = int(track_data["value"]["duration"] / 1000) + volume_data = self.send_gpmdp_msg("volume", "getVolume") if volume_data is not None: - self._volume = volume_data['value'] / 100 + self._volume = volume_data["value"] / 100 except OSError: self._available = False @@ -273,21 +325,21 @@ class GPMDP(MediaPlayerDevice): def media_next_track(self): """Send media_next command to media player.""" - self.send_gpmdp_msg('playback', 'forward', False) + self.send_gpmdp_msg("playback", "forward", False) def media_previous_track(self): """Send media_previous command to media player.""" - self.send_gpmdp_msg('playback', 'rewind', False) + self.send_gpmdp_msg("playback", "rewind", False) def media_play(self): """Send media_play command to media player.""" - self.send_gpmdp_msg('playback', 'playPause', False) + self.send_gpmdp_msg("playback", "playPause", False) self._status = STATE_PLAYING self.schedule_update_ha_state() def media_pause(self): """Send media_pause command to media player.""" - self.send_gpmdp_msg('playback', 'playPause', False) + self.send_gpmdp_msg("playback", "playPause", False) self._status = STATE_PAUSED self.schedule_update_ha_state() @@ -296,9 +348,15 @@ class GPMDP(MediaPlayerDevice): websocket = self.get_ws() if websocket is None: return - websocket.send(json.dumps({'namespace': 'playback', - 'method': 'setCurrentTime', - 'arguments': [position*1000]})) + websocket.send( + json.dumps( + { + "namespace": "playback", + "method": "setCurrentTime", + "arguments": [position * 1000], + } + ) + ) self.schedule_update_ha_state() def volume_up(self): @@ -322,7 +380,13 @@ class GPMDP(MediaPlayerDevice): websocket = self.get_ws() if websocket is None: return - websocket.send(json.dumps({'namespace': 'volume', - 'method': 'setVolume', - 'arguments': [volume*100]})) + websocket.send( + json.dumps( + { + "namespace": "volume", + "method": "setVolume", + "arguments": [volume * 100], + } + ) + ) self.schedule_update_ha_state() diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index cccf59a822a..ab4545256ae 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -5,28 +5,34 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_LATITUDE, ATTR_LONGITUDE, CONF_HOST, CONF_PORT, - CONF_NAME) + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_HOST, + CONF_PORT, + CONF_NAME, +) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_CLIMB = 'climb' -ATTR_ELEVATION = 'elevation' -ATTR_GPS_TIME = 'gps_time' -ATTR_MODE = 'mode' -ATTR_SPEED = 'speed' +ATTR_CLIMB = "climb" +ATTR_ELEVATION = "elevation" +ATTR_GPS_TIME = "gps_time" +ATTR_MODE = "mode" +ATTR_SPEED = "speed" -DEFAULT_HOST = 'localhost' -DEFAULT_NAME = 'GPS' +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "GPS" DEFAULT_PORT = 2947 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/gpslogger/.translations/bg.json b/homeassistant/components/gpslogger/.translations/bg.json index 6f06d5c00c6..f7ce9a67f54 100644 --- a/homeassistant/components/gpslogger/.translations/bg.json +++ b/homeassistant/components/gpslogger/.translations/bg.json @@ -1,7 +1,18 @@ { "config": { "abort": { + "not_internet_accessible": "Home Assistant \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u0435\u043d \u043e\u0442 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0437\u0430 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430 \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e\u0442 GPSLogger.", "one_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." - } + }, + "create_entry": { + "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 GPSLogger. \n\n \u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." + }, + "step": { + "user": { + "description": "\u0421\u0438\u0433\u0443\u0440\u043d\u0438 \u043b\u0438 \u0441\u0442\u0435, \u0447\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 GPSLogger Webhook?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435 \u043d\u0430 GPSLogger Webhook" + } + }, + "title": "GPSLogger Webhook" } } \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/ko.json b/homeassistant/components/gpslogger/.translations/ko.json index 2c8881034ff..786a67b0b19 100644 --- a/homeassistant/components/gpslogger/.translations/ko.json +++ b/homeassistant/components/gpslogger/.translations/ko.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 GPSLogger \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 GPSLogger \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/gpslogger/.translations/pt-BR.json b/homeassistant/components/gpslogger/.translations/pt-BR.json new file mode 100644 index 00000000000..86c68a4cfb9 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel na Internet para receber mensagens do GPSLogger.", + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso webhook no GPSLogger. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n\n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) para mais detalhes." + }, + "step": { + "user": { + "description": "Tem a certeza que deseja configurar o GPSLogger Webhook?", + "title": "Configurar o GPSLogger Webhook" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 869b4b66987..839adec2f5b 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -6,8 +6,13 @@ from aiohttp import web import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ATTR_BATTERY -from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, \ - HTTP_OK, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID +from homeassistant.const import ( + HTTP_UNPROCESSABLE_ENTITY, + HTTP_OK, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_WEBHOOK_ID, +) from homeassistant.helpers import config_entry_flow from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER @@ -24,7 +29,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) +TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) DEFAULT_ACCURACY = 200 @@ -33,29 +38,28 @@ DEFAULT_BATTERY = -1 def _id(value: str) -> str: """Coerce id by removing '-'.""" - return value.replace('-', '') + return value.replace("-", "") -WEBHOOK_SCHEMA = vol.Schema({ - vol.Required(ATTR_DEVICE): _id, - vol.Required(ATTR_LATITUDE): cv.latitude, - vol.Required(ATTR_LONGITUDE): cv.longitude, - vol.Optional(ATTR_ACCURACY, default=DEFAULT_ACCURACY): vol.Coerce(float), - vol.Optional(ATTR_ACTIVITY): cv.string, - vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), - vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), - vol.Optional(ATTR_DIRECTION): vol.Coerce(float), - vol.Optional(ATTR_PROVIDER): cv.string, - vol.Optional(ATTR_SPEED): vol.Coerce(float), -}) +WEBHOOK_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE): _id, + vol.Required(ATTR_LATITUDE): cv.latitude, + vol.Required(ATTR_LONGITUDE): cv.longitude, + vol.Optional(ATTR_ACCURACY, default=DEFAULT_ACCURACY): vol.Coerce(float), + vol.Optional(ATTR_ACTIVITY): cv.string, + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), + vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), + vol.Optional(ATTR_DIRECTION): vol.Coerce(float), + vol.Optional(ATTR_PROVIDER): cv.string, + vol.Optional(ATTR_SPEED): vol.Coerce(float), + } +) async def async_setup(hass, hass_config): """Set up the GPSLogger component.""" - hass.data[DOMAIN] = { - 'devices': set(), - 'unsub_device_tracker': {}, - } + hass.data[DOMAIN] = {"devices": set(), "unsub_device_tracker": {}} return True @@ -64,36 +68,36 @@ async def handle_webhook(hass, webhook_id, request): try: data = WEBHOOK_SCHEMA(dict(await request.post())) except vol.MultipleInvalid as error: - return web.Response( - text=error.error_message, - status=HTTP_UNPROCESSABLE_ENTITY - ) + return web.Response(text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY) attrs = { ATTR_SPEED: data.get(ATTR_SPEED), ATTR_DIRECTION: data.get(ATTR_DIRECTION), ATTR_ALTITUDE: data.get(ATTR_ALTITUDE), ATTR_PROVIDER: data.get(ATTR_PROVIDER), - ATTR_ACTIVITY: data.get(ATTR_ACTIVITY) + ATTR_ACTIVITY: data.get(ATTR_ACTIVITY), } device = data[ATTR_DEVICE] async_dispatcher_send( - hass, TRACKER_UPDATE, device, + hass, + TRACKER_UPDATE, + device, (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), - data[ATTR_BATTERY], data[ATTR_ACCURACY], attrs) - - return web.Response( - text='Setting location for {}'.format(device), - status=HTTP_OK + data[ATTR_BATTERY], + data[ATTR_ACCURACY], + attrs, ) + return web.Response(text="Setting location for {}".format(device), status=HTTP_OK) + async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( - DOMAIN, 'GPSLogger', entry.data[CONF_WEBHOOK_ID], handle_webhook) + DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook + ) hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) @@ -104,7 +108,7 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) - hass.data[DOMAIN]['unsub_device_tracker'].pop(entry.entry_id)() + hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) return True diff --git a/homeassistant/components/gpslogger/config_flow.py b/homeassistant/components/gpslogger/config_flow.py index f48d9abc680..a572bddd0e9 100644 --- a/homeassistant/components/gpslogger/config_flow.py +++ b/homeassistant/components/gpslogger/config_flow.py @@ -5,8 +5,6 @@ from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, - 'GPSLogger Webhook', - { - 'docs_url': 'https://www.home-assistant.io/components/gpslogger/' - } + "GPSLogger Webhook", + {"docs_url": "https://www.home-assistant.io/components/gpslogger/"}, ) diff --git a/homeassistant/components/gpslogger/const.py b/homeassistant/components/gpslogger/const.py index 870c5310f29..48dc9e7a431 100644 --- a/homeassistant/components/gpslogger/const.py +++ b/homeassistant/components/gpslogger/const.py @@ -1,11 +1,11 @@ """Const for GPSLogger.""" -DOMAIN = 'gpslogger' +DOMAIN = "gpslogger" -ATTR_ALTITUDE = 'altitude' -ATTR_ACCURACY = 'accuracy' -ATTR_ACTIVITY = 'activity' -ATTR_DEVICE = 'device' -ATTR_DIRECTION = 'direction' -ATTR_PROVIDER = 'provider' -ATTR_SPEED = 'speed' +ATTR_ALTITUDE = "altitude" +ATTR_ACCURACY = "accuracy" +ATTR_ACTIVITY = "activity" +ATTR_DEVICE = "device" +ATTR_DIRECTION = "direction" +ATTR_PROVIDER = "provider" +ATTR_SPEED = "speed" diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 254c9d2b391..c9dbbfee026 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -9,9 +9,7 @@ from homeassistant.const import ( ATTR_LONGITUDE, ) from homeassistant.components.device_tracker import SOURCE_TYPE_GPS -from homeassistant.components.device_tracker.config_entry import ( - TrackerEntity -) +from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity @@ -29,23 +27,22 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, entry, - async_add_entities): +async def async_setup_entry(hass: HomeAssistantType, entry, async_add_entities): """Configure a dispatcher connection based on a config entry.""" + @callback def _receive_data(device, gps, battery, accuracy, attrs): """Receive set location.""" - if device in hass.data[GPL_DOMAIN]['devices']: + if device in hass.data[GPL_DOMAIN]["devices"]: return - hass.data[GPL_DOMAIN]['devices'].add(device) + hass.data[GPL_DOMAIN]["devices"].add(device) - async_add_entities([GPSLoggerEntity( - device, gps, battery, accuracy, attrs - )]) + async_add_entities([GPSLoggerEntity(device, gps, battery, accuracy, attrs)]) - hass.data[GPL_DOMAIN]['unsub_device_tracker'][entry.entry_id] = \ - async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + hass.data[GPL_DOMAIN]["unsub_device_tracker"][ + entry.entry_id + ] = async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) # Restore previously loaded devices dev_reg = await device_registry.async_get_registry(hass) @@ -60,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry, entities = [] for dev_id in dev_ids: - hass.data[GPL_DOMAIN]['devices'].add(dev_id) + hass.data[GPL_DOMAIN]["devices"].add(dev_id) entity = GPSLoggerEntity(dev_id, None, None, None, None) entities.append(entity) @@ -70,8 +67,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry, class GPSLoggerEntity(TrackerEntity, RestoreEntity): """Represent a tracked device.""" - def __init__( - self, device, location, battery, accuracy, attributes): + def __init__(self, device, location, battery, accuracy, attributes): """Set up Geofency entity.""" self._accuracy = accuracy self._attributes = attributes @@ -124,10 +120,7 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): @property def device_info(self): """Return the device info.""" - return { - 'name': self._name, - 'identifiers': {(GPL_DOMAIN, self._unique_id)}, - } + return {"name": self._name, "identifiers": {(GPL_DOMAIN, self._unique_id)}} @property def source_type(self): @@ -138,7 +131,8 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): """Register state update callback.""" await super().async_added_to_hass() self._unsub_dispatcher = async_dispatcher_connect( - self.hass, TRACKER_UPDATE, self._async_receive_data) + self.hass, TRACKER_UPDATE, self._async_receive_data + ) # don't restore if we got created with data if self._location is not None: @@ -159,10 +153,7 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): return attr = state.attributes - self._location = ( - attr.get(ATTR_LATITUDE), - attr.get(ATTR_LONGITUDE), - ) + self._location = (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) self._accuracy = attr.get(ATTR_GPS_ACCURACY) self._attributes = { ATTR_ALTITUDE: attr.get(ATTR_ALTITUDE), @@ -179,8 +170,7 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): self._unsub_dispatcher() @callback - def _async_receive_data(self, device, location, battery, accuracy, - attributes): + def _async_receive_data(self, device, location, battery, accuracy, attributes): """Mark the device as seen.""" if device != self.name: return diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index e3f9e359f5a..550a0ce1d13 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -9,24 +9,34 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_HOST, CONF_PORT, CONF_PREFIX, EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED) + CONF_HOST, + CONF_PORT, + CONF_PREFIX, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + EVENT_STATE_CHANGED, +) from homeassistant.helpers import state _LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = 'localhost' +DEFAULT_HOST = "localhost" DEFAULT_PORT = 2003 -DEFAULT_PREFIX = 'ha' -DOMAIN = 'graphite' +DEFAULT_PREFIX = "ha" +DOMAIN = "graphite" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, config): @@ -59,7 +69,7 @@ class GraphiteFeeder(threading.Thread): self._host = host self._port = port # rstrip any trailing dots in case they think they need it - self._prefix = prefix.rstrip('.') + self._prefix = prefix.rstrip(".") self._queue = queue.Queue() self._quit_object = object() self._we_started = False @@ -67,8 +77,7 @@ class GraphiteFeeder(threading.Thread): hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.start_listen) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown) hass.bus.listen(EVENT_STATE_CHANGED, self.event_listener) - _LOGGER.debug("Graphite feeding to %s:%i initialized", - self._host, self._port) + _LOGGER.debug("Graphite feeding to %s:%i initialized", self._host, self._port) def start_listen(self, event): """Start event-processing thread.""" @@ -87,16 +96,15 @@ class GraphiteFeeder(threading.Thread): _LOGGER.debug("Received event") self._queue.put(event) else: - _LOGGER.error( - "Graphite feeder thread has died, not queuing event") + _LOGGER.error("Graphite feeder thread has died, not queuing event") def _send_to_graphite(self, data): """Send data to Graphite.""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(10) sock.connect((self._host, self._port)) - sock.sendall(data.encode('ascii')) - sock.send('\n'.encode('ascii')) + sock.sendall(data.encode("ascii")) + sock.send("\n".encode("ascii")) sock.close() def _report_attributes(self, entity_id, new_state): @@ -104,19 +112,20 @@ class GraphiteFeeder(threading.Thread): now = time.time() things = dict(new_state.attributes) try: - things['state'] = state.state_as_number(new_state) + things["state"] = state.state_as_number(new_state) except ValueError: pass - lines = ['%s.%s.%s %f %i' % (self._prefix, - entity_id, key.replace(' ', '_'), - value, now) - for key, value in things.items() - if isinstance(value, (float, int))] + lines = [ + "%s.%s.%s %f %i" + % (self._prefix, entity_id, key.replace(" ", "_"), value, now) + for key, value in things.items() + if isinstance(value, (float, int)) + ] if not lines: return _LOGGER.debug("Sending to graphite: %s", lines) try: - self._send_to_graphite('\n'.join(lines)) + self._send_to_graphite("\n".join(lines)) except socket.gaierror: _LOGGER.error("Unable to connect to host %s", self._host) except socket.error: @@ -130,19 +139,19 @@ class GraphiteFeeder(threading.Thread): _LOGGER.debug("Event processing thread stopped") self._queue.task_done() return - if event.event_type == EVENT_STATE_CHANGED and \ - event.data.get('new_state'): - _LOGGER.debug("Processing STATE_CHANGED event for %s", - event.data['entity_id']) + if event.event_type == EVENT_STATE_CHANGED and event.data.get("new_state"): + _LOGGER.debug( + "Processing STATE_CHANGED event for %s", event.data["entity_id"] + ) try: self._report_attributes( - event.data['entity_id'], event.data['new_state']) + event.data["entity_id"], event.data["new_state"] + ) except Exception: # pylint: disable=broad-except # Catch this so we can avoid the thread dying and # make it visible. _LOGGER.exception("Failed to process STATE_CHANGED event") else: - _LOGGER.warning( - "Processing unexpected event type %s", event.event_type) + _LOGGER.warning("Processing unexpected event type %s", event.event_type) self._queue.task_done() diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index 0f12c3cd479..cb67ac7faa4 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -7,102 +7,105 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, CONF_TEMPERATURE_UNIT, - EVENT_HOMEASSISTANT_STOP) + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform _LOGGER = logging.getLogger(__name__) -CONF_CHANNELS = 'channels' -CONF_COUNTED_QUANTITY = 'counted_quantity' -CONF_COUNTED_QUANTITY_PER_PULSE = 'counted_quantity_per_pulse' -CONF_MONITOR_SERIAL_NUMBER = 'monitor' -CONF_MONITORS = 'monitors' -CONF_NET_METERING = 'net_metering' -CONF_NUMBER = 'number' -CONF_PULSE_COUNTERS = 'pulse_counters' -CONF_SERIAL_NUMBER = 'serial_number' -CONF_SENSORS = 'sensors' -CONF_SENSOR_TYPE = 'sensor_type' -CONF_TEMPERATURE_SENSORS = 'temperature_sensors' -CONF_TIME_UNIT = 'time_unit' +CONF_CHANNELS = "channels" +CONF_COUNTED_QUANTITY = "counted_quantity" +CONF_COUNTED_QUANTITY_PER_PULSE = "counted_quantity_per_pulse" +CONF_MONITOR_SERIAL_NUMBER = "monitor" +CONF_MONITORS = "monitors" +CONF_NET_METERING = "net_metering" +CONF_NUMBER = "number" +CONF_PULSE_COUNTERS = "pulse_counters" +CONF_SERIAL_NUMBER = "serial_number" +CONF_SENSORS = "sensors" +CONF_SENSOR_TYPE = "sensor_type" +CONF_TEMPERATURE_SENSORS = "temperature_sensors" +CONF_TIME_UNIT = "time_unit" -DATA_GREENEYE_MONITOR = 'greeneye_monitor' -DOMAIN = 'greeneye_monitor' +DATA_GREENEYE_MONITOR = "greeneye_monitor" +DOMAIN = "greeneye_monitor" -SENSOR_TYPE_CURRENT = 'current_sensor' -SENSOR_TYPE_PULSE_COUNTER = 'pulse_counter' -SENSOR_TYPE_TEMPERATURE = 'temperature_sensor' +SENSOR_TYPE_CURRENT = "current_sensor" +SENSOR_TYPE_PULSE_COUNTER = "pulse_counter" +SENSOR_TYPE_TEMPERATURE = "temperature_sensor" -TEMPERATURE_UNIT_CELSIUS = 'C' +TEMPERATURE_UNIT_CELSIUS = "C" -TIME_UNIT_SECOND = 's' -TIME_UNIT_MINUTE = 'min' -TIME_UNIT_HOUR = 'h' +TIME_UNIT_SECOND = "s" +TIME_UNIT_MINUTE = "min" +TIME_UNIT_HOUR = "h" -TEMPERATURE_SENSOR_SCHEMA = vol.Schema({ - vol.Required(CONF_NUMBER): vol.Range(1, 8), - vol.Required(CONF_NAME): cv.string, -}) +TEMPERATURE_SENSOR_SCHEMA = vol.Schema( + {vol.Required(CONF_NUMBER): vol.Range(1, 8), vol.Required(CONF_NAME): cv.string} +) -TEMPERATURE_SENSORS_SCHEMA = vol.Schema({ - vol.Required(CONF_TEMPERATURE_UNIT): cv.temperature_unit, - vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, - [TEMPERATURE_SENSOR_SCHEMA]), -}) +TEMPERATURE_SENSORS_SCHEMA = vol.Schema( + { + vol.Required(CONF_TEMPERATURE_UNIT): cv.temperature_unit, + vol.Required(CONF_SENSORS): vol.All( + cv.ensure_list, [TEMPERATURE_SENSOR_SCHEMA] + ), + } +) -PULSE_COUNTER_SCHEMA = vol.Schema({ - vol.Required(CONF_NUMBER): vol.Range(1, 4), - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_COUNTED_QUANTITY): cv.string, - vol.Optional( - CONF_COUNTED_QUANTITY_PER_PULSE, default=1.0): vol.Coerce(float), - vol.Optional(CONF_TIME_UNIT, default=TIME_UNIT_SECOND): vol.Any( - TIME_UNIT_SECOND, - TIME_UNIT_MINUTE, - TIME_UNIT_HOUR), -}) +PULSE_COUNTER_SCHEMA = vol.Schema( + { + vol.Required(CONF_NUMBER): vol.Range(1, 4), + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_COUNTED_QUANTITY): cv.string, + vol.Optional(CONF_COUNTED_QUANTITY_PER_PULSE, default=1.0): vol.Coerce(float), + vol.Optional(CONF_TIME_UNIT, default=TIME_UNIT_SECOND): vol.Any( + TIME_UNIT_SECOND, TIME_UNIT_MINUTE, TIME_UNIT_HOUR + ), + } +) PULSE_COUNTERS_SCHEMA = vol.All(cv.ensure_list, [PULSE_COUNTER_SCHEMA]) -CHANNEL_SCHEMA = vol.Schema({ - vol.Required(CONF_NUMBER): vol.Range(1, 48), - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_NET_METERING, default=False): cv.boolean, -}) +CHANNEL_SCHEMA = vol.Schema( + { + vol.Required(CONF_NUMBER): vol.Range(1, 48), + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_NET_METERING, default=False): cv.boolean, + } +) CHANNELS_SCHEMA = vol.All(cv.ensure_list, [CHANNEL_SCHEMA]) -MONITOR_SCHEMA = vol.Schema({ - vol.Required(CONF_SERIAL_NUMBER): - vol.All( +MONITOR_SCHEMA = vol.Schema( + { + vol.Required(CONF_SERIAL_NUMBER): vol.All( cv.string, vol.Length( min=8, max=8, msg="GEM serial number must be specified as an 8-character " - "string (including leading zeroes)."), - vol.Coerce(int)), - vol.Optional(CONF_CHANNELS, default=[]): CHANNELS_SCHEMA, - vol.Optional( - CONF_TEMPERATURE_SENSORS, - default={ - CONF_TEMPERATURE_UNIT: TEMPERATURE_UNIT_CELSIUS, - CONF_SENSORS: [], - }): TEMPERATURE_SENSORS_SCHEMA, - vol.Optional(CONF_PULSE_COUNTERS, default=[]): PULSE_COUNTERS_SCHEMA, -}) + "string (including leading zeroes).", + ), + vol.Coerce(int), + ), + vol.Optional(CONF_CHANNELS, default=[]): CHANNELS_SCHEMA, + vol.Optional( + CONF_TEMPERATURE_SENSORS, + default={CONF_TEMPERATURE_UNIT: TEMPERATURE_UNIT_CELSIUS, CONF_SENSORS: []}, + ): TEMPERATURE_SENSORS_SCHEMA, + vol.Optional(CONF_PULSE_COUNTERS, default=[]): PULSE_COUNTERS_SCHEMA, + } +) MONITORS_SCHEMA = vol.All(cv.ensure_list, [MONITOR_SCHEMA]) -COMPONENT_SCHEMA = vol.Schema({ - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_MONITORS): MONITORS_SCHEMA, -}) +COMPONENT_SCHEMA = vol.Schema( + {vol.Required(CONF_PORT): cv.port, vol.Required(CONF_MONITORS): MONITORS_SCHEMA} +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: COMPONENT_SCHEMA, -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA) async def async_setup(hass, config): @@ -124,49 +127,53 @@ async def async_setup(hass, config): all_sensors = [] for monitor_config in server_config[CONF_MONITORS]: monitor_serial_number = { - CONF_MONITOR_SERIAL_NUMBER: monitor_config[CONF_SERIAL_NUMBER], + CONF_MONITOR_SERIAL_NUMBER: monitor_config[CONF_SERIAL_NUMBER] } channel_configs = monitor_config[CONF_CHANNELS] for channel_config in channel_configs: - all_sensors.append({ - CONF_SENSOR_TYPE: SENSOR_TYPE_CURRENT, - **monitor_serial_number, - **channel_config, - }) + all_sensors.append( + { + CONF_SENSOR_TYPE: SENSOR_TYPE_CURRENT, + **monitor_serial_number, + **channel_config, + } + ) - sensor_configs = \ - monitor_config[CONF_TEMPERATURE_SENSORS] + sensor_configs = monitor_config[CONF_TEMPERATURE_SENSORS] if sensor_configs: temperature_unit = { - CONF_TEMPERATURE_UNIT: sensor_configs[CONF_TEMPERATURE_UNIT], + CONF_TEMPERATURE_UNIT: sensor_configs[CONF_TEMPERATURE_UNIT] } for sensor_config in sensor_configs[CONF_SENSORS]: - all_sensors.append({ - CONF_SENSOR_TYPE: SENSOR_TYPE_TEMPERATURE, - **monitor_serial_number, - **temperature_unit, - **sensor_config, - }) + all_sensors.append( + { + CONF_SENSOR_TYPE: SENSOR_TYPE_TEMPERATURE, + **monitor_serial_number, + **temperature_unit, + **sensor_config, + } + ) counter_configs = monitor_config[CONF_PULSE_COUNTERS] for counter_config in counter_configs: - all_sensors.append({ - CONF_SENSOR_TYPE: SENSOR_TYPE_PULSE_COUNTER, - **monitor_serial_number, - **counter_config, - }) + all_sensors.append( + { + CONF_SENSOR_TYPE: SENSOR_TYPE_PULSE_COUNTER, + **monitor_serial_number, + **counter_config, + } + ) if not all_sensors: - _LOGGER.error("Configuration must specify at least one " - "channel, pulse counter or temperature sensor") + _LOGGER.error( + "Configuration must specify at least one " + "channel, pulse counter or temperature sensor" + ) return False - hass.async_create_task(async_load_platform( - hass, - 'sensor', - DOMAIN, - all_sensors, - config)) + hass.async_create_task( + async_load_platform(hass, "sensor", DOMAIN, all_sensors, config) + ) return True diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 499d5351ad4..c4b5fc67898 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -23,21 +23,17 @@ from . import ( _LOGGER = logging.getLogger(__name__) -DATA_PULSES = 'pulses' -DATA_WATT_SECONDS = 'watt_seconds' +DATA_PULSES = "pulses" +DATA_WATT_SECONDS = "watt_seconds" UNIT_WATTS = POWER_WATT -COUNTER_ICON = 'mdi:counter' -CURRENT_SENSOR_ICON = 'mdi:flash' -TEMPERATURE_ICON = 'mdi:thermometer' +COUNTER_ICON = "mdi:counter" +CURRENT_SENSOR_ICON = "mdi:flash" +TEMPERATURE_ICON = "mdi:thermometer" -async def async_setup_platform( - hass, - config, - async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a single GEM temperature sensor.""" if not discovery_info: return @@ -46,25 +42,34 @@ async def async_setup_platform( for sensor in discovery_info: sensor_type = sensor[CONF_SENSOR_TYPE] if sensor_type == SENSOR_TYPE_CURRENT: - entities.append(CurrentSensor( - sensor[CONF_MONITOR_SERIAL_NUMBER], - sensor[CONF_NUMBER], - sensor[CONF_NAME], - sensor[CONF_NET_METERING])) + entities.append( + CurrentSensor( + sensor[CONF_MONITOR_SERIAL_NUMBER], + sensor[CONF_NUMBER], + sensor[CONF_NAME], + sensor[CONF_NET_METERING], + ) + ) elif sensor_type == SENSOR_TYPE_PULSE_COUNTER: - entities.append(PulseCounter( - sensor[CONF_MONITOR_SERIAL_NUMBER], - sensor[CONF_NUMBER], - sensor[CONF_NAME], - sensor[CONF_COUNTED_QUANTITY], - sensor[CONF_TIME_UNIT], - sensor[CONF_COUNTED_QUANTITY_PER_PULSE])) + entities.append( + PulseCounter( + sensor[CONF_MONITOR_SERIAL_NUMBER], + sensor[CONF_NUMBER], + sensor[CONF_NAME], + sensor[CONF_COUNTED_QUANTITY], + sensor[CONF_TIME_UNIT], + sensor[CONF_COUNTED_QUANTITY_PER_PULSE], + ) + ) elif sensor_type == SENSOR_TYPE_TEMPERATURE: - entities.append(TemperatureSensor( - sensor[CONF_MONITOR_SERIAL_NUMBER], - sensor[CONF_NUMBER], - sensor[CONF_NAME], - sensor[CONF_TEMPERATURE_UNIT])) + entities.append( + TemperatureSensor( + sensor[CONF_MONITOR_SERIAL_NUMBER], + sensor[CONF_NUMBER], + sensor[CONF_NAME], + sensor[CONF_TEMPERATURE_UNIT], + ) + ) async_add_entities(entities) @@ -141,7 +146,7 @@ class CurrentSensor(GEMSensor): def __init__(self, monitor_serial_number, number, name, net_metering): """Construct the entity.""" - super().__init__(monitor_serial_number, name, 'current', number) + super().__init__(monitor_serial_number, name, "current", number) self._net_metering = net_metering def _get_sensor(self, monitor): @@ -176,24 +181,23 @@ class CurrentSensor(GEMSensor): else: watt_seconds = self._sensor.absolute_watt_seconds - return { - DATA_WATT_SECONDS: watt_seconds - } + return {DATA_WATT_SECONDS: watt_seconds} class PulseCounter(GEMSensor): """Entity showing rate of change in one pulse counter of the monitor.""" def __init__( - self, - monitor_serial_number, - number, - name, - counted_quantity, - time_unit, - counted_quantity_per_pulse): + self, + monitor_serial_number, + number, + name, + counted_quantity, + time_unit, + counted_quantity_per_pulse, + ): """Construct the entity.""" - super().__init__(monitor_serial_number, name, 'pulse', number) + super().__init__(monitor_serial_number, name, "pulse", number) self._counted_quantity = counted_quantity self._counted_quantity_per_pulse = counted_quantity_per_pulse self._time_unit = time_unit @@ -212,9 +216,11 @@ class PulseCounter(GEMSensor): if not self._sensor or self._sensor.pulses_per_second is None: return None - return (self._sensor.pulses_per_second * - self._counted_quantity_per_pulse * - self._seconds_per_time_unit) + return ( + self._sensor.pulses_per_second + * self._counted_quantity_per_pulse + * self._seconds_per_time_unit + ) @property def _seconds_per_time_unit(self): @@ -230,8 +236,7 @@ class PulseCounter(GEMSensor): def unit_of_measurement(self): """Return the unit of measurement for this pulse counter.""" return "{counted_quantity}/{time_unit}".format( - counted_quantity=self._counted_quantity, - time_unit=self._time_unit, + counted_quantity=self._counted_quantity, time_unit=self._time_unit ) @property @@ -240,9 +245,7 @@ class PulseCounter(GEMSensor): if not self._sensor: return None - return { - DATA_PULSES: self._sensor.pulses - } + return {DATA_PULSES: self._sensor.pulses} class TemperatureSensor(GEMSensor): @@ -250,7 +253,7 @@ class TemperatureSensor(GEMSensor): def __init__(self, monitor_serial_number, number, name, unit): """Construct the entity.""" - super().__init__(monitor_serial_number, name, 'temp', number) + super().__init__(monitor_serial_number, name, "temp", number) self._unit = unit def _get_sensor(self, monitor): diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index a8418a01ac2..5a6ce2c51c2 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -5,21 +5,24 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light) + ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + Light, +) from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -CONF_VERSION = 'version' +CONF_VERSION = "version" SUPPORTED_FEATURES = SUPPORT_BRIGHTNESS -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_VERSION): cv.positive_int, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_VERSION): cv.positive_int} +) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) @@ -28,25 +31,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Greenwave Reality Platform.""" import greenwavereality as greenwave import os + host = config.get(CONF_HOST) - tokenfile = hass.config.path('.greenwave') + tokenfile = hass.config.path(".greenwave") if config.get(CONF_VERSION) == 3: if os.path.exists(tokenfile): with open(tokenfile) as tokenfile: token = tokenfile.read() else: try: - token = greenwave.grab_token(host, 'hass', 'homeassistant') + token = greenwave.grab_token(host, "hass", "homeassistant") except PermissionError: - _LOGGER.error('The Gateway Is Not In Sync Mode') + _LOGGER.error("The Gateway Is Not In Sync Mode") raise with open(tokenfile, "w+") as tokenfile: tokenfile.write(token) else: token = None bulbs = greenwave.grab_bulbs(host, token) - add_entities(GreenwaveLight(device, host, token, GatewayData(host, token)) - for device in bulbs.values()) + add_entities( + GreenwaveLight(device, host, token, GatewayData(host, token)) + for device in bulbs.values() + ) class GreenwaveLight(Light): @@ -55,9 +61,10 @@ class GreenwaveLight(Light): def __init__(self, light, host, token, gatewaydata): """Initialize a Greenwave Reality Light.""" import greenwavereality as greenwave - self._did = int(light['did']) - self._name = light['name'] - self._state = int(light['state']) + + self._did = int(light["did"]) + self._name = light["name"] + self._state = int(light["state"]) self._brightness = greenwave.hass_brightness(light) self._host = host self._online = greenwave.check_online(light) @@ -92,27 +99,28 @@ class GreenwaveLight(Light): def turn_on(self, **kwargs): """Instruct the light to turn on.""" import greenwavereality as greenwave - temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) - / 255) * 100) - greenwave.set_brightness(self._host, self._did, - temp_brightness, self._token) + + temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) / 255) * 100) + greenwave.set_brightness(self._host, self._did, temp_brightness, self._token) greenwave.turn_on(self._host, self._did, self._token) def turn_off(self, **kwargs): """Instruct the light to turn off.""" import greenwavereality as greenwave + greenwave.turn_off(self._host, self._did, self._token) def update(self): """Fetch new state data for this light.""" import greenwavereality as greenwave + self._gatewaydata.update() bulbs = self._gatewaydata.greenwave - self._state = int(bulbs[self._did]['state']) + self._state = int(bulbs[self._did]["state"]) self._brightness = greenwave.hass_brightness(bulbs[self._did]) self._online = greenwave.check_online(bulbs[self._did]) - self._name = bulbs[self._did]['name'] + self._name = bulbs[self._did]["name"] class GatewayData: @@ -121,6 +129,7 @@ class GatewayData: def __init__(self, host, token): """Initialize the data object.""" import greenwavereality as greenwave + self._host = host self._token = token self._greenwave = greenwave.grab_bulbs(host, token) @@ -134,5 +143,6 @@ class GatewayData: def update(self): """Get the latest data from the gateway.""" import greenwavereality as greenwave + self._greenwave = greenwave.grab_bulbs(self._host, self._token) return self._greenwave diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index d13580ec42a..fc10fa2f737 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -6,67 +6,82 @@ import voluptuous as vol from homeassistant import core as ha from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, - STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, - STATE_UNLOCKED, STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, - ATTR_ASSUMED_STATE, SERVICE_RELOAD, ATTR_NAME, ATTR_ICON) + ATTR_ENTITY_ID, + CONF_ICON, + CONF_NAME, + STATE_CLOSED, + STATE_HOME, + STATE_NOT_HOME, + STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_LOCKED, + STATE_UNLOCKED, + STATE_OK, + STATE_PROBLEM, + STATE_UNKNOWN, + ATTR_ASSUMED_STATE, + SERVICE_RELOAD, + ATTR_NAME, + ATTR_ICON, +) from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.util.async_ import run_coroutine_threadsafe from .reproduce_state import async_reproduce_states # noqa -DOMAIN = 'group' +DOMAIN = "group" -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" -CONF_ENTITIES = 'entities' -CONF_VIEW = 'view' -CONF_CONTROL = 'control' -CONF_ALL = 'all' +CONF_ENTITIES = "entities" +CONF_VIEW = "view" +CONF_CONTROL = "control" +CONF_ALL = "all" -ATTR_ADD_ENTITIES = 'add_entities' -ATTR_AUTO = 'auto' -ATTR_CONTROL = 'control' -ATTR_ENTITIES = 'entities' -ATTR_OBJECT_ID = 'object_id' -ATTR_ORDER = 'order' -ATTR_VIEW = 'view' -ATTR_VISIBLE = 'visible' -ATTR_ALL = 'all' +ATTR_ADD_ENTITIES = "add_entities" +ATTR_AUTO = "auto" +ATTR_CONTROL = "control" +ATTR_ENTITIES = "entities" +ATTR_OBJECT_ID = "object_id" +ATTR_ORDER = "order" +ATTR_VIEW = "view" +ATTR_VISIBLE = "visible" +ATTR_ALL = "all" -SERVICE_SET_VISIBILITY = 'set_visibility' -SERVICE_SET = 'set' -SERVICE_REMOVE = 'remove' +SERVICE_SET_VISIBILITY = "set_visibility" +SERVICE_SET = "set" +SERVICE_REMOVE = "remove" -CONTROL_TYPES = vol.In(['hidden', None]) +CONTROL_TYPES = vol.In(["hidden", None]) -SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_VISIBLE): cv.boolean -}) +SET_VISIBILITY_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_VISIBLE): cv.boolean} +) RELOAD_SERVICE_SCHEMA = vol.Schema({}) -SET_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_OBJECT_ID): cv.slug, - vol.Optional(ATTR_NAME): cv.string, - vol.Optional(ATTR_VIEW): cv.boolean, - vol.Optional(ATTR_ICON): cv.string, - vol.Optional(ATTR_CONTROL): CONTROL_TYPES, - vol.Optional(ATTR_VISIBLE): cv.boolean, - vol.Optional(ATTR_ALL): cv.boolean, - vol.Exclusive(ATTR_ENTITIES, 'entities'): cv.entity_ids, - vol.Exclusive(ATTR_ADD_ENTITIES, 'entities'): cv.entity_ids, -}) +SET_SERVICE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_OBJECT_ID): cv.slug, + vol.Optional(ATTR_NAME): cv.string, + vol.Optional(ATTR_VIEW): cv.boolean, + vol.Optional(ATTR_ICON): cv.string, + vol.Optional(ATTR_CONTROL): CONTROL_TYPES, + vol.Optional(ATTR_VISIBLE): cv.boolean, + vol.Optional(ATTR_ALL): cv.boolean, + vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids, + vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids, + } +) -REMOVE_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_OBJECT_ID): cv.slug, -}) +REMOVE_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_OBJECT_ID): cv.slug}) _LOGGER = logging.getLogger(__name__) @@ -79,23 +94,30 @@ def _conf_preprocess(value): return value -GROUP_SCHEMA = vol.Schema({ - vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None), - CONF_VIEW: cv.boolean, - CONF_NAME: cv.string, - CONF_ICON: cv.icon, - CONF_CONTROL: CONTROL_TYPES, - CONF_ALL: cv.boolean, -}) +GROUP_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None), + CONF_VIEW: cv.boolean, + CONF_NAME: cv.string, + CONF_ICON: cv.icon, + CONF_CONTROL: CONTROL_TYPES, + CONF_ALL: cv.boolean, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({cv.match_all: vol.All(_conf_preprocess, GROUP_SCHEMA)}) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({cv.match_all: vol.All(_conf_preprocess, GROUP_SCHEMA)})}, + extra=vol.ALLOW_EXTRA, +) # List of ON/OFF state tuples for groupable states -_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME), - (STATE_OPEN, STATE_CLOSED), (STATE_LOCKED, STATE_UNLOCKED), - (STATE_PROBLEM, STATE_OK)] +_GROUP_TYPES = [ + (STATE_ON, STATE_OFF), + (STATE_HOME, STATE_NOT_HOME), + (STATE_OPEN, STATE_CLOSED), + (STATE_LOCKED, STATE_UNLOCKED), + (STATE_PROBLEM, STATE_OK), +] def _get_group_on_off(state): @@ -144,9 +166,10 @@ def expand_entity_ids(hass, entity_ids): child_entities = list(child_entities) child_entities.remove(entity_id) found_ids.extend( - ent_id for ent_id - in expand_entity_ids(hass, child_entities) - if ent_id not in found_ids) + ent_id + for ent_id in expand_entity_ids(hass, child_entities) + if ent_id not in found_ids + ) else: if entity_id not in found_ids: @@ -174,10 +197,9 @@ def get_entity_ids(hass, entity_id, domain_filter=None): if not domain_filter: return entity_ids - domain_filter = domain_filter.lower() + '.' + domain_filter = domain_filter.lower() + "." - return [ent_id for ent_id in entity_ids - if ent_id.startswith(domain_filter)] + return [ent_id for ent_id in entity_ids if ent_id.startswith(domain_filter)] async def async_setup(hass, config): @@ -201,8 +223,8 @@ async def async_setup(hass, config): await component.async_add_entities(auto) hass.services.async_register( - DOMAIN, SERVICE_RELOAD, reload_service_handler, - schema=RELOAD_SERVICE_SCHEMA) + DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA + ) service_lock = asyncio.Lock() @@ -219,26 +241,31 @@ async def async_setup(hass, config): # new group if service.service == SERVICE_SET and group is None: - entity_ids = service.data.get(ATTR_ENTITIES) or \ - service.data.get(ATTR_ADD_ENTITIES) or None + entity_ids = ( + service.data.get(ATTR_ENTITIES) + or service.data.get(ATTR_ADD_ENTITIES) + or None + ) - extra_arg = {attr: service.data[attr] for attr in ( - ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL - ) if service.data.get(attr) is not None} + extra_arg = { + attr: service.data[attr] + for attr in (ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL) + if service.data.get(attr) is not None + } await Group.async_create_group( - hass, service.data.get(ATTR_NAME, object_id), + hass, + service.data.get(ATTR_NAME, object_id), object_id=object_id, entity_ids=entity_ids, user_defined=False, mode=service.data.get(ATTR_ALL), - **extra_arg + **extra_arg, ) return if group is None: - _LOGGER.warning("%s:Group '%s' doesn't exist!", - service.service, object_id) + _LOGGER.warning("%s:Group '%s' doesn't exist!", service.service, object_id) return # update group @@ -288,12 +315,12 @@ async def async_setup(hass, config): await component.async_remove_entity(entity_id) hass.services.async_register( - DOMAIN, SERVICE_SET, locked_service_handler, - schema=SET_SERVICE_SCHEMA) + DOMAIN, SERVICE_SET, locked_service_handler, schema=SET_SERVICE_SCHEMA + ) hass.services.async_register( - DOMAIN, SERVICE_REMOVE, groups_service_handler, - schema=REMOVE_SERVICE_SCHEMA) + DOMAIN, SERVICE_REMOVE, groups_service_handler, schema=REMOVE_SERVICE_SCHEMA + ) async def visibility_service_handler(service): """Change visibility of a group.""" @@ -301,7 +328,8 @@ async def async_setup(hass, config): tasks = [] for group in await component.async_extract_from_service( - service, expand_group=False): + service, expand_group=False + ): group.visible = visible tasks.append(group.async_update_ha_state()) @@ -309,8 +337,11 @@ async def async_setup(hass, config): await asyncio.wait(tasks) hass.services.async_register( - DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, - schema=SET_VISIBILITY_SERVICE_SCHEMA) + DOMAIN, + SERVICE_SET_VISIBILITY, + visibility_service_handler, + schema=SET_VISIBILITY_SERVICE_SCHEMA, + ) return True @@ -328,16 +359,33 @@ async def _async_process_config(hass, config, component): # Don't create tasks and await them all. The order is important as # groups get a number based on creation order. await Group.async_create_group( - hass, name, entity_ids, icon=icon, view=view, - control=control, object_id=object_id, mode=mode) + hass, + name, + entity_ids, + icon=icon, + view=view, + control=control, + object_id=object_id, + mode=mode, + ) class Group(Entity): """Track a group of entity ids.""" - def __init__(self, hass, name, order=None, visible=True, icon=None, - view=False, control=None, user_defined=True, entity_ids=None, - mode=None): + def __init__( + self, + hass, + name, + order=None, + visible=True, + icon=None, + view=False, + control=None, + user_defined=True, + entity_ids=None, + mode=None, + ): """Initialize a group. This Object has factory function for creation. @@ -364,41 +412,74 @@ class Group(Entity): self._async_unsub_state_changed = None @staticmethod - def create_group(hass, name, entity_ids=None, user_defined=True, - visible=True, icon=None, view=False, control=None, - object_id=None, mode=None): + def create_group( + hass, + name, + entity_ids=None, + user_defined=True, + visible=True, + icon=None, + view=False, + control=None, + object_id=None, + mode=None, + ): """Initialize a group.""" return run_coroutine_threadsafe( Group.async_create_group( - hass, name, entity_ids, user_defined, visible, icon, view, - control, object_id, mode), - hass.loop).result() + hass, + name, + entity_ids, + user_defined, + visible, + icon, + view, + control, + object_id, + mode, + ), + hass.loop, + ).result() @staticmethod - async def async_create_group(hass, name, entity_ids=None, - user_defined=True, visible=True, icon=None, - view=False, control=None, object_id=None, - mode=None): + async def async_create_group( + hass, + name, + entity_ids=None, + user_defined=True, + visible=True, + icon=None, + view=False, + control=None, + object_id=None, + mode=None, + ): """Initialize a group. This method must be run in the event loop. """ group = Group( - hass, name, + hass, + name, order=len(hass.states.async_entity_ids(DOMAIN)), - visible=visible, icon=icon, view=view, control=control, - user_defined=user_defined, entity_ids=entity_ids, mode=mode + visible=visible, + icon=icon, + view=view, + control=control, + user_defined=user_defined, + entity_ids=entity_ids, + mode=mode, ) group.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, object_id or name, hass=hass) + ENTITY_ID_FORMAT, object_id or name, hass=hass + ) # If called before the platform async_setup is called (test cases) component = hass.data.get(DOMAIN) if component is None: - component = hass.data[DOMAIN] = \ - EntityComponent(_LOGGER, DOMAIN, hass) + component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) await component.async_add_entities([group], True) @@ -444,10 +525,7 @@ class Group(Entity): @property def state_attributes(self): """Return the state attributes for the group.""" - data = { - ATTR_ENTITY_ID: self.tracking, - ATTR_ORDER: self._order, - } + data = {ATTR_ENTITY_ID: self.tracking, ATTR_ORDER: self._order} if not self.user_defined: data[ATTR_AUTO] = True if self.view: @@ -515,8 +593,7 @@ class Group(Entity): self._async_unsub_state_changed() self._async_unsub_state_changed = None - async def _async_state_changed_listener(self, entity_id, old_state, - new_state): + async def _async_state_changed_listener(self, entity_id, old_state, new_state): """Respond to a member state changing. This method must be run in the event loop. @@ -562,8 +639,7 @@ class Group(Entity): states = self._tracking_states for state in states: - gr_on, gr_off = \ - _get_group_on_off(state.state) + gr_on, gr_off = _get_group_on_off(state.state) if gr_on is not None: break else: @@ -577,11 +653,11 @@ class Group(Entity): return # pylint: disable=too-many-boolean-expressions - if tr_state is None or ((gr_state == gr_on and - tr_state.state == gr_off) or - (gr_state == gr_off and - tr_state.state == gr_on) or - tr_state.state not in (gr_on, gr_off)): + if tr_state is None or ( + (gr_state == gr_on and tr_state.state == gr_off) + or (gr_state == gr_off and tr_state.state == gr_on) + or tr_state.state not in (gr_on, gr_off) + ): if states is None: states = self._tracking_states @@ -593,14 +669,17 @@ class Group(Entity): elif tr_state.state in (gr_on, gr_off): self._state = tr_state.state - if tr_state is None or self._assumed_state and \ - not tr_state.attributes.get(ATTR_ASSUMED_STATE): + if ( + tr_state is None + or self._assumed_state + and not tr_state.attributes.get(ATTR_ASSUMED_STATE) + ): if states is None: states = self._tracking_states self._assumed_state = self.mode( - state.attributes.get(ATTR_ASSUMED_STATE) for state - in states) + state.attributes.get(ATTR_ASSUMED_STATE) for state in states + ) elif tr_state.attributes.get(ATTR_ASSUMED_STATE): self._assumed_state = True diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 385d20949d6..faa4ddfc87d 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -4,41 +4,63 @@ import logging import voluptuous as vol from homeassistant.const import ( - ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, - CONF_NAME, STATE_CLOSED) + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_NAME, + STATE_CLOSED, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.components.cover import ( - ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, ATTR_POSITION, - ATTR_TILT_POSITION, DOMAIN, PLATFORM_SCHEMA, SERVICE_CLOSE_COVER, - SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, - SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, - SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, SUPPORT_CLOSE, - SUPPORT_CLOSE_TILT, SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, - SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, SUPPORT_STOP_TILT, CoverDevice) + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, + DOMAIN, + PLATFORM_SCHEMA, + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, + SUPPORT_STOP_TILT, + CoverDevice, +) _LOGGER = logging.getLogger(__name__) -KEY_OPEN_CLOSE = 'open_close' -KEY_STOP = 'stop' -KEY_POSITION = 'position' +KEY_OPEN_CLOSE = "open_close" +KEY_STOP = "stop" +KEY_POSITION = "position" -DEFAULT_NAME = 'Cover Group' +DEFAULT_NAME = "Cover Group" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Group Cover platform.""" - async_add_entities( - [CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])]) + async_add_entities([CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])]) class CoverGroup(CoverDevice): @@ -54,14 +76,13 @@ class CoverGroup(CoverDevice): self._assumed_state = True self._entities = entities - self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), - KEY_POSITION: set()} - self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), - KEY_POSITION: set()} + self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), KEY_POSITION: set()} + self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), KEY_POSITION: set()} @callback - def update_supported_features(self, entity_id, old_state, new_state, - update_state=True): + def update_supported_features( + self, entity_id, old_state, new_state, update_state=True + ): """Update dictionaries with supported features.""" if not new_state: for values in self._covers.values(): @@ -107,10 +128,12 @@ class CoverGroup(CoverDevice): """Register listeners.""" for entity_id in self._entities: new_state = self.hass.states.get(entity_id) - self.update_supported_features(entity_id, None, new_state, - update_state=False) - async_track_state_change(self.hass, self._entities, - self.update_supported_features) + self.update_supported_features( + entity_id, None, new_state, update_state=False + ) + async_track_state_change( + self.hass, self._entities, self.update_supported_features + ) await self.async_update() @property @@ -152,51 +175,63 @@ class CoverGroup(CoverDevice): """Move the covers up.""" data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} await self.hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER, data, blocking=True) + DOMAIN, SERVICE_OPEN_COVER, data, blocking=True + ) async def async_close_cover(self, **kwargs): """Move the covers down.""" data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} await self.hass.services.async_call( - DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True) + DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True + ) async def async_stop_cover(self, **kwargs): """Fire the stop action.""" data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]} await self.hass.services.async_call( - DOMAIN, SERVICE_STOP_COVER, data, blocking=True) + DOMAIN, SERVICE_STOP_COVER, data, blocking=True + ) async def async_set_cover_position(self, **kwargs): """Set covers position.""" - data = {ATTR_ENTITY_ID: self._covers[KEY_POSITION], - ATTR_POSITION: kwargs[ATTR_POSITION]} + data = { + ATTR_ENTITY_ID: self._covers[KEY_POSITION], + ATTR_POSITION: kwargs[ATTR_POSITION], + } await self.hass.services.async_call( - DOMAIN, SERVICE_SET_COVER_POSITION, data, blocking=True) + DOMAIN, SERVICE_SET_COVER_POSITION, data, blocking=True + ) async def async_open_cover_tilt(self, **kwargs): """Tilt covers open.""" data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} await self.hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True) + DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True + ) async def async_close_cover_tilt(self, **kwargs): """Tilt covers closed.""" data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} await self.hass.services.async_call( - DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True) + DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True + ) async def async_stop_cover_tilt(self, **kwargs): """Stop cover tilt.""" data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]} await self.hass.services.async_call( - DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True) + DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True + ) async def async_set_cover_tilt_position(self, **kwargs): """Set tilt position.""" - data = {ATTR_ENTITY_ID: self._tilts[KEY_POSITION], - ATTR_TILT_POSITION: kwargs[ATTR_TILT_POSITION]} + data = { + ATTR_ENTITY_ID: self._tilts[KEY_POSITION], + ATTR_TILT_POSITION: kwargs[ATTR_TILT_POSITION], + } await self.hass.services.async_call( - DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data, blocking=True) + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data, blocking=True + ) async def async_update(self): """Update state and attributes.""" @@ -244,18 +279,18 @@ class CoverGroup(CoverDevice): self._tilt_position = position supported_features = 0 - supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE \ - if self._covers[KEY_OPEN_CLOSE] else 0 - supported_features |= SUPPORT_STOP \ - if self._covers[KEY_STOP] else 0 - supported_features |= SUPPORT_SET_POSITION \ - if self._covers[KEY_POSITION] else 0 - supported_features |= SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT \ - if self._tilts[KEY_OPEN_CLOSE] else 0 - supported_features |= SUPPORT_STOP_TILT \ - if self._tilts[KEY_STOP] else 0 - supported_features |= SUPPORT_SET_TILT_POSITION \ - if self._tilts[KEY_POSITION] else 0 + supported_features |= ( + SUPPORT_OPEN | SUPPORT_CLOSE if self._covers[KEY_OPEN_CLOSE] else 0 + ) + supported_features |= SUPPORT_STOP if self._covers[KEY_STOP] else 0 + supported_features |= SUPPORT_SET_POSITION if self._covers[KEY_POSITION] else 0 + supported_features |= ( + SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT if self._tilts[KEY_OPEN_CLOSE] else 0 + ) + supported_features |= SUPPORT_STOP_TILT if self._tilts[KEY_STOP] else 0 + supported_features |= ( + SUPPORT_SET_TILT_POSITION if self._tilts[KEY_POSITION] else 0 + ) self._supported_features = supported_features if not self._assumed_state: diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 170e93398a1..f0d29d923c8 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -8,40 +8,66 @@ import voluptuous as vol from homeassistant.components import light from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, CONF_NAME, - STATE_ON, STATE_UNAVAILABLE) + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_NAME, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_EFFECT_LIST, - ATTR_FLASH, ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, - ATTR_TRANSITION, ATTR_WHITE_VALUE, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_EFFECT_LIST, + ATTR_FLASH, + ATTR_HS_COLOR, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, + ATTR_TRANSITION, + ATTR_WHITE_VALUE, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_FLASH, + SUPPORT_TRANSITION, + SUPPORT_WHITE_VALUE, +) _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Light Group' +DEFAULT_NAME = "Light Group" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ENTITIES): cv.entities_domain(light.DOMAIN) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ENTITIES): cv.entities_domain(light.DOMAIN), + } +) -SUPPORT_GROUP_LIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT - | SUPPORT_FLASH | SUPPORT_COLOR | SUPPORT_TRANSITION - | SUPPORT_WHITE_VALUE) +SUPPORT_GROUP_LIGHT = ( + SUPPORT_BRIGHTNESS + | SUPPORT_COLOR_TEMP + | SUPPORT_EFFECT + | SUPPORT_FLASH + | SUPPORT_COLOR + | SUPPORT_TRANSITION + | SUPPORT_WHITE_VALUE +) -async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, - async_add_entities, - discovery_info=None) -> None: +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Initialize light.group platform.""" - async_add_entities([LightGroup(config.get(CONF_NAME), - config[CONF_ENTITIES])]) + async_add_entities([LightGroup(config.get(CONF_NAME), config[CONF_ENTITIES])]) class LightGroup(light.Light): @@ -66,14 +92,17 @@ class LightGroup(light.Light): async def async_added_to_hass(self) -> None: """Register callbacks.""" + @callback - def async_state_changed_listener(entity_id: str, old_state: State, - new_state: State): + def async_state_changed_listener( + entity_id: str, old_state: State, new_state: State + ): """Handle child updates.""" self.async_schedule_update_ha_state(True) self._async_unsub_state_changed = async_track_state_change( - self.hass, self._entity_ids, async_state_changed_listener) + self.hass, self._entity_ids, async_state_changed_listener + ) await self.async_update() async def async_will_remove_from_hass(self): @@ -173,7 +202,8 @@ class LightGroup(light.Light): data[ATTR_FLASH] = kwargs[ATTR_FLASH] await self.hass.services.async_call( - light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True) + light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + ) async def async_turn_off(self, **kwargs): """Forward the turn_off command to all lights in the light group.""" @@ -183,7 +213,8 @@ class LightGroup(light.Light): data[ATTR_TRANSITION] = kwargs[ATTR_TRANSITION] await self.hass.services.async_call( - light.DOMAIN, light.SERVICE_TURN_OFF, data, blocking=True) + light.DOMAIN, light.SERVICE_TURN_OFF, data, blocking=True + ) async def async_update(self): """Query all members and determine the light group state.""" @@ -192,25 +223,24 @@ class LightGroup(light.Light): on_states = [state for state in states if state.state == STATE_ON] self._is_on = len(on_states) > 0 - self._available = any(state.state != STATE_UNAVAILABLE - for state in states) + self._available = any(state.state != STATE_UNAVAILABLE for state in states) self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS) - self._hs_color = _reduce_attribute( - on_states, ATTR_HS_COLOR, reduce=_mean_tuple) + self._hs_color = _reduce_attribute(on_states, ATTR_HS_COLOR, reduce=_mean_tuple) self._white_value = _reduce_attribute(on_states, ATTR_WHITE_VALUE) self._color_temp = _reduce_attribute(on_states, ATTR_COLOR_TEMP) self._min_mireds = _reduce_attribute( - states, ATTR_MIN_MIREDS, default=154, reduce=min) + states, ATTR_MIN_MIREDS, default=154, reduce=min + ) self._max_mireds = _reduce_attribute( - states, ATTR_MAX_MIREDS, default=500, reduce=max) + states, ATTR_MAX_MIREDS, default=500, reduce=max + ) self._effect_list = None - all_effect_lists = list( - _find_state_attributes(states, ATTR_EFFECT_LIST)) + all_effect_lists = list(_find_state_attributes(states, ATTR_EFFECT_LIST)) if all_effect_lists: # Merge all effects from all effect_lists with a union merge. self._effect_list = list(set().union(*all_effect_lists)) @@ -232,8 +262,7 @@ class LightGroup(light.Light): self._supported_features &= SUPPORT_GROUP_LIGHT -def _find_state_attributes(states: List[State], - key: str) -> Iterator[Any]: +def _find_state_attributes(states: List[State], key: str) -> Iterator[Any]: """Find attributes with matching key from states.""" for state in states: value = state.attributes.get(key) @@ -251,10 +280,12 @@ def _mean_tuple(*args): return tuple(sum(l) / len(l) for l in zip(*args)) -def _reduce_attribute(states: List[State], - key: str, - default: Optional[Any] = None, - reduce: Callable[..., Any] = _mean_int) -> Any: +def _reduce_attribute( + states: List[State], + key: str, + default: Optional[Any] = None, + reduce: Callable[..., Any] = _mean_int, +) -> Any: """Find the first attribute matching key from states. If none are found, return default. diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index e13499878e9..3d3c644fea9 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -10,18 +10,25 @@ from homeassistant.const import ATTR_SERVICE import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_DATA, ATTR_MESSAGE, DOMAIN, PLATFORM_SCHEMA, BaseNotificationService) + ATTR_DATA, + ATTR_MESSAGE, + DOMAIN, + PLATFORM_SCHEMA, + BaseNotificationService, +) _LOGGER = logging.getLogger(__name__) -CONF_SERVICES = 'services' +CONF_SERVICES = "services" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SERVICES): vol.All(cv.ensure_list, [{ - vol.Required(ATTR_SERVICE): cv.slug, - vol.Optional(ATTR_DATA): dict, - }]) -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_SERVICES): vol.All( + cv.ensure_list, + [{vol.Required(ATTR_SERVICE): cv.slug, vol.Optional(ATTR_DATA): dict}], + ) + } +) def update(input_dict, update_source): @@ -61,8 +68,11 @@ class GroupNotifyPlatform(BaseNotificationService): sending_payload = deepcopy(payload.copy()) if entity.get(ATTR_DATA) is not None: update(sending_payload, entity.get(ATTR_DATA)) - tasks.append(self.hass.services.async_call( - DOMAIN, entity.get(ATTR_SERVICE), sending_payload)) + tasks.append( + self.hass.services.async_call( + DOMAIN, entity.get(ATTR_SERVICE), sending_payload + ) + ) if tasks: await asyncio.wait(tasks) diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py index 1cf1793e6f6..f2170c4df16 100644 --- a/homeassistant/components/group/reproduce_state.py +++ b/homeassistant/components/group/reproduce_state.py @@ -7,22 +7,25 @@ from homeassistant.loader import bind_hass @bind_hass -async def async_reproduce_states(hass: HomeAssistantType, - states: Iterable[State], - context: Optional[Context] = None) -> None: +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: """Reproduce component states.""" from . import get_entity_ids from homeassistant.helpers.state import async_reproduce_state + states_copy = [] for state in states: members = get_entity_ids(hass, state.entity_id) for member in members: states_copy.append( - State(member, - state.state, - state.attributes, - last_changed=state.last_changed, - last_updated=state.last_updated, - context=state.context)) - await async_reproduce_state(hass, states_copy, blocking=True, - context=context) + State( + member, + state.state, + state.attributes, + last_changed=state.last_changed, + last_updated=state.last_updated, + context=state.context, + ) + ) + await async_reproduce_state(hass, states_copy, blocking=True, context=context) diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index f7404010513..a213587bc0e 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -3,32 +3,41 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_SET) + MEDIA_TYPE_MUSIC, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_VOLUME_SET, +) from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_PIPELINE = 'pipeline' +CONF_PIPELINE = "pipeline" -DOMAIN = 'gstreamer' +DOMAIN = "gstreamer" -SUPPORT_GSTREAMER = SUPPORT_VOLUME_SET | SUPPORT_PLAY | SUPPORT_PAUSE |\ - SUPPORT_PLAY_MEDIA | SUPPORT_NEXT_TRACK +SUPPORT_GSTREAMER = ( + SUPPORT_VOLUME_SET + | SUPPORT_PLAY + | SUPPORT_PAUSE + | SUPPORT_PLAY_MEDIA + | SUPPORT_NEXT_TRACK +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_PIPELINE): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string} +) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gstreamer platform.""" from gsp import GstreamerPlayer + name = config.get(CONF_NAME) pipeline = config.get(CONF_PIPELINE) player = GstreamerPlayer(pipeline) @@ -73,7 +82,7 @@ class GstreamerDevice(MediaPlayerDevice): def play_media(self, media_type, media_id, **kwargs): """Play media.""" if media_type != MEDIA_TYPE_MUSIC: - _LOGGER.error('invalid media type') + _LOGGER.error("invalid media type") return self._player.queue(media_id) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 0a9301f8c33..b5c1000681d 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -9,8 +9,12 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET, DEVICE_CLASS_TIMESTAMP, - STATE_UNKNOWN) + ATTR_ATTRIBUTION, + CONF_NAME, + CONF_OFFSET, + DEVICE_CLASS_TIMESTAMP, + STATE_UNKNOWN, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -19,111 +23,104 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -ATTR_ARRIVAL = 'arrival' -ATTR_BICYCLE = 'trip_bikes_allowed_state' -ATTR_DAY = 'day' -ATTR_FIRST = 'first' -ATTR_DROP_OFF_DESTINATION = 'destination_stop_drop_off_type_state' -ATTR_DROP_OFF_ORIGIN = 'origin_stop_drop_off_type_state' -ATTR_INFO = 'info' +ATTR_ARRIVAL = "arrival" +ATTR_BICYCLE = "trip_bikes_allowed_state" +ATTR_DAY = "day" +ATTR_FIRST = "first" +ATTR_DROP_OFF_DESTINATION = "destination_stop_drop_off_type_state" +ATTR_DROP_OFF_ORIGIN = "origin_stop_drop_off_type_state" +ATTR_INFO = "info" ATTR_OFFSET = CONF_OFFSET -ATTR_LAST = 'last' -ATTR_LOCATION_DESTINATION = 'destination_station_location_type_name' -ATTR_LOCATION_ORIGIN = 'origin_station_location_type_name' -ATTR_PICKUP_DESTINATION = 'destination_stop_pickup_type_state' -ATTR_PICKUP_ORIGIN = 'origin_stop_pickup_type_state' -ATTR_ROUTE_TYPE = 'route_type_name' -ATTR_TIMEPOINT_DESTINATION = 'destination_stop_timepoint_exact' -ATTR_TIMEPOINT_ORIGIN = 'origin_stop_timepoint_exact' -ATTR_WHEELCHAIR = 'trip_wheelchair_access_available' -ATTR_WHEELCHAIR_DESTINATION = \ - 'destination_station_wheelchair_boarding_available' -ATTR_WHEELCHAIR_ORIGIN = 'origin_station_wheelchair_boarding_available' +ATTR_LAST = "last" +ATTR_LOCATION_DESTINATION = "destination_station_location_type_name" +ATTR_LOCATION_ORIGIN = "origin_station_location_type_name" +ATTR_PICKUP_DESTINATION = "destination_stop_pickup_type_state" +ATTR_PICKUP_ORIGIN = "origin_stop_pickup_type_state" +ATTR_ROUTE_TYPE = "route_type_name" +ATTR_TIMEPOINT_DESTINATION = "destination_stop_timepoint_exact" +ATTR_TIMEPOINT_ORIGIN = "origin_stop_timepoint_exact" +ATTR_WHEELCHAIR = "trip_wheelchair_access_available" +ATTR_WHEELCHAIR_DESTINATION = "destination_station_wheelchair_boarding_available" +ATTR_WHEELCHAIR_ORIGIN = "origin_station_wheelchair_boarding_available" -CONF_DATA = 'data' -CONF_DESTINATION = 'destination' -CONF_ORIGIN = 'origin' -CONF_TOMORROW = 'include_tomorrow' +CONF_DATA = "data" +CONF_DESTINATION = "destination" +CONF_ORIGIN = "origin" +CONF_TOMORROW = "include_tomorrow" -DEFAULT_NAME = 'GTFS Sensor' -DEFAULT_PATH = 'gtfs' +DEFAULT_NAME = "GTFS Sensor" +DEFAULT_PATH = "gtfs" BICYCLE_ALLOWED_DEFAULT = STATE_UNKNOWN -BICYCLE_ALLOWED_OPTIONS = { - 1: True, - 2: False, -} +BICYCLE_ALLOWED_OPTIONS = {1: True, 2: False} DROP_OFF_TYPE_DEFAULT = STATE_UNKNOWN DROP_OFF_TYPE_OPTIONS = { - 0: 'Regular', - 1: 'Not Available', - 2: 'Call Agency', - 3: 'Contact Driver', + 0: "Regular", + 1: "Not Available", + 2: "Call Agency", + 3: "Contact Driver", } -ICON = 'mdi:train' +ICON = "mdi:train" ICONS = { - 0: 'mdi:tram', - 1: 'mdi:subway', - 2: 'mdi:train', - 3: 'mdi:bus', - 4: 'mdi:ferry', - 5: 'mdi:train-variant', - 6: 'mdi:gondola', - 7: 'mdi:stairs', + 0: "mdi:tram", + 1: "mdi:subway", + 2: "mdi:train", + 3: "mdi:bus", + 4: "mdi:ferry", + 5: "mdi:train-variant", + 6: "mdi:gondola", + 7: "mdi:stairs", } -LOCATION_TYPE_DEFAULT = 'Stop' +LOCATION_TYPE_DEFAULT = "Stop" LOCATION_TYPE_OPTIONS = { - 0: 'Station', - 1: 'Stop', + 0: "Station", + 1: "Stop", 2: "Station Entrance/Exit", - 3: 'Other', + 3: "Other", } PICKUP_TYPE_DEFAULT = STATE_UNKNOWN PICKUP_TYPE_OPTIONS = { - 0: 'Regular', + 0: "Regular", 1: "None Available", 2: "Call Agency", 3: "Contact Driver", } ROUTE_TYPE_OPTIONS = { - 0: 'Tram', - 1: 'Subway', - 2: 'Rail', - 3: 'Bus', - 4: 'Ferry', + 0: "Tram", + 1: "Subway", + 2: "Rail", + 3: "Bus", + 4: "Ferry", 5: "Cable Tram", 6: "Aerial Lift", - 7: 'Funicular', + 7: "Funicular", } TIMEPOINT_DEFAULT = True -TIMEPOINT_OPTIONS = { - 0: False, - 1: True, -} +TIMEPOINT_OPTIONS = {0: False, 1: True} WHEELCHAIR_ACCESS_DEFAULT = STATE_UNKNOWN -WHEELCHAIR_ACCESS_OPTIONS = { - 1: True, - 2: False, -} +WHEELCHAIR_ACCESS_OPTIONS = {1: True, 2: False} WHEELCHAIR_BOARDING_DEFAULT = STATE_UNKNOWN -WHEELCHAIR_BOARDING_OPTIONS = { - 1: True, - 2: False, -} +WHEELCHAIR_BOARDING_OPTIONS = {1: True, 2: False} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # type: ignore - vol.Required(CONF_ORIGIN): cv.string, - vol.Required(CONF_DESTINATION): cv.string, - vol.Required(CONF_DATA): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OFFSET, default=0): cv.time_period, - vol.Optional(CONF_TOMORROW, default=False): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { # type: ignore + vol.Required(CONF_ORIGIN): cv.string, + vol.Required(CONF_DESTINATION): cv.string, + vol.Required(CONF_DATA): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_OFFSET, default=0): cv.time_period, + vol.Optional(CONF_TOMORROW, default=False): cv.boolean, + } +) -def get_next_departure(schedule: Any, start_station_id: Any, - end_station_id: Any, offset: cv.time_period, - include_tomorrow: bool = False) -> dict: +def get_next_departure( + schedule: Any, + start_station_id: Any, + end_station_id: Any, + offset: cv.time_period, + include_tomorrow: bool = False, +) -> dict: """Get the next departure for the given schedule.""" now = datetime.datetime.now() + offset now_date = now.strftime(dt_util.DATE_STR_FORMAT) @@ -138,10 +135,10 @@ def get_next_departure(schedule: Any, start_station_id: Any, # up to an overkill maximum in case of a departure every minute for those # days. limit = 24 * 60 * 60 * 2 - tomorrow_select = tomorrow_where = tomorrow_order = '' + tomorrow_select = tomorrow_where = tomorrow_order = "" if include_tomorrow: limit = int(limit / 2 * 3) - tomorrow_name = tomorrow.strftime('%A').lower() + tomorrow_name = tomorrow.strftime("%A").lower() tomorrow_select = "calendar.{} AS tomorrow,".format(tomorrow_name) tomorrow_where = "OR calendar.{} = 1".format(tomorrow_name) tomorrow_order = "calendar.{} DESC,".format(tomorrow_name) @@ -195,74 +192,67 @@ def get_next_departure(schedule: Any, start_station_id: Any, {tomorrow_order} origin_stop_time.departure_time LIMIT :limit - """.format(yesterday_name=yesterday.strftime('%A').lower(), - today_name=now.strftime('%A').lower(), - tomorrow_select=tomorrow_select, - tomorrow_where=tomorrow_where, - tomorrow_order=tomorrow_order) - result = schedule.engine.execute(text(sql_query), - origin_station_id=start_station_id, - end_station_id=end_station_id, - today=now_date, - limit=limit) + """.format( + yesterday_name=yesterday.strftime("%A").lower(), + today_name=now.strftime("%A").lower(), + tomorrow_select=tomorrow_select, + tomorrow_where=tomorrow_where, + tomorrow_order=tomorrow_order, + ) + result = schedule.engine.execute( + text(sql_query), + origin_station_id=start_station_id, + end_station_id=end_station_id, + today=now_date, + limit=limit, + ) # Create lookup timetable for today and possibly tomorrow, taking into # account any departures from yesterday scheduled after midnight, # as long as all departures are within the calendar date range. timetable = {} yesterday_start = today_start = tomorrow_start = None - yesterday_last = today_last = '' + yesterday_last = today_last = "" for row in result: - if row['yesterday'] == 1 and yesterday_date >= row['start_date']: - extras = { - 'day': 'yesterday', - 'first': None, - 'last': False, - } + if row["yesterday"] == 1 and yesterday_date >= row["start_date"]: + extras = {"day": "yesterday", "first": None, "last": False} if yesterday_start is None: - yesterday_start = row['origin_depart_date'] - if yesterday_start != row['origin_depart_date']: - idx = '{} {}'.format(now_date, - row['origin_depart_time']) + yesterday_start = row["origin_depart_date"] + if yesterday_start != row["origin_depart_date"]: + idx = "{} {}".format(now_date, row["origin_depart_time"]) timetable[idx] = {**row, **extras} yesterday_last = idx - if row['today'] == 1: - extras = { - 'day': 'today', - 'first': False, - 'last': False, - } + if row["today"] == 1: + extras = {"day": "today", "first": False, "last": False} if today_start is None: - today_start = row['origin_depart_date'] - extras['first'] = True - if today_start == row['origin_depart_date']: + today_start = row["origin_depart_date"] + extras["first"] = True + if today_start == row["origin_depart_date"]: idx_prefix = now_date else: idx_prefix = tomorrow_date - idx = '{} {}'.format(idx_prefix, row['origin_depart_time']) + idx = "{} {}".format(idx_prefix, row["origin_depart_time"]) timetable[idx] = {**row, **extras} today_last = idx - if 'tomorrow' in row and row['tomorrow'] == 1 and tomorrow_date <= \ - row['end_date']: - extras = { - 'day': 'tomorrow', - 'first': False, - 'last': None, - } + if ( + "tomorrow" in row + and row["tomorrow"] == 1 + and tomorrow_date <= row["end_date"] + ): + extras = {"day": "tomorrow", "first": False, "last": None} if tomorrow_start is None: - tomorrow_start = row['origin_depart_date'] - extras['first'] = True - if tomorrow_start == row['origin_depart_date']: - idx = '{} {}'.format(tomorrow_date, - row['origin_depart_time']) + tomorrow_start = row["origin_depart_date"] + extras["first"] = True + if tomorrow_start == row["origin_depart_date"]: + idx = "{} {}".format(tomorrow_date, row["origin_depart_time"]) timetable[idx] = {**row, **extras} # Flag last departures. for idx in filter(None, [yesterday_last, today_last]): - timetable[idx]['last'] = True + timetable[idx]["last"] = True _LOGGER.debug("Timetable: %s", sorted(timetable.keys())) @@ -270,8 +260,9 @@ def get_next_departure(schedule: Any, start_station_id: Any, for key in sorted(timetable.keys()): if dt_util.parse_datetime(key) > now: item = timetable[key] - _LOGGER.debug("Departure found for station %s @ %s -> %s", - start_station_id, key, item) + _LOGGER.debug( + "Departure found for station %s @ %s -> %s", start_station_id, key, item + ) break if item == {}: @@ -280,69 +271,72 @@ def get_next_departure(schedule: Any, start_station_id: Any, # Format arrival and departure dates and times, accounting for the # possibility of times crossing over midnight. origin_arrival = now - if item['origin_arrival_time'] > item['origin_depart_time']: + if item["origin_arrival_time"] > item["origin_depart_time"]: origin_arrival -= datetime.timedelta(days=1) - origin_arrival_time = '{} {}'.format( - origin_arrival.strftime(dt_util.DATE_STR_FORMAT), - item['origin_arrival_time']) + origin_arrival_time = "{} {}".format( + origin_arrival.strftime(dt_util.DATE_STR_FORMAT), item["origin_arrival_time"] + ) - origin_depart_time = '{} {}'.format(now_date, item['origin_depart_time']) + origin_depart_time = "{} {}".format(now_date, item["origin_depart_time"]) dest_arrival = now - if item['dest_arrival_time'] < item['origin_depart_time']: + if item["dest_arrival_time"] < item["origin_depart_time"]: dest_arrival += datetime.timedelta(days=1) - dest_arrival_time = '{} {}'.format( - dest_arrival.strftime(dt_util.DATE_STR_FORMAT), - item['dest_arrival_time']) + dest_arrival_time = "{} {}".format( + dest_arrival.strftime(dt_util.DATE_STR_FORMAT), item["dest_arrival_time"] + ) dest_depart = dest_arrival - if item['dest_depart_time'] < item['dest_arrival_time']: + if item["dest_depart_time"] < item["dest_arrival_time"]: dest_depart += datetime.timedelta(days=1) - dest_depart_time = '{} {}'.format( - dest_depart.strftime(dt_util.DATE_STR_FORMAT), - item['dest_depart_time']) + dest_depart_time = "{} {}".format( + dest_depart.strftime(dt_util.DATE_STR_FORMAT), item["dest_depart_time"] + ) depart_time = dt_util.parse_datetime(origin_depart_time) arrival_time = dt_util.parse_datetime(dest_arrival_time) origin_stop_time = { - 'Arrival Time': origin_arrival_time, - 'Departure Time': origin_depart_time, - 'Drop Off Type': item['origin_drop_off_type'], - 'Pickup Type': item['origin_pickup_type'], - 'Shape Dist Traveled': item['origin_dist_traveled'], - 'Headsign': item['origin_stop_headsign'], - 'Sequence': item['origin_stop_sequence'], - 'Timepoint': item['origin_stop_timepoint'], + "Arrival Time": origin_arrival_time, + "Departure Time": origin_depart_time, + "Drop Off Type": item["origin_drop_off_type"], + "Pickup Type": item["origin_pickup_type"], + "Shape Dist Traveled": item["origin_dist_traveled"], + "Headsign": item["origin_stop_headsign"], + "Sequence": item["origin_stop_sequence"], + "Timepoint": item["origin_stop_timepoint"], } destination_stop_time = { - 'Arrival Time': dest_arrival_time, - 'Departure Time': dest_depart_time, - 'Drop Off Type': item['dest_drop_off_type'], - 'Pickup Type': item['dest_pickup_type'], - 'Shape Dist Traveled': item['dest_dist_traveled'], - 'Headsign': item['dest_stop_headsign'], - 'Sequence': item['dest_stop_sequence'], - 'Timepoint': item['dest_stop_timepoint'], + "Arrival Time": dest_arrival_time, + "Departure Time": dest_depart_time, + "Drop Off Type": item["dest_drop_off_type"], + "Pickup Type": item["dest_pickup_type"], + "Shape Dist Traveled": item["dest_dist_traveled"], + "Headsign": item["dest_stop_headsign"], + "Sequence": item["dest_stop_sequence"], + "Timepoint": item["dest_stop_timepoint"], } return { - 'trip_id': item['trip_id'], - 'route_id': item['route_id'], - 'day': item['day'], - 'first': item['first'], - 'last': item['last'], - 'departure_time': depart_time, - 'arrival_time': arrival_time, - 'origin_stop_time': origin_stop_time, - 'destination_stop_time': destination_stop_time, + "trip_id": item["trip_id"], + "route_id": item["route_id"], + "day": item["day"], + "first": item["first"], + "last": item["last"], + "departure_time": depart_time, + "arrival_time": arrival_time, + "origin_stop_time": origin_stop_time, + "destination_stop_time": destination_stop_time, } -def setup_platform(hass: HomeAssistantType, config: ConfigType, - add_entities: Callable[[list], None], - discovery_info: Optional[dict] = None) -> None: +def setup_platform( + hass: HomeAssistantType, + config: ConfigType, + add_entities: Callable[[list], None], + discovery_info: Optional[dict] = None, +) -> None: """Set up the GTFS sensor.""" gtfs_dir = hass.config.path(DEFAULT_PATH) data = config[CONF_DATA] @@ -371,17 +365,23 @@ def setup_platform(hass: HomeAssistantType, config: ConfigType, if not gtfs.feeds: pygtfs.append_feed(gtfs, os.path.join(gtfs_dir, data)) - add_entities([ - GTFSDepartureSensor(gtfs, name, origin, destination, offset, - include_tomorrow)]) + add_entities( + [GTFSDepartureSensor(gtfs, name, origin, destination, offset, include_tomorrow)] + ) class GTFSDepartureSensor(Entity): """Implementation of a GTFS departure sensor.""" - def __init__(self, pygtfs: Any, name: Optional[Any], origin: Any, - destination: Any, offset: cv.time_period, - include_tomorrow: bool) -> None: + def __init__( + self, + pygtfs: Any, + name: Optional[Any], + origin: Any, + destination: Any, + offset: cv.time_period, + include_tomorrow: bool, + ) -> None: """Initialize the sensor.""" self._pygtfs = pygtfs self.origin = origin @@ -392,7 +392,7 @@ class GTFSDepartureSensor(Entity): self._available = False self._icon = ICON - self._name = '' + self._name = "" self._state = None # type: Optional[str] self._attributes = {} # type: dict @@ -452,8 +452,9 @@ class GTFSDepartureSensor(Entity): stops = self._pygtfs.stops_by_id(self.destination) if not stops: self._available = False - _LOGGER.warning("Destination stop ID %s not found", - self.destination) + _LOGGER.warning( + "Destination stop ID %s not found", self.destination + ) return self._destination = stops[0] @@ -461,43 +462,47 @@ class GTFSDepartureSensor(Entity): # Fetch next departure self._departure = get_next_departure( - self._pygtfs, self.origin, self.destination, self._offset, - self._include_tomorrow) + self._pygtfs, + self.origin, + self.destination, + self._offset, + self._include_tomorrow, + ) # Define the state as a UTC timestamp with ISO 8601 format if not self._departure: self._state = None else: self._state = dt_util.as_utc( - self._departure['departure_time']).isoformat() + self._departure["departure_time"] + ).isoformat() # Fetch trip and route details once, unless updated if not self._departure: self._trip = None else: - trip_id = self._departure['trip_id'] + trip_id = self._departure["trip_id"] if not self._trip or self._trip.trip_id != trip_id: _LOGGER.debug("Fetching trip details for %s", trip_id) self._trip = self._pygtfs.trips_by_id(trip_id)[0] - route_id = self._departure['route_id'] + route_id = self._departure["route_id"] if not self._route or self._route.route_id != route_id: _LOGGER.debug("Fetching route details for %s", route_id) self._route = self._pygtfs.routes_by_id(route_id)[0] # Fetch agency details exactly once if self._agency is None and self._route: - _LOGGER.debug("Fetching agency details for %s", - self._route.agency_id) + _LOGGER.debug("Fetching agency details for %s", self._route.agency_id) try: - self._agency = self._pygtfs.agencies_by_id( - self._route.agency_id)[0] + self._agency = self._pygtfs.agencies_by_id(self._route.agency_id)[0] except IndexError: _LOGGER.warning( "Agency ID '%s' was not found in agency table, " "you may want to update the routes database table " "to fix this missing reference", - self._route.agency_id) + self._route.agency_id, + ) self._agency = False # Assign attributes, icon and name @@ -508,33 +513,33 @@ class GTFSDepartureSensor(Entity): else: self._icon = ICON - name = '{agency} {origin} to {destination} next departure' + name = "{agency} {origin} to {destination} next departure" if not self._departure: - name = '{default}' - self._name = (self._custom_name or - name.format(agency=getattr(self._agency, - 'agency_name', - DEFAULT_NAME), - default=DEFAULT_NAME, - origin=self.origin, - destination=self.destination)) + name = "{default}" + self._name = self._custom_name or name.format( + agency=getattr(self._agency, "agency_name", DEFAULT_NAME), + default=DEFAULT_NAME, + origin=self.origin, + destination=self.destination, + ) def update_attributes(self) -> None: """Update state attributes.""" # Add departure information if self._departure: self._attributes[ATTR_ARRIVAL] = dt_util.as_utc( - self._departure['arrival_time']).isoformat() + self._departure["arrival_time"] + ).isoformat() - self._attributes[ATTR_DAY] = self._departure['day'] + self._attributes[ATTR_DAY] = self._departure["day"] if self._departure[ATTR_FIRST] is not None: - self._attributes[ATTR_FIRST] = self._departure['first'] + self._attributes[ATTR_FIRST] = self._departure["first"] elif ATTR_FIRST in self._attributes: del self._attributes[ATTR_FIRST] if self._departure[ATTR_LAST] is not None: - self._attributes[ATTR_LAST] = self._departure['last'] + self._attributes[ATTR_LAST] = self._departure["last"] elif ATTR_LAST in self._attributes: del self._attributes[ATTR_LAST] else: @@ -551,8 +556,11 @@ class GTFSDepartureSensor(Entity): self._attributes[ATTR_OFFSET] = self._offset.seconds / 60 if self._state is None: - self._attributes[ATTR_INFO] = "No more departures" if \ - self._include_tomorrow else "No more departures today" + self._attributes[ATTR_INFO] = ( + "No more departures" + if self._include_tomorrow + else "No more departures today" + ) elif ATTR_INFO in self._attributes: del self._attributes[ATTR_INFO] @@ -562,113 +570,115 @@ class GTFSDepartureSensor(Entity): del self._attributes[ATTR_ATTRIBUTION] # Add extra metadata - key = 'agency_id' + key = "agency_id" if self._agency and key not in self._attributes: - self.append_keys(self.dict_for_table(self._agency), 'Agency') + self.append_keys(self.dict_for_table(self._agency), "Agency") - key = 'origin_station_stop_id' + key = "origin_station_stop_id" if self._origin and key not in self._attributes: - self.append_keys(self.dict_for_table(self._origin), - "Origin Station") - self._attributes[ATTR_LOCATION_ORIGIN] = \ - LOCATION_TYPE_OPTIONS.get( - self._origin.location_type, - LOCATION_TYPE_DEFAULT) - self._attributes[ATTR_WHEELCHAIR_ORIGIN] = \ - WHEELCHAIR_BOARDING_OPTIONS.get( - self._origin.wheelchair_boarding, - WHEELCHAIR_BOARDING_DEFAULT) + self.append_keys(self.dict_for_table(self._origin), "Origin Station") + self._attributes[ATTR_LOCATION_ORIGIN] = LOCATION_TYPE_OPTIONS.get( + self._origin.location_type, LOCATION_TYPE_DEFAULT + ) + self._attributes[ATTR_WHEELCHAIR_ORIGIN] = WHEELCHAIR_BOARDING_OPTIONS.get( + self._origin.wheelchair_boarding, WHEELCHAIR_BOARDING_DEFAULT + ) - key = 'destination_station_stop_id' + key = "destination_station_stop_id" if self._destination and key not in self._attributes: - self.append_keys(self.dict_for_table(self._destination), - "Destination Station") - self._attributes[ATTR_LOCATION_DESTINATION] = \ - LOCATION_TYPE_OPTIONS.get( - self._destination.location_type, - LOCATION_TYPE_DEFAULT) - self._attributes[ATTR_WHEELCHAIR_DESTINATION] = \ - WHEELCHAIR_BOARDING_OPTIONS.get( - self._destination.wheelchair_boarding, - WHEELCHAIR_BOARDING_DEFAULT) + self.append_keys( + self.dict_for_table(self._destination), "Destination Station" + ) + self._attributes[ATTR_LOCATION_DESTINATION] = LOCATION_TYPE_OPTIONS.get( + self._destination.location_type, LOCATION_TYPE_DEFAULT + ) + self._attributes[ + ATTR_WHEELCHAIR_DESTINATION + ] = WHEELCHAIR_BOARDING_OPTIONS.get( + self._destination.wheelchair_boarding, WHEELCHAIR_BOARDING_DEFAULT + ) # Manage Route metadata - key = 'route_id' + key = "route_id" if not self._route and key in self._attributes: - self.remove_keys('Route') - elif self._route and (key not in self._attributes or - self._attributes[key] != self._route.route_id): - self.append_keys(self.dict_for_table(self._route), 'Route') - self._attributes[ATTR_ROUTE_TYPE] = \ - ROUTE_TYPE_OPTIONS[self._route.route_type] + self.remove_keys("Route") + elif self._route and ( + key not in self._attributes or self._attributes[key] != self._route.route_id + ): + self.append_keys(self.dict_for_table(self._route), "Route") + self._attributes[ATTR_ROUTE_TYPE] = ROUTE_TYPE_OPTIONS[ + self._route.route_type + ] # Manage Trip metadata - key = 'trip_id' + key = "trip_id" if not self._trip and key in self._attributes: - self.remove_keys('Trip') - elif self._trip and (key not in self._attributes or - self._attributes[key] != self._trip.trip_id): - self.append_keys(self.dict_for_table(self._trip), 'Trip') + self.remove_keys("Trip") + elif self._trip and ( + key not in self._attributes or self._attributes[key] != self._trip.trip_id + ): + self.append_keys(self.dict_for_table(self._trip), "Trip") self._attributes[ATTR_BICYCLE] = BICYCLE_ALLOWED_OPTIONS.get( - self._trip.bikes_allowed, - BICYCLE_ALLOWED_DEFAULT) + self._trip.bikes_allowed, BICYCLE_ALLOWED_DEFAULT + ) self._attributes[ATTR_WHEELCHAIR] = WHEELCHAIR_ACCESS_OPTIONS.get( - self._trip.wheelchair_accessible, - WHEELCHAIR_ACCESS_DEFAULT) + self._trip.wheelchair_accessible, WHEELCHAIR_ACCESS_DEFAULT + ) # Manage Stop Times metadata - prefix = 'origin_stop' + prefix = "origin_stop" if self._departure: - self.append_keys(self._departure['origin_stop_time'], prefix) + self.append_keys(self._departure["origin_stop_time"], prefix) self._attributes[ATTR_DROP_OFF_ORIGIN] = DROP_OFF_TYPE_OPTIONS.get( - self._departure['origin_stop_time']['Drop Off Type'], - DROP_OFF_TYPE_DEFAULT) + self._departure["origin_stop_time"]["Drop Off Type"], + DROP_OFF_TYPE_DEFAULT, + ) self._attributes[ATTR_PICKUP_ORIGIN] = PICKUP_TYPE_OPTIONS.get( - self._departure['origin_stop_time']['Pickup Type'], - PICKUP_TYPE_DEFAULT) + self._departure["origin_stop_time"]["Pickup Type"], PICKUP_TYPE_DEFAULT + ) self._attributes[ATTR_TIMEPOINT_ORIGIN] = TIMEPOINT_OPTIONS.get( - self._departure['origin_stop_time']['Timepoint'], - TIMEPOINT_DEFAULT) + self._departure["origin_stop_time"]["Timepoint"], TIMEPOINT_DEFAULT + ) else: self.remove_keys(prefix) - prefix = 'destination_stop' + prefix = "destination_stop" if self._departure: - self.append_keys(self._departure['destination_stop_time'], prefix) - self._attributes[ATTR_DROP_OFF_DESTINATION] = \ - DROP_OFF_TYPE_OPTIONS.get( - self._departure['destination_stop_time']['Drop Off Type'], - DROP_OFF_TYPE_DEFAULT) - self._attributes[ATTR_PICKUP_DESTINATION] = \ - PICKUP_TYPE_OPTIONS.get( - self._departure['destination_stop_time']['Pickup Type'], - PICKUP_TYPE_DEFAULT) - self._attributes[ATTR_TIMEPOINT_DESTINATION] = \ - TIMEPOINT_OPTIONS.get( - self._departure['destination_stop_time']['Timepoint'], - TIMEPOINT_DEFAULT) + self.append_keys(self._departure["destination_stop_time"], prefix) + self._attributes[ATTR_DROP_OFF_DESTINATION] = DROP_OFF_TYPE_OPTIONS.get( + self._departure["destination_stop_time"]["Drop Off Type"], + DROP_OFF_TYPE_DEFAULT, + ) + self._attributes[ATTR_PICKUP_DESTINATION] = PICKUP_TYPE_OPTIONS.get( + self._departure["destination_stop_time"]["Pickup Type"], + PICKUP_TYPE_DEFAULT, + ) + self._attributes[ATTR_TIMEPOINT_DESTINATION] = TIMEPOINT_OPTIONS.get( + self._departure["destination_stop_time"]["Timepoint"], TIMEPOINT_DEFAULT + ) else: self.remove_keys(prefix) @staticmethod def dict_for_table(resource: Any) -> dict: """Return a dictionary for the SQLAlchemy resource given.""" - return dict((col, getattr(resource, col)) - for col in resource.__table__.columns.keys()) + return dict( + (col, getattr(resource, col)) for col in resource.__table__.columns.keys() + ) - def append_keys(self, resource: dict, prefix: Optional[str] = None) -> \ - None: + def append_keys(self, resource: dict, prefix: Optional[str] = None) -> None: """Properly format key val pairs to append to attributes.""" for attr, val in resource.items(): - if val == '' or val is None or attr == 'feed_id': + if val == "" or val is None or attr == "feed_id": continue key = attr if prefix and not key.startswith(prefix): - key = '{} {}'.format(prefix, key) + key = "{} {}".format(prefix, key) key = slugify(key) self._attributes[key] = val def remove_keys(self, prefix: str) -> None: """Remove attributes whose key starts with prefix.""" - self._attributes = {k: v for k, v in self._attributes.items() if - not k.startswith(prefix)} + self._attributes = { + k: v for k, v in self._attributes.items() if not k.startswith(prefix) + } diff --git a/homeassistant/components/gtt/sensor.py b/homeassistant/components/gtt/sensor.py index ecabd5f0a71..43f13c94620 100644 --- a/homeassistant/components/gtt/sensor.py +++ b/homeassistant/components/gtt/sensor.py @@ -11,17 +11,16 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_STOP = 'stop' -CONF_BUS_NAME = 'bus_name' +CONF_STOP = "stop" +CONF_BUS_NAME = "bus_name" -ICON = 'mdi:train' +ICON = "mdi:train" SCAN_INTERVAL = timedelta(minutes=2) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_STOP): cv.string, - vol.Optional(CONF_BUS_NAME): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_STOP): cv.string, vol.Optional(CONF_BUS_NAME): cv.string} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -39,7 +38,7 @@ class GttSensor(Entity): """Initialize the Gtt sensor.""" self.data = GttData(stop, bus_name) self._state = None - self._name = 'Stop {}'.format(stop) + self._name = "Stop {}".format(stop) @property def name(self): @@ -64,9 +63,7 @@ class GttSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - attr = { - 'bus_name': self.data.state_bus['bus_name'] - } + attr = {"bus_name": self.data.state_bus["bus_name"]} return attr def update(self): @@ -82,6 +79,7 @@ class GttData: def __init__(self, stop, bus_name): """Initialize the GttData class.""" from pygtt import PyGTT + self._pygtt = PyGTT() self._stop = stop self._bus_name = bus_name @@ -102,13 +100,13 @@ class GttData: def get_bus_by_name(self): """Get the bus by name.""" for bus in self.bus_list: - if bus['bus_name'] == self._bus_name: + if bus["bus_name"] == self._bus_name: return bus def get_datetime(bus): """Get the datetime from a bus.""" - bustime = datetime.strptime(bus['time'][0]['run'], "%H:%M") + bustime = datetime.strptime(bus["time"][0]["run"], "%H:%M") now = datetime.now() bustime = bustime.replace(year=now.year, month=now.month, day=now.day) if bustime < now: diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 611e8df006a..f94a09e4da5 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -5,44 +5,48 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_API_KEY, CONF_NAME, CONF_PATH, CONF_SENSORS, CONF_URL) + CONF_API_KEY, + CONF_NAME, + CONF_PATH, + CONF_SENSORS, + CONF_URL, +) from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) -CONF_API_USER = 'api_user' +CONF_API_USER = "api_user" -DEFAULT_URL = 'https://habitica.com' -DOMAIN = 'habitica' +DEFAULT_URL = "https://habitica.com" +DOMAIN = "habitica" -ST = SensorType = namedtuple('SensorType', [ - 'name', 'icon', 'unit', 'path' -]) +ST = SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"]) SENSORS_TYPES = { - 'name': ST('Name', None, '', ['profile', 'name']), - 'hp': ST('HP', 'mdi:heart', 'HP', ['stats', 'hp']), - 'maxHealth': ST('max HP', 'mdi:heart', 'HP', ['stats', 'maxHealth']), - 'mp': ST('Mana', 'mdi:auto-fix', 'MP', ['stats', 'mp']), - 'maxMP': ST('max Mana', 'mdi:auto-fix', 'MP', ['stats', 'maxMP']), - 'exp': ST('EXP', 'mdi:star', 'EXP', ['stats', 'exp']), - 'toNextLevel': ST( - 'Next Lvl', 'mdi:star', 'EXP', ['stats', 'toNextLevel']), - 'lvl': ST( - 'Lvl', 'mdi:arrow-up-bold-circle-outline', 'Lvl', ['stats', 'lvl']), - 'gp': ST('Gold', 'mdi:coin', 'Gold', ['stats', 'gp']), - 'class': ST('Class', 'mdi:sword', '', ['stats', 'class']) + "name": ST("Name", None, "", ["profile", "name"]), + "hp": ST("HP", "mdi:heart", "HP", ["stats", "hp"]), + "maxHealth": ST("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]), + "mp": ST("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]), + "maxMP": ST("max Mana", "mdi:auto-fix", "MP", ["stats", "maxMP"]), + "exp": ST("EXP", "mdi:star", "EXP", ["stats", "exp"]), + "toNextLevel": ST("Next Lvl", "mdi:star", "EXP", ["stats", "toNextLevel"]), + "lvl": ST("Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]), + "gp": ST("Gold", "mdi:coin", "Gold", ["stats", "gp"]), + "class": ST("Class", "mdi:sword", "", ["stats", "class"]), } -INSTANCE_SCHEMA = vol.Schema({ - vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url, - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_API_USER): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): - vol.All(cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))]), -}) +INSTANCE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url, + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_API_USER): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): vol.All( + cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))] + ), + } +) has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name # because we want a handy alias @@ -59,33 +63,31 @@ def has_all_unique_users_names(value): """Validate that all user's names are unique and set if any is set.""" names = [user.get(CONF_NAME) for user in value] if None in names and any(name is not None for name in names): - raise vol.Invalid( - 'user names of all users must be set if any is set') + raise vol.Invalid("user names of all users must be set if any is set") if not all(name is None for name in names): has_unique_values(names) return value INSTANCE_LIST_SCHEMA = vol.All( - cv.ensure_list, has_all_unique_users, has_all_unique_users_names, - [INSTANCE_SCHEMA]) + cv.ensure_list, has_all_unique_users, has_all_unique_users_names, [INSTANCE_SCHEMA] +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: INSTANCE_LIST_SCHEMA -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema({DOMAIN: INSTANCE_LIST_SCHEMA}, extra=vol.ALLOW_EXTRA) -SERVICE_API_CALL = 'api_call' +SERVICE_API_CALL = "api_call" ATTR_NAME = CONF_NAME ATTR_PATH = CONF_PATH -ATTR_ARGS = 'args' -EVENT_API_CALL_SUCCESS = '{0}_{1}_{2}'.format( - DOMAIN, SERVICE_API_CALL, 'success') +ATTR_ARGS = "args" +EVENT_API_CALL_SUCCESS = "{0}_{1}_{2}".format(DOMAIN, SERVICE_API_CALL, "success") -SERVICE_API_CALL_SCHEMA = vol.Schema({ - vol.Required(ATTR_NAME): str, - vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]), - vol.Optional(ATTR_ARGS): dict, -}) +SERVICE_API_CALL_SCHEMA = vol.Schema( + { + vol.Required(ATTR_NAME): str, + vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]), + vol.Optional(ATTR_ARGS): dict, + } +) async def async_setup(hass, config): @@ -107,17 +109,22 @@ async def async_setup(hass, config): username = instance[CONF_API_USER] password = instance[CONF_API_KEY] name = instance.get(CONF_NAME) - config_dict = {'url': url, 'login': username, 'password': password} + config_dict = {"url": url, "login": username, "password": password} api = HAHabitipyAsync(config_dict) user = await api.user.get() if name is None: - name = user['profile']['name'] + name = user["profile"]["name"] data[name] = api if CONF_SENSORS in instance: hass.async_create_task( discovery.async_load_platform( - hass, 'sensor', DOMAIN, - {'name': name, 'sensors': instance[CONF_SENSORS]}, config)) + hass, + "sensor", + DOMAIN, + {"name": name, "sensors": instance[CONF_SENSORS]}, + config, + ) + ) async def handle_api_call(call): name = call.data[ATTR_NAME] @@ -131,15 +138,16 @@ async def async_setup(hass, config): api = api[element] except KeyError: _LOGGER.error( - "API_CALL: Path %s is invalid for API on '{%s}' element", - path, element) + "API_CALL: Path %s is invalid for API on '{%s}' element", path, element + ) return kwargs = call.data.get(ATTR_ARGS, {}) data = await api(**kwargs) hass.bus.async_fire( - EVENT_API_CALL_SUCCESS, {'name': name, 'path': path, 'data': data}) + EVENT_API_CALL_SUCCESS, {"name": name, "path": path, "data": data} + ) hass.services.async_register( - DOMAIN, SERVICE_API_CALL, handle_api_call, - schema=SERVICE_API_CALL_SCHEMA) + DOMAIN, SERVICE_API_CALL, handle_api_call, schema=SERVICE_API_CALL_SCHEMA + ) return True diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index fb3a5670c2b..e70d0eb696a 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -11,8 +11,7 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) -async def async_setup_platform( - hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the habitica platform.""" if discovery_info is None: return @@ -21,10 +20,9 @@ async def async_setup_platform( sensors = discovery_info[habitica.CONF_SENSORS] sensor_data = HabitipyData(hass.data[habitica.DOMAIN][name]) await sensor_data.update() - async_add_devices([ - HabitipySensor(name, sensor, sensor_data) - for sensor in sensors - ], True) + async_add_devices( + [HabitipySensor(name, sensor, sensor_data) for sensor in sensors], True + ) class HabitipyData: @@ -68,8 +66,7 @@ class HabitipySensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{0}_{1}_{2}".format( - habitica.DOMAIN, self._name, self._sensor_name) + return "{0}_{1}_{2}".format(habitica.DOMAIN, self._name, self._sensor_name) @property def state(self): diff --git a/homeassistant/components/hangouts/.translations/bg.json b/homeassistant/components/hangouts/.translations/bg.json new file mode 100644 index 00000000000..10c1666074c --- /dev/null +++ b/homeassistant/components/hangouts/.translations/bg.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "unknown": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430." + }, + "error": { + "invalid_2fa": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 2-\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f, \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", + "invalid_2fa_method": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043c\u0435\u0442\u043e\u0434 2FA (\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430).", + "invalid_login": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0432\u043b\u0438\u0437\u0430\u043d\u0435, \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA PIN" + }, + "title": "\u0414\u0432\u0443-\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "user": { + "data": { + "authorization_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f (\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0437\u0430 \u0440\u044a\u0447\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435)", + "email": "E-mail \u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "title": "\u0412\u0445\u043e\u0434 \u0432 Google Hangouts" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/da.json b/homeassistant/components/hangouts/.translations/da.json index 079b57722e2..4155da38f8f 100644 --- a/homeassistant/components/hangouts/.translations/da.json +++ b/homeassistant/components/hangouts/.translations/da.json @@ -18,6 +18,7 @@ }, "user": { "data": { + "authorization_code": "Autorisationskode (kr\u00e6ves til manuel godkendelse)", "email": "Email adresse", "password": "Adgangskode" }, diff --git a/homeassistant/components/hangouts/.translations/pt-BR.json b/homeassistant/components/hangouts/.translations/pt-BR.json index 444edc40838..553360d8da6 100644 --- a/homeassistant/components/hangouts/.translations/pt-BR.json +++ b/homeassistant/components/hangouts/.translations/pt-BR.json @@ -19,6 +19,7 @@ }, "user": { "data": { + "authorization_code": "C\u00f3digo de Autoriza\u00e7\u00e3o (requerido para autentica\u00e7\u00e3o manual)", "email": "Endere\u00e7o de e-mail", "password": "Senha" }, diff --git a/homeassistant/components/hangouts/.translations/vi.json b/homeassistant/components/hangouts/.translations/vi.json new file mode 100644 index 00000000000..d794a0b5afa --- /dev/null +++ b/homeassistant/components/hangouts/.translations/vi.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_2fa_method": "Ph\u01b0\u01a1ng ph\u00e1p 2FA kh\u00f4ng h\u1ee3p l\u1ec7 (X\u00e1c minh tr\u00ean \u0111i\u1ec7n tho\u1ea1i)." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/zh-Hans.json b/homeassistant/components/hangouts/.translations/zh-Hans.json index bee6bf753db..d4e6e360c23 100644 --- a/homeassistant/components/hangouts/.translations/zh-Hans.json +++ b/homeassistant/components/hangouts/.translations/zh-Hans.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA Pin" }, + "description": "\u65e0", "title": "\u53cc\u91cd\u8ba4\u8bc1" }, "user": { @@ -21,6 +22,7 @@ "email": "\u7535\u5b50\u90ae\u4ef6\u5730\u5740", "password": "\u5bc6\u7801" }, + "description": "\u65e0", "title": "\u767b\u5f55 Google Hangouts" } }, diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 50936ac62a0..885ac5d1670 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -12,26 +12,44 @@ import homeassistant.helpers.config_validation as cv from .intents import HelpIntent from .config_flow import HangoutsFlowHandler # noqa: F401 from .const import ( - CONF_BOT, CONF_DEFAULT_CONVERSATIONS, CONF_ERROR_SUPPRESSED_CONVERSATIONS, - CONF_INTENTS, CONF_MATCHERS, CONF_REFRESH_TOKEN, CONF_SENTENCES, DOMAIN, - EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, INTENT_HELP, INTENT_SCHEMA, - MESSAGE_SCHEMA, SERVICE_RECONNECT, SERVICE_SEND_MESSAGE, SERVICE_UPDATE, - TARGETS_SCHEMA) + CONF_BOT, + CONF_DEFAULT_CONVERSATIONS, + CONF_ERROR_SUPPRESSED_CONVERSATIONS, + CONF_INTENTS, + CONF_MATCHERS, + CONF_REFRESH_TOKEN, + CONF_SENTENCES, + DOMAIN, + EVENT_HANGOUTS_CONNECTED, + EVENT_HANGOUTS_CONVERSATIONS_CHANGED, + EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, + INTENT_HELP, + INTENT_SCHEMA, + MESSAGE_SCHEMA, + SERVICE_RECONNECT, + SERVICE_SEND_MESSAGE, + SERVICE_UPDATE, + TARGETS_SCHEMA, +) _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_INTENTS, default={}): vol.Schema({ - cv.string: INTENT_SCHEMA - }), - vol.Optional(CONF_DEFAULT_CONVERSATIONS, default=[]): - [TARGETS_SCHEMA], - vol.Optional(CONF_ERROR_SUPPRESSED_CONVERSATIONS, default=[]): - [TARGETS_SCHEMA], - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_INTENTS, default={}): vol.Schema( + {cv.string: INTENT_SCHEMA} + ), + vol.Optional(CONF_DEFAULT_CONVERSATIONS, default=[]): [TARGETS_SCHEMA], + vol.Optional(CONF_ERROR_SUPPRESSED_CONVERSATIONS, default=[]): [ + TARGETS_SCHEMA + ], + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): @@ -50,14 +68,16 @@ async def async_setup(hass, config): hass.data[DOMAIN] = { CONF_INTENTS: config[CONF_INTENTS], CONF_DEFAULT_CONVERSATIONS: config[CONF_DEFAULT_CONVERSATIONS], - CONF_ERROR_SUPPRESSED_CONVERSATIONS: - config[CONF_ERROR_SUPPRESSED_CONVERSATIONS], + CONF_ERROR_SUPPRESSED_CONVERSATIONS: config[ + CONF_ERROR_SUPPRESSED_CONVERSATIONS + ], } - if (hass.data[DOMAIN][CONF_INTENTS] and - INTENT_HELP not in hass.data[DOMAIN][CONF_INTENTS]): - hass.data[DOMAIN][CONF_INTENTS][INTENT_HELP] = { - CONF_SENTENCES: ['HELP']} + if ( + hass.data[DOMAIN][CONF_INTENTS] + and INTENT_HELP not in hass.data[DOMAIN][CONF_INTENTS] + ): + hass.data[DOMAIN][CONF_INTENTS][INTENT_HELP] = {CONF_SENTENCES: ["HELP"]} for data in hass.data[DOMAIN][CONF_INTENTS].values(): matchers = [] @@ -66,9 +86,11 @@ async def async_setup(hass, config): data[CONF_MATCHERS] = matchers - hass.async_create_task(hass.config_entries.flow.async_init( - DOMAIN, context={'source': config_entries.SOURCE_IMPORT} - )) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + ) return True @@ -85,46 +107,47 @@ async def async_setup_entry(hass, config): config.data.get(CONF_REFRESH_TOKEN), hass.data[DOMAIN][CONF_INTENTS], hass.data[DOMAIN][CONF_DEFAULT_CONVERSATIONS], - hass.data[DOMAIN][CONF_ERROR_SUPPRESSED_CONVERSATIONS]) + hass.data[DOMAIN][CONF_ERROR_SUPPRESSED_CONVERSATIONS], + ) hass.data[DOMAIN][CONF_BOT] = bot except GoogleAuthError as exception: _LOGGER.error("Hangouts failed to log in: %s", str(exception)) return False dispatcher.async_dispatcher_connect( - hass, - EVENT_HANGOUTS_CONNECTED, - bot.async_handle_update_users_and_conversations) + hass, EVENT_HANGOUTS_CONNECTED, bot.async_handle_update_users_and_conversations + ) dispatcher.async_dispatcher_connect( - hass, - EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - bot.async_resolve_conversations) + hass, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, bot.async_resolve_conversations + ) dispatcher.async_dispatcher_connect( hass, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, - bot.async_update_conversation_commands) + bot.async_update_conversation_commands, + ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, - bot.async_handle_hass_stop) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, bot.async_handle_hass_stop) await bot.async_connect() - hass.services.async_register(DOMAIN, SERVICE_SEND_MESSAGE, - bot.async_handle_send_message, - schema=MESSAGE_SCHEMA) - hass.services.async_register(DOMAIN, - SERVICE_UPDATE, - bot. - async_handle_update_users_and_conversations, - schema=vol.Schema({})) + hass.services.async_register( + DOMAIN, + SERVICE_SEND_MESSAGE, + bot.async_handle_send_message, + schema=MESSAGE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_UPDATE, + bot.async_handle_update_users_and_conversations, + schema=vol.Schema({}), + ) - hass.services.async_register(DOMAIN, - SERVICE_RECONNECT, - bot. - async_handle_reconnect, - schema=vol.Schema({})) + hass.services.async_register( + DOMAIN, SERVICE_RECONNECT, bot.async_handle_reconnect, schema=vol.Schema({}) + ) intent.async_register(hass, HelpIntent(hass)) diff --git a/homeassistant/components/hangouts/config_flow.py b/homeassistant/components/hangouts/config_flow.py index 743c49abfdf..8e262d8b40f 100644 --- a/homeassistant/components/hangouts/config_flow.py +++ b/homeassistant/components/hangouts/config_flow.py @@ -7,8 +7,12 @@ from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback -from .const import CONF_2FA, CONF_REFRESH_TOKEN, CONF_AUTH_CODE, \ - DOMAIN as HANGOUTS_DOMAIN +from .const import ( + CONF_2FA, + CONF_REFRESH_TOKEN, + CONF_AUTH_CODE, + DOMAIN as HANGOUTS_DOMAIN, +) @callback @@ -41,26 +45,31 @@ class HangoutsFlowHandler(config_entries.ConfigFlow): if user_input is not None: from hangups import get_auth - from .hangups_utils import (HangoutsCredentials, - HangoutsRefreshToken, - GoogleAuthError, Google2FAError) + from .hangups_utils import ( + HangoutsCredentials, + HangoutsRefreshToken, + GoogleAuthError, + Google2FAError, + ) + user_email = user_input[CONF_EMAIL] user_password = user_input[CONF_PASSWORD] user_auth_code = user_input.get(CONF_AUTH_CODE) manual_login = user_auth_code is not None user_pin = None - self._credentials = HangoutsCredentials(user_email, - user_password, - user_pin, - user_auth_code) + self._credentials = HangoutsCredentials( + user_email, user_password, user_pin, user_auth_code + ) self._refresh_token = HangoutsRefreshToken(None) try: await self.hass.async_add_executor_job( - functools.partial(get_auth, - self._credentials, - self._refresh_token, - manual_login=manual_login) + functools.partial( + get_auth, + self._credentials, + self._refresh_token, + manual_login=manual_login, + ) ) return await self.async_step_final() @@ -68,19 +77,21 @@ class HangoutsFlowHandler(config_entries.ConfigFlow): if isinstance(err, Google2FAError): return await self.async_step_2fa() msg = str(err) - if msg == 'Unknown verification code input': - errors['base'] = 'invalid_2fa_method' + if msg == "Unknown verification code input": + errors["base"] = "invalid_2fa_method" else: - errors['base'] = 'invalid_login' + errors["base"] = "invalid_login" return self.async_show_form( - step_id='user', - data_schema=vol.Schema({ - vol.Required(CONF_EMAIL): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_AUTH_CODE): str - }), - errors=errors + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_AUTH_CODE): str, + } + ), + errors=errors, ) async def async_step_2fa(self, user_input=None): @@ -90,22 +101,21 @@ class HangoutsFlowHandler(config_entries.ConfigFlow): if user_input is not None: from hangups import get_auth from .hangups_utils import GoogleAuthError + self._credentials.set_verification_code(user_input[CONF_2FA]) try: - await self.hass.async_add_executor_job(get_auth, - self._credentials, - self._refresh_token) + await self.hass.async_add_executor_job( + get_auth, self._credentials, self._refresh_token + ) return await self.async_step_final() except GoogleAuthError: - errors['base'] = 'invalid_2fa' + errors["base"] = "invalid_2fa" return self.async_show_form( step_id=CONF_2FA, - data_schema=vol.Schema({ - vol.Required(CONF_2FA): str, - }), - errors=errors + data_schema=vol.Schema({vol.Required(CONF_2FA): str}), + errors=errors, ) async def async_step_final(self): @@ -114,8 +124,9 @@ class HangoutsFlowHandler(config_entries.ConfigFlow): title=self._credentials.get_email(), data={ CONF_EMAIL: self._credentials.get_email(), - CONF_REFRESH_TOKEN: self._refresh_token.get() - }) + CONF_REFRESH_TOKEN: self._refresh_token.get(), + }, + ) async def async_step_import(self, _): """Handle a flow import.""" diff --git a/homeassistant/components/hangouts/const.py b/homeassistant/components/hangouts/const.py index f664e769b9f..0508bf48703 100644 --- a/homeassistant/components/hangouts/const.py +++ b/homeassistant/components/hangouts/const.py @@ -3,77 +3,83 @@ import logging import voluptuous as vol -from homeassistant.components.notify import ( - ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET) +from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger('.') +_LOGGER = logging.getLogger(".") -DOMAIN = 'hangouts' +DOMAIN = "hangouts" -CONF_2FA = '2fa' -CONF_AUTH_CODE = 'authorization_code' -CONF_REFRESH_TOKEN = 'refresh_token' -CONF_BOT = 'bot' +CONF_2FA = "2fa" +CONF_AUTH_CODE = "authorization_code" +CONF_REFRESH_TOKEN = "refresh_token" +CONF_BOT = "bot" -CONF_CONVERSATIONS = 'conversations' -CONF_DEFAULT_CONVERSATIONS = 'default_conversations' -CONF_ERROR_SUPPRESSED_CONVERSATIONS = 'error_suppressed_conversations' +CONF_CONVERSATIONS = "conversations" +CONF_DEFAULT_CONVERSATIONS = "default_conversations" +CONF_ERROR_SUPPRESSED_CONVERSATIONS = "error_suppressed_conversations" -CONF_INTENTS = 'intents' -CONF_INTENT_TYPE = 'intent_type' -CONF_SENTENCES = 'sentences' -CONF_MATCHERS = 'matchers' +CONF_INTENTS = "intents" +CONF_INTENT_TYPE = "intent_type" +CONF_SENTENCES = "sentences" +CONF_MATCHERS = "matchers" -INTENT_HELP = 'HangoutsHelp' +INTENT_HELP = "HangoutsHelp" -EVENT_HANGOUTS_CONNECTED = 'hangouts_connected' -EVENT_HANGOUTS_DISCONNECTED = 'hangouts_disconnected' -EVENT_HANGOUTS_USERS_CHANGED = 'hangouts_users_changed' -EVENT_HANGOUTS_CONVERSATIONS_CHANGED = 'hangouts_conversations_changed' -EVENT_HANGOUTS_CONVERSATIONS_RESOLVED = 'hangouts_conversations_resolved' -EVENT_HANGOUTS_MESSAGE_RECEIVED = 'hangouts_message_received' +EVENT_HANGOUTS_CONNECTED = "hangouts_connected" +EVENT_HANGOUTS_DISCONNECTED = "hangouts_disconnected" +EVENT_HANGOUTS_USERS_CHANGED = "hangouts_users_changed" +EVENT_HANGOUTS_CONVERSATIONS_CHANGED = "hangouts_conversations_changed" +EVENT_HANGOUTS_CONVERSATIONS_RESOLVED = "hangouts_conversations_resolved" +EVENT_HANGOUTS_MESSAGE_RECEIVED = "hangouts_message_received" -CONF_CONVERSATION_ID = 'id' -CONF_CONVERSATION_NAME = 'name' +CONF_CONVERSATION_ID = "id" +CONF_CONVERSATION_NAME = "name" -SERVICE_SEND_MESSAGE = 'send_message' -SERVICE_UPDATE = 'update' -SERVICE_RECONNECT = 'reconnect' +SERVICE_SEND_MESSAGE = "send_message" +SERVICE_UPDATE = "update" +SERVICE_RECONNECT = "reconnect" TARGETS_SCHEMA = vol.All( - vol.Schema({ - vol.Exclusive(CONF_CONVERSATION_ID, 'id or name'): cv.string, - vol.Exclusive(CONF_CONVERSATION_NAME, 'id or name'): cv.string - }), - cv.has_at_least_one_key(CONF_CONVERSATION_ID, CONF_CONVERSATION_NAME) + vol.Schema( + { + vol.Exclusive(CONF_CONVERSATION_ID, "id or name"): cv.string, + vol.Exclusive(CONF_CONVERSATION_NAME, "id or name"): cv.string, + } + ), + cv.has_at_least_one_key(CONF_CONVERSATION_ID, CONF_CONVERSATION_NAME), +) +MESSAGE_SEGMENT_SCHEMA = vol.Schema( + { + vol.Required("text"): cv.string, + vol.Optional("is_bold"): cv.boolean, + vol.Optional("is_italic"): cv.boolean, + vol.Optional("is_strikethrough"): cv.boolean, + vol.Optional("is_underline"): cv.boolean, + vol.Optional("parse_str"): cv.boolean, + vol.Optional("link_target"): cv.string, + } +) +MESSAGE_DATA_SCHEMA = vol.Schema( + {vol.Optional("image_file"): cv.string, vol.Optional("image_url"): cv.string} ) -MESSAGE_SEGMENT_SCHEMA = vol.Schema({ - vol.Required('text'): cv.string, - vol.Optional('is_bold'): cv.boolean, - vol.Optional('is_italic'): cv.boolean, - vol.Optional('is_strikethrough'): cv.boolean, - vol.Optional('is_underline'): cv.boolean, - vol.Optional('parse_str'): cv.boolean, - vol.Optional('link_target'): cv.string -}) -MESSAGE_DATA_SCHEMA = vol.Schema({ - vol.Optional('image_file'): cv.string, - vol.Optional('image_url'): cv.string -}) -MESSAGE_SCHEMA = vol.Schema({ - vol.Required(ATTR_TARGET): [TARGETS_SCHEMA], - vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA], - vol.Optional(ATTR_DATA): MESSAGE_DATA_SCHEMA -}) +MESSAGE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_TARGET): [TARGETS_SCHEMA], + vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA], + vol.Optional(ATTR_DATA): MESSAGE_DATA_SCHEMA, + } +) INTENT_SCHEMA = vol.All( # Basic Schema - vol.Schema({ - vol.Required(CONF_SENTENCES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_CONVERSATIONS): [TARGETS_SCHEMA] - }), + vol.Schema( + { + vol.Required(CONF_SENTENCES): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_CONVERSATIONS): [TARGETS_SCHEMA], + } + ) ) diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index fe72c50de77..d444e852cca 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -9,11 +9,21 @@ from homeassistant.helpers import dispatcher, intent from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATION_ID, - CONF_CONVERSATION_NAME, CONF_CONVERSATIONS, CONF_MATCHERS, DOMAIN, - EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, EVENT_HANGOUTS_DISCONNECTED, - EVENT_HANGOUTS_MESSAGE_RECEIVED, INTENT_HELP) + ATTR_DATA, + ATTR_MESSAGE, + ATTR_TARGET, + CONF_CONVERSATION_ID, + CONF_CONVERSATION_NAME, + CONF_CONVERSATIONS, + CONF_MATCHERS, + DOMAIN, + EVENT_HANGOUTS_CONNECTED, + EVENT_HANGOUTS_CONVERSATIONS_CHANGED, + EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, + EVENT_HANGOUTS_DISCONNECTED, + EVENT_HANGOUTS_MESSAGE_RECEIVED, + INTENT_HELP, +) _LOGGER = logging.getLogger(__name__) @@ -21,8 +31,9 @@ _LOGGER = logging.getLogger(__name__) class HangoutsBot: """The Hangouts Bot.""" - def __init__(self, hass, refresh_token, intents, - default_convs, error_suppressed_convs): + def __init__( + self, hass, refresh_token, intents, default_convs, error_suppressed_convs + ): """Set up the client.""" self.hass = hass self._connected = False @@ -41,8 +52,10 @@ class HangoutsBot: self._error_suppressed_conv_ids = None dispatcher.async_dispatcher_connect( - self.hass, EVENT_HANGOUTS_MESSAGE_RECEIVED, - self._async_handle_conversation_message) + self.hass, + EVENT_HANGOUTS_MESSAGE_RECEIVED, + self._async_handle_conversation_message, + ) def _resolve_conversation_id(self, obj): if CONF_CONVERSATION_ID in obj: @@ -70,14 +83,15 @@ class HangoutsBot: conv_id = self._resolve_conversation_id(conversation) if conv_id is not None: conversations.append(conv_id) - data['_' + CONF_CONVERSATIONS] = conversations + data["_" + CONF_CONVERSATIONS] = conversations elif self._default_conv_ids: - data['_' + CONF_CONVERSATIONS] = self._default_conv_ids + data["_" + CONF_CONVERSATIONS] = self._default_conv_ids else: - data['_' + CONF_CONVERSATIONS] = \ - [conv.id_ for conv in self._conversation_list.get_all()] + data["_" + CONF_CONVERSATIONS] = [ + conv.id_ for conv in self._conversation_list.get_all() + ] - for conv_id in data['_' + CONF_CONVERSATIONS]: + for conv_id in data["_" + CONF_CONVERSATIONS]: if conv_id not in self._conversation_intents: self._conversation_intents[conv_id] = {} @@ -85,11 +99,13 @@ class HangoutsBot: try: self._conversation_list.on_event.remove_observer( - self._async_handle_conversation_event) + self._async_handle_conversation_event + ) except ValueError: pass self._conversation_list.on_event.add_observer( - self._async_handle_conversation_event) + self._async_handle_conversation_event + ) def async_resolve_conversations(self, _): """Resolve the list of default and error suppressed conversations.""" @@ -105,34 +121,36 @@ class HangoutsBot: conv_id = self._resolve_conversation_id(conversation) if conv_id is not None: self._error_suppressed_conv_ids.append(conv_id) - dispatcher.async_dispatcher_send(self.hass, - EVENT_HANGOUTS_CONVERSATIONS_RESOLVED) + dispatcher.async_dispatcher_send( + self.hass, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED + ) async def _async_handle_conversation_event(self, event): from hangups import ChatMessageEvent - if isinstance(event, ChatMessageEvent): - dispatcher.async_dispatcher_send(self.hass, - EVENT_HANGOUTS_MESSAGE_RECEIVED, - event.conversation_id, - event.user_id, event) - async def _async_handle_conversation_message(self, - conv_id, user_id, event): + if isinstance(event, ChatMessageEvent): + dispatcher.async_dispatcher_send( + self.hass, + EVENT_HANGOUTS_MESSAGE_RECEIVED, + event.conversation_id, + event.user_id, + event, + ) + + async def _async_handle_conversation_message(self, conv_id, user_id, event): """Handle a message sent to a conversation.""" user = self._user_list.get_user(user_id) if user.is_self: return message = event.text - _LOGGER.debug("Handling message '%s' from %s", - message, user.full_name) + _LOGGER.debug("Handling message '%s' from %s", message, user.full_name) intents = self._conversation_intents.get(conv_id) if intents is not None: is_error = False try: - intent_result = await self._async_process(intents, message, - conv_id) + intent_result = await self._async_process(intents, message, conv_id) except (intent.UnknownIntent, intent.IntentHandleError) as err: is_error = True intent_result = intent.IntentResponse() @@ -141,18 +159,20 @@ class HangoutsBot: if intent_result is None: is_error = True intent_result = intent.IntentResponse() - intent_result.async_set_speech( - "Sorry, I didn't understand that") + intent_result.async_set_speech("Sorry, I didn't understand that") - message = intent_result.as_dict().get('speech', {})\ - .get('plain', {}).get('speech') + message = ( + intent_result.as_dict().get("speech", {}).get("plain", {}).get("speech") + ) if (message is not None) and not ( - is_error and conv_id in self._error_suppressed_conv_ids): + is_error and conv_id in self._error_suppressed_conv_ids + ): await self._async_send_message( - [{'text': message, 'parse_str': True}], + [{"text": message, "parse_str": True}], [{CONF_CONVERSATION_ID: conv_id}], - None) + None, + ) async def _async_process(self, intents, text, conv_id): """Detect a matching intent.""" @@ -164,13 +184,15 @@ class HangoutsBot: continue if intent_type == INTENT_HELP: return await self.hass.helpers.intent.async_handle( - DOMAIN, intent_type, - {'conv_id': {'value': conv_id}}, text) + DOMAIN, intent_type, {"conv_id": {"value": conv_id}}, text + ) return await self.hass.helpers.intent.async_handle( - DOMAIN, intent_type, - {key: {'value': value} - for key, value in match.groupdict().items()}, text) + DOMAIN, + intent_type, + {key: {"value": value} for key, value in match.groupdict().items()}, + text, + ) async def async_connect(self): """Login to the Google Hangouts.""" @@ -178,9 +200,12 @@ class HangoutsBot: from hangups import Client from hangups import get_auth + session = await self.hass.async_add_executor_job( - get_auth, HangoutsCredentials(None, None, None), - HangoutsRefreshToken(self._refresh_token)) + get_auth, + HangoutsCredentials(None, None, None), + HangoutsRefreshToken(self._refresh_token), + ) self._client = Client(session) self._client.on_connect.add_observer(self._on_connect) @@ -189,18 +214,17 @@ class HangoutsBot: self.hass.loop.create_task(self._client.connect()) def _on_connect(self): - _LOGGER.debug('Connected!') + _LOGGER.debug("Connected!") self._connected = True dispatcher.async_dispatcher_send(self.hass, EVENT_HANGOUTS_CONNECTED) async def _on_disconnect(self): """Handle disconnecting.""" if self._connected: - _LOGGER.debug('Connection lost! Reconnect...') + _LOGGER.debug("Connection lost! Reconnect...") await self.async_connect() else: - dispatcher.async_dispatcher_send(self.hass, - EVENT_HANGOUTS_DISCONNECTED) + dispatcher.async_dispatcher_send(self.hass, EVENT_HANGOUTS_DISCONNECTED) async def async_disconnect(self): """Disconnect the client if it is connected.""" @@ -217,11 +241,11 @@ class HangoutsBot: for target in targets: conversation = None if CONF_CONVERSATION_ID in target: - conversation = self._conversation_list.get( - target[CONF_CONVERSATION_ID]) + conversation = self._conversation_list.get(target[CONF_CONVERSATION_ID]) elif CONF_CONVERSATION_NAME in target: conversation = self._resolve_conversation_name( - target[CONF_CONVERSATION_NAME]) + target[CONF_CONVERSATION_NAME] + ) if conversation is not None: conversations.append(conversation) @@ -229,31 +253,32 @@ class HangoutsBot: return False from hangups import ChatMessageSegment, hangouts_pb2 + messages = [] for segment in message: if messages: - messages.append(ChatMessageSegment('', - segment_type=hangouts_pb2. - SEGMENT_TYPE_LINE_BREAK)) - if 'parse_str' in segment and segment['parse_str']: - messages.extend(ChatMessageSegment.from_str(segment['text'])) + messages.append( + ChatMessageSegment( + "", segment_type=hangouts_pb2.SEGMENT_TYPE_LINE_BREAK + ) + ) + if "parse_str" in segment and segment["parse_str"]: + messages.extend(ChatMessageSegment.from_str(segment["text"])) else: - if 'parse_str' in segment: - del segment['parse_str'] + if "parse_str" in segment: + del segment["parse_str"] messages.append(ChatMessageSegment(**segment)) image_file = None if data: - if data.get('image_url'): - uri = data.get('image_url') + if data.get("image_url"): + uri = data.get("image_url") try: websession = async_get_clientsession(self.hass) async with websession.get(uri, timeout=5) as response: if response.status != 200: _LOGGER.error( - 'Fetch image failed, %s, %s', - response.status, - response + "Fetch image failed, %s, %s", response.status, response ) image_file = None else: @@ -261,21 +286,16 @@ class HangoutsBot: image_file = io.BytesIO(image_data) image_file.name = "image.png" except (asyncio.TimeoutError, aiohttp.ClientError) as error: - _LOGGER.error( - 'Failed to fetch image, %s', - type(error) - ) + _LOGGER.error("Failed to fetch image, %s", type(error)) image_file = None - elif data.get('image_file'): - uri = data.get('image_file') + elif data.get("image_file"): + uri = data.get("image_file") if self.hass.config.is_allowed_path(uri): try: - image_file = open(uri, 'rb') + image_file = open(uri, "rb") except IOError as error: _LOGGER.error( - 'Image file I/O error(%s): %s', - error.errno, - error.strerror + "Image file I/O error(%s): %s", error.errno, error.strerror ) else: _LOGGER.error('Path "%s" not allowed', uri) @@ -287,29 +307,37 @@ class HangoutsBot: async def _async_list_conversations(self): import hangups - self._user_list, self._conversation_list = \ - (await hangups.build_user_conversation_list(self._client)) + + self._user_list, self._conversation_list = await hangups.build_user_conversation_list( + self._client + ) conversations = {} for i, conv in enumerate(self._conversation_list.get_all()): users_in_conversation = [] for user in conv.users: users_in_conversation.append(user.full_name) - conversations[str(i)] = {CONF_CONVERSATION_ID: str(conv.id_), - CONF_CONVERSATION_NAME: conv.name, - 'users': users_in_conversation} + conversations[str(i)] = { + CONF_CONVERSATION_ID: str(conv.id_), + CONF_CONVERSATION_NAME: conv.name, + "users": users_in_conversation, + } - self.hass.states.async_set("{}.conversations".format(DOMAIN), - len(self._conversation_list.get_all()), - attributes=conversations) - dispatcher.async_dispatcher_send(self.hass, - EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - conversations) + self.hass.states.async_set( + "{}.conversations".format(DOMAIN), + len(self._conversation_list.get_all()), + attributes=conversations, + ) + dispatcher.async_dispatcher_send( + self.hass, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, conversations + ) async def async_handle_send_message(self, service): """Handle the send_message service.""" - await self._async_send_message(service.data[ATTR_MESSAGE], - service.data[ATTR_TARGET], - service.data.get(ATTR_DATA, {})) + await self._async_send_message( + service.data[ATTR_MESSAGE], + service.data[ATTR_TARGET], + service.data.get(ATTR_DATA, {}), + ) async def async_handle_update_users_and_conversations(self, _=None): """Handle the update_users_and_conversations service.""" diff --git a/homeassistant/components/hangouts/intents.py b/homeassistant/components/hangouts/intents.py index 3887a644700..a26da7a4872 100644 --- a/homeassistant/components/hangouts/intents.py +++ b/homeassistant/components/hangouts/intents.py @@ -9,9 +9,7 @@ class HelpIntent(intent.IntentHandler): """Handle Help intents.""" intent_type = INTENT_HELP - slot_schema = { - 'conv_id': cv.string - } + slot_schema = {"conv_id": cv.string} def __init__(self, hass): """Set up the intent.""" @@ -20,13 +18,13 @@ class HelpIntent(intent.IntentHandler): async def async_handle(self, intent_obj): """Handle the intent.""" slots = self.async_validate_slots(intent_obj.slots) - conv_id = slots['conv_id']['value'] + conv_id = slots["conv_id"]["value"] intents = self.hass.data[DOMAIN][CONF_BOT].get_intents(conv_id) response = intent_obj.create_response() help_text = "I understand the following sentences:" for intent_data in intents.values(): - for sentence in intent_data['sentences']: + for sentence in intent_data["sentences"]: help_text += "\n'{}'".format(sentence) response.async_set_speech(help_text) diff --git a/homeassistant/components/hangouts/notify.py b/homeassistant/components/hangouts/notify.py index e88f80afbcd..01e4208fd48 100644 --- a/homeassistant/components/hangouts/notify.py +++ b/homeassistant/components/hangouts/notify.py @@ -4,17 +4,25 @@ import logging import voluptuous as vol from homeassistant.components.notify import ( - ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, PLATFORM_SCHEMA, - BaseNotificationService) + ATTR_DATA, + ATTR_MESSAGE, + ATTR_TARGET, + PLATFORM_SCHEMA, + BaseNotificationService, +) from .const import ( - CONF_DEFAULT_CONVERSATIONS, DOMAIN, SERVICE_SEND_MESSAGE, TARGETS_SCHEMA) + CONF_DEFAULT_CONVERSATIONS, + DOMAIN, + SERVICE_SEND_MESSAGE, + TARGETS_SCHEMA, +) _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEFAULT_CONVERSATIONS): [TARGETS_SCHEMA] -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_DEFAULT_CONVERSATIONS): [TARGETS_SCHEMA]} +) def get_service(hass, config, discovery_info=None): @@ -35,21 +43,19 @@ class HangoutsNotificationService(BaseNotificationService): if ATTR_TARGET in kwargs: target_conversations = [] for target in kwargs.get(ATTR_TARGET): - target_conversations.append({'id': target}) + target_conversations.append({"id": target}) else: target_conversations = self._default_conversations messages = [] - if 'title' in kwargs: - messages.append({'text': kwargs['title'], 'is_bold': True}) + if "title" in kwargs: + messages.append({"text": kwargs["title"], "is_bold": True}) - messages.append({'text': message, 'parse_str': True}) - service_data = { - ATTR_TARGET: target_conversations, - ATTR_MESSAGE: messages, - } + messages.append({"text": message, "parse_str": True}) + service_data = {ATTR_TARGET: target_conversations, ATTR_MESSAGE: messages} if kwargs[ATTR_DATA]: service_data[ATTR_DATA] = kwargs[ATTR_DATA] return self.hass.services.call( - DOMAIN, SERVICE_SEND_MESSAGE, service_data=service_data) + DOMAIN, SERVICE_SEND_MESSAGE, service_data=service_data + ) diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index dc200f39b9c..01948943adf 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -4,28 +4,36 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_TURN_ON, SUPPORT_SELECT_SOURCE) -from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) + SUPPORT_TURN_OFF, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_STEP, + SUPPORT_TURN_ON, + SUPPORT_SELECT_SOURCE, +) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Harman Kardon AVR' +DEFAULT_NAME = "Harman Kardon AVR" DEFAULT_PORT = 10025 -SUPPORT_HARMAN_KARDON_AVR = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ - SUPPORT_SELECT_SOURCE +SUPPORT_HARMAN_KARDON_AVR = ( + SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_MUTE + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_SELECT_SOURCE +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) def setup_platform(hass, config, add_entities, discover_info=None): diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index c4aebb1bdcb..b78f276bf28 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -7,10 +7,21 @@ import voluptuous as vol from homeassistant.components import remote from homeassistant.components.remote import ( - ATTR_ACTIVITY, ATTR_DELAY_SECS, ATTR_DEVICE, ATTR_HOLD_SECS, - ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, DOMAIN, PLATFORM_SCHEMA) + ATTR_ACTIVITY, + ATTR_DELAY_SECS, + ATTR_DEVICE, + ATTR_HOLD_SECS, + ATTR_NUM_REPEATS, + DEFAULT_DELAY_SECS, + DOMAIN, + PLATFORM_SCHEMA, +) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady @@ -18,37 +29,37 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -ATTR_CHANNEL = 'channel' -ATTR_CURRENT_ACTIVITY = 'current_activity' +ATTR_CHANNEL = "channel" +ATTR_CURRENT_ACTIVITY = "current_activity" DEFAULT_PORT = 8088 DEVICES = [] -CONF_DEVICE_CACHE = 'harmony_device_cache' +CONF_DEVICE_CACHE = "harmony_device_cache" -SERVICE_SYNC = 'harmony_sync' -SERVICE_CHANGE_CHANNEL = 'harmony_change_channel' +SERVICE_SYNC = "harmony_sync" +SERVICE_CHANGE_CHANNEL = "harmony_change_channel" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(ATTR_ACTIVITY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): - vol.Coerce(float), - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(ATTR_ACTIVITY): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float), + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) -HARMONY_SYNC_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) +HARMONY_SYNC_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) -HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_CHANNEL): cv.positive_int, -}) +HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_CHANNEL): cv.positive_int, + } +) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Harmony platform.""" activity = None @@ -57,9 +68,14 @@ async def async_setup_platform( if discovery_info: # Find the discovered device in the list of user configurations - override = next((c for c in hass.data[CONF_DEVICE_CACHE] - if c.get(CONF_NAME) == discovery_info.get(CONF_NAME)), - None) + override = next( + ( + c + for c in hass.data[CONF_DEVICE_CACHE] + if c.get(CONF_NAME) == discovery_info.get(CONF_NAME) + ), + None, + ) port = DEFAULT_PORT delay_secs = DEFAULT_DELAY_SECS @@ -68,21 +84,14 @@ async def async_setup_platform( delay_secs = override.get(ATTR_DELAY_SECS) port = override.get(CONF_PORT, DEFAULT_PORT) - host = ( - discovery_info.get(CONF_NAME), - discovery_info.get(CONF_HOST), - port) + host = (discovery_info.get(CONF_NAME), discovery_info.get(CONF_HOST), port) # Ignore hub name when checking if this hub is known - ip and port only if host[1:] in ((h.host, h.port) for h in DEVICES): _LOGGER.debug("Discovered host already known: %s", host) return elif CONF_HOST in config: - host = ( - config.get(CONF_NAME), - config.get(CONF_HOST), - config.get(CONF_PORT), - ) + host = (config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT)) activity = config.get(ATTR_ACTIVITY) delay_secs = config.get(ATTR_DELAY_SECS) else: @@ -90,14 +99,21 @@ async def async_setup_platform( return name, address, port = host - _LOGGER.info("Loading Harmony Platform: %s at %s:%s, startup activity: %s", - name, address, port, activity) + _LOGGER.info( + "Loading Harmony Platform: %s at %s:%s, startup activity: %s", + name, + address, + port, + activity, + ) harmony_conf_file = hass.config.path( - '{}{}{}'.format('harmony_', slugify(name), '.conf')) + "{}{}{}".format("harmony_", slugify(name), ".conf") + ) try: device = HarmonyRemote( - name, address, port, activity, harmony_conf_file, delay_secs) + name, address, port, activity, harmony_conf_file, delay_secs + ) if not await device.connect(): raise PlatformNotReady @@ -111,21 +127,23 @@ async def async_setup_platform( def register_services(hass): """Register all services for harmony devices.""" hass.services.async_register( - DOMAIN, SERVICE_SYNC, _sync_service, - schema=HARMONY_SYNC_SCHEMA) + DOMAIN, SERVICE_SYNC, _sync_service, schema=HARMONY_SYNC_SCHEMA + ) hass.services.async_register( - DOMAIN, SERVICE_CHANGE_CHANNEL, _change_channel_service, - schema=HARMONY_CHANGE_CHANNEL_SCHEMA) + DOMAIN, + SERVICE_CHANGE_CHANNEL, + _change_channel_service, + schema=HARMONY_CHANGE_CHANNEL_SCHEMA, + ) async def _apply_service(service, service_func, *service_func_args): """Handle services to apply.""" - entity_ids = service.data.get('entity_id') + entity_ids = service.data.get("entity_id") if entity_ids: - _devices = [device for device in DEVICES - if device.entity_id in entity_ids] + _devices = [device for device in DEVICES if device.entity_id in entity_ids] else: _devices = DEVICES @@ -170,7 +188,7 @@ class HarmonyRemote(remote.RemoteDevice): new_activity=self.new_activity, config_updated=self.new_config, connect=self.got_connected, - disconnect=self.got_disconnected + disconnect=self.got_disconnected, ) # Store Harmony HUB config, this will also update our current @@ -207,7 +225,7 @@ class HarmonyRemote(remote.RemoteDevice): @property def is_on(self): """Return False if PowerOff is the current activity, otherwise True.""" - return self._current_activity not in [None, 'PowerOff'] + return self._current_activity not in [None, "PowerOff"] @property def available(self): @@ -233,8 +251,7 @@ class HarmonyRemote(remote.RemoteDevice): def new_activity(self, activity_info: tuple) -> None: """Call for updating the current activity.""" activity_id, activity_name = activity_info - _LOGGER.debug("%s: activity reported as: %s", self._name, - activity_name) + _LOGGER.debug("%s: activity reported as: %s", self._name, activity_name) self._current_activity = activity_name self._state = bool(activity_id != -1) self._available = True @@ -275,34 +292,30 @@ class HarmonyRemote(remote.RemoteDevice): if activity: activity_id = None - if activity.isdigit() or activity == '-1': + if activity.isdigit() or activity == "-1": _LOGGER.debug("%s: Activity is numeric", self.name) if self._client.get_activity_name(int(activity)): activity_id = activity if activity_id is None: _LOGGER.debug("%s: Find activity ID based on name", self.name) - activity_id = self._client.get_activity_id( - str(activity).strip()) + activity_id = self._client.get_activity_id(str(activity).strip()) if activity_id is None: - _LOGGER.error("%s: Activity %s is invalid", - self.name, activity) + _LOGGER.error("%s: Activity %s is invalid", self.name, activity) return try: await self._client.start_activity(activity_id) except aioexc.TimeOut: - _LOGGER.error("%s: Starting activity %s timed-out", - self.name, - activity) + _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity) else: - _LOGGER.error("%s: No activity specified with turn_on service", - self.name) + _LOGGER.error("%s: No activity specified with turn_on service", self.name) async def async_turn_off(self, **kwargs): """Start the PowerOff activity.""" import aioharmony.exceptions as aioexc + _LOGGER.debug("%s: Turn Off", self.name) try: await self._client.power_off() @@ -323,14 +336,14 @@ class HarmonyRemote(remote.RemoteDevice): device_id = None if device.isdigit(): - _LOGGER.debug("%s: Device %s is numeric", - self.name, device) + _LOGGER.debug("%s: Device %s is numeric", self.name, device) if self._client.get_device_name(int(device)): device_id = device if device_id is None: - _LOGGER.debug("%s: Find device ID %s based on device name", - self.name, device) + _LOGGER.debug( + "%s: Find device ID %s based on device name", self.name, device + ) device_id = self._client.get_device_id(str(device).strip()) if device_id is None: @@ -340,18 +353,20 @@ class HarmonyRemote(remote.RemoteDevice): num_repeats = kwargs[ATTR_NUM_REPEATS] delay_secs = kwargs.get(ATTR_DELAY_SECS, self._delay_secs) hold_secs = kwargs[ATTR_HOLD_SECS] - _LOGGER.debug("Sending commands to device %s holding for %s seconds " - "with a delay of %s seconds", - device, hold_secs, delay_secs) + _LOGGER.debug( + "Sending commands to device %s holding for %s seconds " + "with a delay of %s seconds", + device, + hold_secs, + delay_secs, + ) # Creating list of commands to send. snd_cmnd_list = [] for _ in range(num_repeats): for single_command in command: send_command = SendCommandDevice( - device=device_id, - command=single_command, - delay=hold_secs + device=device_id, command=single_command, delay=hold_secs ) snd_cmnd_list.append(send_command) if delay_secs > 0: @@ -365,26 +380,23 @@ class HarmonyRemote(remote.RemoteDevice): return for result in result_list: - _LOGGER.error("Sending command %s to device %s failed with code " - "%s: %s", - result.command.command, - result.command.device, - result.code, - result.msg - ) + _LOGGER.error( + "Sending command %s to device %s failed with code " "%s: %s", + result.command.command, + result.command.device, + result.code, + result.msg, + ) async def change_channel(self, channel): """Change the channel using Harmony remote.""" import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Changing channel to %s", - self.name, channel) + _LOGGER.debug("%s: Changing channel to %s", self.name, channel) try: await self._client.change_channel(channel) except aioexc.TimeOut: - _LOGGER.error("%s: Changing channel to %s timed-out", - self.name, - channel) + _LOGGER.error("%s: Changing channel to %s timed-out", self.name, channel) async def sync(self): """Sync the Harmony device with the web service.""" @@ -394,25 +406,26 @@ class HarmonyRemote(remote.RemoteDevice): try: await self._client.sync() except aioexc.TimeOut: - _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", - self.name) + _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name) else: await self.hass.async_add_executor_job(self.write_config_file) def write_config_file(self): """Write Harmony configuration file.""" - _LOGGER.debug("%s: Writing hub config to file: %s", - self.name, - self._config_path) + _LOGGER.debug( + "%s: Writing hub config to file: %s", self.name, self._config_path + ) if self._client.config is None: - _LOGGER.warning("%s: No configuration received from hub", - self.name) + _LOGGER.warning("%s: No configuration received from hub", self.name) return try: - with open(self._config_path, 'w+', encoding='utf-8') as file_out: - json.dump(self._client.json_config, file_out, - sort_keys=True, indent=4) + with open(self._config_path, "w+", encoding="utf-8") as file_out: + json.dump(self._client.json_config, file_out, sort_keys=True, indent=4) except IOError as exc: - _LOGGER.error("%s: Unable to write HUB configuration to %s: %s", - self.name, self._config_path, exc) + _LOGGER.error( + "%s: Unable to write HUB configuration to %s: %s", + self.name, + self._config_path, + exc, + ) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 7e8afdc5312..801c20b5c2b 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -9,8 +9,11 @@ from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG import homeassistant.config as conf_util from homeassistant.const import ( - ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, - EVENT_CORE_CONFIG_UPDATE) + ATTR_NAME, + SERVICE_HOMEASSISTANT_RESTART, + SERVICE_HOMEASSISTANT_STOP, + EVENT_CORE_CONFIG_UPDATE, +) from homeassistant.core import DOMAIN as HASS_DOMAIN, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -26,90 +29,97 @@ from .ingress import async_setup_ingress_view _LOGGER = logging.getLogger(__name__) -DOMAIN = 'hassio' +DOMAIN = "hassio" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -CONF_FRONTEND_REPO = 'development_repo' +CONF_FRONTEND_REPO = "development_repo" -CONFIG_SCHEMA = vol.Schema({ - vol.Optional(DOMAIN): vol.Schema({ - vol.Optional(CONF_FRONTEND_REPO): cv.isdir, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): vol.Schema({vol.Optional(CONF_FRONTEND_REPO): cv.isdir})}, + extra=vol.ALLOW_EXTRA, +) -DATA_HOMEASSISTANT_VERSION = 'hassio_hass_version' +DATA_HOMEASSISTANT_VERSION = "hassio_hass_version" HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) -SERVICE_ADDON_START = 'addon_start' -SERVICE_ADDON_STOP = 'addon_stop' -SERVICE_ADDON_RESTART = 'addon_restart' -SERVICE_ADDON_STDIN = 'addon_stdin' -SERVICE_HOST_SHUTDOWN = 'host_shutdown' -SERVICE_HOST_REBOOT = 'host_reboot' -SERVICE_SNAPSHOT_FULL = 'snapshot_full' -SERVICE_SNAPSHOT_PARTIAL = 'snapshot_partial' -SERVICE_RESTORE_FULL = 'restore_full' -SERVICE_RESTORE_PARTIAL = 'restore_partial' +SERVICE_ADDON_START = "addon_start" +SERVICE_ADDON_STOP = "addon_stop" +SERVICE_ADDON_RESTART = "addon_restart" +SERVICE_ADDON_STDIN = "addon_stdin" +SERVICE_HOST_SHUTDOWN = "host_shutdown" +SERVICE_HOST_REBOOT = "host_reboot" +SERVICE_SNAPSHOT_FULL = "snapshot_full" +SERVICE_SNAPSHOT_PARTIAL = "snapshot_partial" +SERVICE_RESTORE_FULL = "restore_full" +SERVICE_RESTORE_PARTIAL = "restore_partial" -ATTR_ADDON = 'addon' -ATTR_INPUT = 'input' -ATTR_SNAPSHOT = 'snapshot' -ATTR_ADDONS = 'addons' -ATTR_FOLDERS = 'folders' -ATTR_HOMEASSISTANT = 'homeassistant' -ATTR_PASSWORD = 'password' +ATTR_ADDON = "addon" +ATTR_INPUT = "input" +ATTR_SNAPSHOT = "snapshot" +ATTR_ADDONS = "addons" +ATTR_FOLDERS = "folders" +ATTR_HOMEASSISTANT = "homeassistant" +ATTR_PASSWORD = "password" SCHEMA_NO_DATA = vol.Schema({}) -SCHEMA_ADDON = vol.Schema({ - vol.Required(ATTR_ADDON): cv.slug, -}) +SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): cv.slug}) -SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend({ - vol.Required(ATTR_INPUT): vol.Any(dict, cv.string) -}) +SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend( + {vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)} +) -SCHEMA_SNAPSHOT_FULL = vol.Schema({ - vol.Optional(ATTR_NAME): cv.string, - vol.Optional(ATTR_PASSWORD): cv.string, -}) +SCHEMA_SNAPSHOT_FULL = vol.Schema( + {vol.Optional(ATTR_NAME): cv.string, vol.Optional(ATTR_PASSWORD): cv.string} +) -SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend({ - vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), -}) +SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend( + { + vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), + } +) -SCHEMA_RESTORE_FULL = vol.Schema({ - vol.Required(ATTR_SNAPSHOT): cv.slug, - vol.Optional(ATTR_PASSWORD): cv.string, -}) +SCHEMA_RESTORE_FULL = vol.Schema( + {vol.Required(ATTR_SNAPSHOT): cv.slug, vol.Optional(ATTR_PASSWORD): cv.string} +) -SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend({ - vol.Optional(ATTR_HOMEASSISTANT): cv.boolean, - vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), -}) +SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( + { + vol.Optional(ATTR_HOMEASSISTANT): cv.boolean, + vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), + } +) MAP_SERVICE_API = { - SERVICE_ADDON_START: ('/addons/{addon}/start', SCHEMA_ADDON, 60, False), - SERVICE_ADDON_STOP: ('/addons/{addon}/stop', SCHEMA_ADDON, 60, False), - SERVICE_ADDON_RESTART: - ('/addons/{addon}/restart', SCHEMA_ADDON, 60, False), - SERVICE_ADDON_STDIN: - ('/addons/{addon}/stdin', SCHEMA_ADDON_STDIN, 60, False), - SERVICE_HOST_SHUTDOWN: ('/host/shutdown', SCHEMA_NO_DATA, 60, False), - SERVICE_HOST_REBOOT: ('/host/reboot', SCHEMA_NO_DATA, 60, False), - SERVICE_SNAPSHOT_FULL: - ('/snapshots/new/full', SCHEMA_SNAPSHOT_FULL, 300, True), - SERVICE_SNAPSHOT_PARTIAL: - ('/snapshots/new/partial', SCHEMA_SNAPSHOT_PARTIAL, 300, True), - SERVICE_RESTORE_FULL: - ('/snapshots/{snapshot}/restore/full', SCHEMA_RESTORE_FULL, 300, True), - SERVICE_RESTORE_PARTIAL: - ('/snapshots/{snapshot}/restore/partial', SCHEMA_RESTORE_PARTIAL, 300, - True), + SERVICE_ADDON_START: ("/addons/{addon}/start", SCHEMA_ADDON, 60, False), + SERVICE_ADDON_STOP: ("/addons/{addon}/stop", SCHEMA_ADDON, 60, False), + SERVICE_ADDON_RESTART: ("/addons/{addon}/restart", SCHEMA_ADDON, 60, False), + SERVICE_ADDON_STDIN: ("/addons/{addon}/stdin", SCHEMA_ADDON_STDIN, 60, False), + SERVICE_HOST_SHUTDOWN: ("/host/shutdown", SCHEMA_NO_DATA, 60, False), + SERVICE_HOST_REBOOT: ("/host/reboot", SCHEMA_NO_DATA, 60, False), + SERVICE_SNAPSHOT_FULL: ("/snapshots/new/full", SCHEMA_SNAPSHOT_FULL, 300, True), + SERVICE_SNAPSHOT_PARTIAL: ( + "/snapshots/new/partial", + SCHEMA_SNAPSHOT_PARTIAL, + 300, + True, + ), + SERVICE_RESTORE_FULL: ( + "/snapshots/{snapshot}/restore/full", + SCHEMA_RESTORE_FULL, + 300, + True, + ), + SERVICE_RESTORE_PARTIAL: ( + "/snapshots/{snapshot}/restore/partial", + SCHEMA_RESTORE_PARTIAL, + 300, + True, + ), } @@ -136,13 +146,13 @@ def is_hassio(hass): async def async_setup(hass, config): """Set up the Hass.io component.""" # Check local setup - for env in ('HASSIO', 'HASSIO_TOKEN'): + for env in ("HASSIO", "HASSIO_TOKEN"): if os.environ.get(env): continue _LOGGER.error("Missing %s environment variable.", env) return False - host = os.environ['HASSIO'] + host = os.environ["HASSIO"] websession = hass.helpers.aiohttp_client.async_get_clientsession() hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host) @@ -156,44 +166,42 @@ async def async_setup(hass, config): data = {} refresh_token = None - if 'hassio_user' in data: - user = await hass.auth.async_get_user(data['hassio_user']) + if "hassio_user" in data: + user = await hass.auth.async_get_user(data["hassio_user"]) if user and user.refresh_tokens: refresh_token = list(user.refresh_tokens.values())[0] # Migrate old hass.io users to be admin. if not user.is_admin: - await hass.auth.async_update_user( - user, group_ids=[GROUP_ID_ADMIN]) + await hass.auth.async_update_user(user, group_ids=[GROUP_ID_ADMIN]) if refresh_token is None: - user = await hass.auth.async_create_system_user( - 'Hass.io', [GROUP_ID_ADMIN]) + user = await hass.auth.async_create_system_user("Hass.io", [GROUP_ID_ADMIN]) refresh_token = await hass.auth.async_create_refresh_token(user) - data['hassio_user'] = user.id + data["hassio_user"] = user.id await store.async_save(data) # This overrides the normal API call that would be forwarded development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO) if development_repo is not None: hass.http.register_static_path( - '/api/hassio/app', - os.path.join(development_repo, 'hassio/build'), False) + "/api/hassio/app", os.path.join(development_repo, "hassio/build"), False + ) hass.http.register_view(HassIOView(host, websession)) - if 'frontend' in hass.config.components: + if "frontend" in hass.config.components: await hass.components.panel_custom.async_register_panel( - frontend_url_path='hassio', - webcomponent_name='hassio-main', - sidebar_title='Hass.io', - sidebar_icon='hass:home-assistant', - js_url='/api/hassio/app/entrypoint.js', + frontend_url_path="hassio", + webcomponent_name="hassio-main", + sidebar_title="Hass.io", + sidebar_icon="hass:home-assistant", + js_url="/api/hassio/app/entrypoint.js", embed_iframe=True, require_admin=True, ) - await hassio.update_hass_api(config.get('http', {}), refresh_token.token) + await hassio.update_hass_api(config.get("http", {}), refresh_token.token) async def push_config(_): """Push core config to Hass.io.""" @@ -221,25 +229,28 @@ async def async_setup(hass, config): try: await hassio.send_command( api_command.format(addon=addon, snapshot=snapshot), - payload=payload, timeout=MAP_SERVICE_API[service.service][2] + payload=payload, + timeout=MAP_SERVICE_API[service.service][2], ) except HassioAPIError as err: _LOGGER.error("Error on Hass.io API: %s", err) for service, settings in MAP_SERVICE_API.items(): hass.services.async_register( - DOMAIN, service, async_service_handler, schema=settings[1]) + DOMAIN, service, async_service_handler, schema=settings[1] + ) async def update_homeassistant_version(now): """Update last available Home Assistant version.""" try: data = await hassio.get_homeassistant_info() - hass.data[DATA_HOMEASSISTANT_VERSION] = data['last_version'] + hass.data[DATA_HOMEASSISTANT_VERSION] = data["last_version"] except HassioAPIError as err: _LOGGER.warning("Can't read last version: %s", err) hass.helpers.event.async_track_point_in_utc_time( - update_homeassistant_version, utcnow() + HASSIO_UPDATE_INTERVAL) + update_homeassistant_version, utcnow() + HASSIO_UPDATE_INTERVAL + ) # Fetch last version await update_homeassistant_version(None) @@ -259,17 +270,21 @@ async def async_setup(hass, config): _LOGGER.error(errors) hass.components.persistent_notification.async_create( "Config error. See dev-info panel for details.", - "Config validating", "{0}.check_config".format(HASS_DOMAIN)) + "Config validating", + "{0}.check_config".format(HASS_DOMAIN), + ) return if call.service == SERVICE_HOMEASSISTANT_RESTART: await hassio.restart_homeassistant() # Mock core services - for service in (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, - SERVICE_CHECK_CONFIG): - hass.services.async_register( - HASS_DOMAIN, service, async_handle_core_service) + for service in ( + SERVICE_HOMEASSISTANT_STOP, + SERVICE_HOMEASSISTANT_RESTART, + SERVICE_CHECK_CONFIG, + ): + hass.services.async_register(HASS_DOMAIN, service, async_handle_core_service) # Init discovery Hass.io feature async_setup_discovery_view(hass, hassio) diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index e85c8f12247..b60c864a893 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -81,13 +81,11 @@ def _register_panel(hass, addon, data): """ return hass.components.panel_custom.async_register_panel( frontend_url_path=addon, - webcomponent_name='hassio-main', + webcomponent_name="hassio-main", sidebar_title=data[ATTR_TITLE], sidebar_icon=data[ATTR_ICON], - js_url='/api/hassio/app/entrypoint.js', + js_url="/api/hassio/app/entrypoint.js", embed_iframe=True, require_admin=data[ATTR_ADMIN], - config={ - "ingress": addon - } + config={"ingress": addon}, ) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 85ae6473562..19e4c63b5a5 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -20,11 +20,14 @@ from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME _LOGGER = logging.getLogger(__name__) -SCHEMA_API_AUTH = vol.Schema({ - vol.Required(ATTR_USERNAME): cv.string, - vol.Required(ATTR_PASSWORD): cv.string, - vol.Required(ATTR_ADDON): cv.string, -}, extra=vol.ALLOW_EXTRA) +SCHEMA_API_AUTH = vol.Schema( + { + vol.Required(ATTR_USERNAME): cv.string, + vol.Required(ATTR_PASSWORD): cv.string, + vol.Required(ATTR_ADDON): cv.string, + }, + extra=vol.ALLOW_EXTRA, +) @callback @@ -47,10 +50,9 @@ class HassIOAuth(HomeAssistantView): @RequestDataValidator(SCHEMA_API_AUTH) async def post(self, request, data): """Handle new discovery requests.""" - hassio_ip = os.environ['HASSIO'].split(':')[0] + hassio_ip = os.environ["HASSIO"].split(":")[0] if request[KEY_REAL_IP] != ip_address(hassio_ip): - _LOGGER.error( - "Invalid auth request from %s", request[KEY_REAL_IP]) + _LOGGER.error("Invalid auth request from %s", request[KEY_REAL_IP]) raise HTTPForbidden() await self._check_login(data[ATTR_USERNAME], data[ATTR_PASSWORD]) @@ -58,7 +60,7 @@ class HassIOAuth(HomeAssistantView): def _get_provider(self): """Return Homeassistant auth provider.""" - prv = self.hass.auth.get_auth_provider('homeassistant', None) + prv = self.hass.auth.get_auth_provider("homeassistant", None) if prv is not None: return prv diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 9656346cd2c..ffccb325395 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -1,21 +1,21 @@ """Hass.io const variables.""" -ATTR_ADDONS = 'addons' -ATTR_DISCOVERY = 'discovery' -ATTR_ADDON = 'addon' -ATTR_NAME = 'name' -ATTR_SERVICE = 'service' -ATTR_CONFIG = 'config' -ATTR_UUID = 'uuid' -ATTR_USERNAME = 'username' -ATTR_PASSWORD = 'password' -ATTR_PANELS = 'panels' -ATTR_ENABLE = 'enable' -ATTR_TITLE = 'title' -ATTR_ICON = 'icon' -ATTR_ADMIN = 'admin' +ATTR_ADDONS = "addons" +ATTR_DISCOVERY = "discovery" +ATTR_ADDON = "addon" +ATTR_NAME = "name" +ATTR_SERVICE = "service" +ATTR_CONFIG = "config" +ATTR_UUID = "uuid" +ATTR_USERNAME = "username" +ATTR_PASSWORD = "password" +ATTR_PANELS = "panels" +ATTR_ENABLE = "enable" +ATTR_TITLE = "title" +ATTR_ICON = "icon" +ATTR_ADMIN = "admin" -X_HASSIO = 'X-Hassio-Key' +X_HASSIO = "X-Hassio-Key" X_INGRESS_PATH = "X-Ingress-Path" -X_HASS_USER_ID = 'X-Hass-User-ID' -X_HASS_IS_ADMIN = 'X-Hass-Is-Admin' +X_HASS_USER_ID = "X-Hass-User-ID" +X_HASS_IS_ADMIN = "X-Hass-Is-Admin" diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 90953d634c3..55336735133 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -10,8 +10,13 @@ from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from .const import ( - ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_NAME, ATTR_SERVICE, - ATTR_UUID) + ATTR_ADDON, + ATTR_CONFIG, + ATTR_DISCOVERY, + ATTR_NAME, + ATTR_SERVICE, + ATTR_UUID, +) from .handler import HassioAPIError _LOGGER = logging.getLogger(__name__) @@ -32,13 +37,16 @@ def async_setup_discovery_view(hass: HomeAssistantView, hassio): _LOGGER.error("Can't read discover info: %s", err) return - jobs = [hassio_discovery.async_process_new(discovery) - for discovery in data[ATTR_DISCOVERY]] + jobs = [ + hassio_discovery.async_process_new(discovery) + for discovery in data[ATTR_DISCOVERY] + ] if jobs: await asyncio.wait(jobs) hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, _async_discovery_start_handler) + EVENT_HOMEASSISTANT_START, _async_discovery_start_handler + ) class HassIODiscovery(HomeAssistantView): @@ -86,7 +94,8 @@ class HassIODiscovery(HomeAssistantView): # Use config flow await self.hass.config_entries.flow.async_init( - service, context={'source': 'hassio'}, data=config_data) + service, context={"source": "hassio"}, data=config_data + ) async def async_process_del(self, data): """Process remove discovery entry.""" @@ -104,6 +113,6 @@ class HassIODiscovery(HomeAssistantView): # Use config flow for entry in self.hass.config_entries.async_entries(service): - if entry.source != 'hassio': + if entry.source != "hassio": continue await self.hass.config_entries.async_remove(entry) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 5e7932acbae..10f21556fb3 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -24,11 +24,12 @@ class HassioAPIError(RuntimeError): def _api_bool(funct): """Return a boolean.""" + async def _wrapper(*argv, **kwargs): """Wrap function.""" try: data = await funct(*argv, **kwargs) - return data['result'] == "ok" + return data["result"] == "ok" except HassioAPIError: return False @@ -37,12 +38,13 @@ def _api_bool(funct): def _api_data(funct): """Return data of an api.""" + async def _wrapper(*argv, **kwargs): """Wrap function.""" data = await funct(*argv, **kwargs) - if data['result'] == "ok": - return data['data'] - raise HassioAPIError(data['message']) + if data["result"] == "ok": + return data["data"] + raise HassioAPIError(data["message"]) return _wrapper @@ -78,8 +80,7 @@ class HassIO: This method return a coroutine. """ - return self.send_command( - "/addons/{}/info".format(addon), method="get") + return self.send_command("/addons/{}/info".format(addon), method="get") @_api_data def get_ingress_panels(self): @@ -126,18 +127,17 @@ class HassIO: """Update Home Assistant API data on Hass.io.""" port = http_config.get(CONF_SERVER_PORT) or SERVER_PORT options = { - 'ssl': CONF_SSL_CERTIFICATE in http_config, - 'port': port, - 'watchdog': True, - 'refresh_token': refresh_token, + "ssl": CONF_SSL_CERTIFICATE in http_config, + "port": port, + "watchdog": True, + "refresh_token": refresh_token, } if CONF_SERVER_HOST in http_config: - options['watchdog'] = False + options["watchdog"] = False _LOGGER.warning("Don't use 'server_host' options with Hass.io") - return await self.send_command("/homeassistant/options", - payload=options) + return await self.send_command("/homeassistant/options", payload=options) @_api_bool def update_hass_timezone(self, timezone): @@ -145,12 +145,9 @@ class HassIO: This method return a coroutine. """ - return self.send_command("/supervisor/options", payload={ - 'timezone': timezone - }) + return self.send_command("/supervisor/options", payload={"timezone": timezone}) - async def send_command(self, command, method="post", payload=None, - timeout=10): + async def send_command(self, command, method="post", payload=None, timeout=10): """Send API command to Hass.io. This method is a coroutine. @@ -158,14 +155,14 @@ class HassIO: try: with async_timeout.timeout(timeout): request = await self.websession.request( - method, "http://{}{}".format(self._ip, command), - json=payload, headers={ - X_HASSIO: os.environ.get('HASSIO_TOKEN', "") - }) + method, + "http://{}{}".format(self._ip, command), + json=payload, + headers={X_HASSIO: os.environ.get("HASSIO_TOKEN", "")}, + ) if request.status not in (200, 400): - _LOGGER.error( - "%s return code %d.", command, request.status) + _LOGGER.error("%s return code %d.", command, request.status) raise HassioAPIError() answer = await request.json() diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index a9c5deda9f9..f42aaca4438 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -19,24 +19,19 @@ _LOGGER = logging.getLogger(__name__) NO_TIMEOUT = re.compile( - r'^(?:' - r'|homeassistant/update' - r'|hassos/update' - r'|hassos/update/cli' - r'|supervisor/update' - r'|addons/[^/]+/(?:update|install|rebuild)' - r'|snapshots/.+/full' - r'|snapshots/.+/partial' - r'|snapshots/[^/]+/(?:upload|download)' - r')$' + r"^(?:" + r"|homeassistant/update" + r"|hassos/update" + r"|hassos/update/cli" + r"|supervisor/update" + r"|addons/[^/]+/(?:update|install|rebuild)" + r"|snapshots/.+/full" + r"|snapshots/.+/partial" + r"|snapshots/[^/]+/(?:upload|download)" + r")$" ) -NO_AUTH = re.compile( - r'^(?:' - r'|app/.*' - r'|addons/[^/]+/logo' - r')$' -) +NO_AUTH = re.compile(r"^(?:" r"|app/.*" r"|addons/[^/]+/logo" r")$") class HassIOView(HomeAssistantView): @@ -52,7 +47,7 @@ class HassIOView(HomeAssistantView): self._websession = websession async def _handle( - self, request: web.Request, path: str + self, request: web.Request, path: str ) -> Union[web.Response, web.StreamResponse]: """Route data to Hass.io.""" if _need_auth(path) and not request[KEY_AUTHENTICATED]: @@ -64,7 +59,7 @@ class HassIOView(HomeAssistantView): post = _handle async def _command_proxy( - self, path: str, request: web.Request + self, path: str, request: web.Request ) -> Union[web.Response, web.StreamResponse]: """Return a client request with proxy origin for Hass.io supervisor. @@ -80,8 +75,10 @@ class HassIOView(HomeAssistantView): method = getattr(self._websession, request.method.lower()) client = await method( - "http://{}/{}".format(self._host, path), data=data, - headers=headers, timeout=read_timeout + "http://{}/{}".format(self._host, path), + data=data, + headers=headers, + timeout=read_timeout, ) # Simple request @@ -89,9 +86,7 @@ class HassIOView(HomeAssistantView): # Return Response body = await client.read() return web.Response( - content_type=client.content_type, - status=client.status, - body=body, + content_type=client.content_type, status=client.status, body=body ) # Stream response @@ -116,15 +111,15 @@ class HassIOView(HomeAssistantView): def _init_header(request: web.Request) -> Dict[str, str]: """Create initial header.""" headers = { - X_HASSIO: os.environ.get('HASSIO_TOKEN', ""), + X_HASSIO: os.environ.get("HASSIO_TOKEN", ""), CONTENT_TYPE: request.content_type, } # Add user data - user = request.get('hass_user') + user = request.get("hass_user") if user is not None: - headers[X_HASS_USER_ID] = request['hass_user'].id - headers[X_HASS_IS_ADMIN] = str(int(request['hass_user'].is_admin)) + headers[X_HASS_USER_ID] = request["hass_user"].id + headers[X_HASS_IS_ADMIN] = str(int(request["hass_user"].is_admin)) return headers diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 250d50681dc..84e2b096362 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -45,7 +45,7 @@ class HassIOIngress(HomeAssistantView): return "http://{}/ingress/{}/{}".format(self._host, token, path) async def _handle( - self, request: web.Request, token: str, path: str + self, request: web.Request, token: str, path: str ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: """Route data to Hass.io ingress service.""" try: @@ -69,14 +69,13 @@ class HassIOIngress(HomeAssistantView): options = _handle async def _handle_websocket( - self, request: web.Request, token: str, path: str + self, request: web.Request, token: str, path: str ) -> web.WebSocketResponse: """Ingress route for websocket.""" if hdrs.SEC_WEBSOCKET_PROTOCOL in request.headers: req_protocols = [ str(proto.strip()) - for proto in - request.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",") + for proto in request.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",") ] else: req_protocols = () @@ -96,8 +95,11 @@ class HassIOIngress(HomeAssistantView): # Start proxy async with self._websession.ws_connect( - url, headers=source_header, protocols=req_protocols, - autoclose=False, autoping=False, + url, + headers=source_header, + protocols=req_protocols, + autoclose=False, + autoping=False, ) as ws_client: # Proxy requests await asyncio.wait( @@ -105,13 +107,13 @@ class HassIOIngress(HomeAssistantView): _websocket_forward(ws_server, ws_client), _websocket_forward(ws_client, ws_server), ], - return_when=asyncio.FIRST_COMPLETED + return_when=asyncio.FIRST_COMPLETED, ) return ws_server async def _handle_request( - self, request: web.Request, token: str, path: str + self, request: web.Request, token: str, path: str ) -> Union[web.Response, web.StreamResponse]: """Ingress route for request.""" url = self._create_url(token, path) @@ -119,30 +121,31 @@ class HassIOIngress(HomeAssistantView): source_header = _init_header(request, token) async with self._websession.request( - request.method, - url, - headers=source_header, - params=request.query, - allow_redirects=False, - data=data + request.method, + url, + headers=source_header, + params=request.query, + allow_redirects=False, + data=data, ) as result: headers = _response_header(result) # Simple request - if hdrs.CONTENT_LENGTH in result.headers and \ - int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000: + if ( + hdrs.CONTENT_LENGTH in result.headers + and int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000 + ): # Return Response body = await result.read() return web.Response( headers=headers, status=result.status, content_type=result.content_type, - body=body + body=body, ) # Stream response - response = web.StreamResponse( - status=result.status, headers=headers) + response = web.StreamResponse(status=result.status, headers=headers) response.content_type = result.content_type try: @@ -157,7 +160,7 @@ class HassIOIngress(HomeAssistantView): def _init_header( - request: web.Request, token: str + request: web.Request, token: str ) -> Union[CIMultiDict, Dict[str, str]]: """Create initial header.""" headers = {} @@ -169,14 +172,14 @@ def _init_header( headers[name] = value # Inject token / cleanup later on Supervisor - headers[X_HASSIO] = os.environ.get('HASSIO_TOKEN', "") + headers[X_HASSIO] = os.environ.get("HASSIO_TOKEN", "") # Ingress information headers[X_INGRESS_PATH] = "/api/hassio_ingress/{}".format(token) # Set X-Forwarded-For forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) - connected_ip = ip_address(request.transport.get_extra_info('peername')[0]) + connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) if forward_for: forward_for = "{}, {!s}".format(forward_for, connected_ip) else: @@ -203,8 +206,12 @@ def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]: headers = {} for name, value in response.headers.items(): - if name in (hdrs.TRANSFER_ENCODING, hdrs.CONTENT_LENGTH, - hdrs.CONTENT_TYPE, hdrs.CONTENT_ENCODING): + if name in ( + hdrs.TRANSFER_ENCODING, + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_TYPE, + hdrs.CONTENT_ENCODING, + ): continue headers[name] = value @@ -215,8 +222,10 @@ def _is_websocket(request: web.Request) -> bool: """Return True if request is a websocket.""" headers = request.headers - if "upgrade" in headers.get(hdrs.CONNECTION, "").lower() and \ - headers.get(hdrs.UPGRADE, "").lower() == "websocket": + if ( + "upgrade" in headers.get(hdrs.CONNECTION, "").lower() + and headers.get(hdrs.UPGRADE, "").lower() == "websocket" + ): return True return False diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 72cc5ced3ef..d78756b9543 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -25,11 +25,11 @@ HA_USER_AGENT = "Home Assistant HaveIBeenPwned Sensor Component" MIN_TIME_BETWEEN_FORCED_UPDATES = timedelta(seconds=5) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) -URL = 'https://haveibeenpwned.com/api/v2/breachedaccount/' +URL = "https://haveibeenpwned.com/api/v2/breachedaccount/" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_EMAIL): vol.All(cv.ensure_list, [cv.string]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_EMAIL): vol.All(cv.ensure_list, [cv.string])} +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -77,11 +77,13 @@ class HaveIBeenPwnedSensor(Entity): return val for idx, value in enumerate(self._data.data[self._email]): - tmpname = "breach {}".format(idx+1) + tmpname = "breach {}".format(idx + 1) tmpvalue = "{} {}".format( value["Title"], - dt_util.as_local(dt_util.parse_datetime( - value["AddedDate"])).strftime(DATE_STR_FORMAT)) + dt_util.as_local(dt_util.parse_datetime(value["AddedDate"])).strftime( + DATE_STR_FORMAT + ), + ) val[tmpname] = tmpvalue return val @@ -103,8 +105,10 @@ class HaveIBeenPwnedSensor(Entity): # normal using update if self._email not in self._data.data: track_point_in_time( - self.hass, self.update_nothrottle, - dt_util.now() + MIN_TIME_BETWEEN_FORCED_UPDATES) + self.hass, + self.update_nothrottle, + dt_util.now() + MIN_TIME_BETWEEN_FORCED_UPDATES, + ) return self._state = len(self._data.data[self._email]) @@ -147,17 +151,20 @@ class HaveIBeenPwnedData: _LOGGER.debug("Checking for breaches for email: %s", self._email) req = requests.get( - url, headers={USER_AGENT: HA_USER_AGENT}, allow_redirects=True, - timeout=5) + url, + headers={USER_AGENT: HA_USER_AGENT}, + allow_redirects=True, + timeout=5, + ) except requests.exceptions.RequestException: _LOGGER.error("Failed fetching data for %s", self._email) return if req.status_code == 200: - self.data[self._email] = sorted(req.json(), - key=lambda k: k["AddedDate"], - reverse=True) + self.data[self._email] = sorted( + req.json(), key=lambda k: k["AddedDate"], reverse=True + ) # Only goto next email if we had data so that # the forced updates try this current email again @@ -171,6 +178,8 @@ class HaveIBeenPwnedData: self.set_next_email() else: - _LOGGER.error("Failed fetching data for %s" - "(HTTP Status_code = %d)", self._email, - req.status_code) + _LOGGER.error( + "Failed fetching data for %s" "(HTTP Status_code = %d)", + self._email, + req.status_code, + ) diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 6d96f244f58..fa3b5fd256c 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -9,27 +9,35 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_DISKS) + CONF_NAME, + CONF_HOST, + CONF_PORT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + CONF_DISKS, +) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_DEVICE = 'device' -ATTR_MODEL = 'model' +ATTR_DEVICE = "device" +ATTR_MODEL = "model" -DEFAULT_HOST = 'localhost' +DEFAULT_HOST = "localhost" DEFAULT_PORT = 7634 -DEFAULT_NAME = 'HD Temperature' +DEFAULT_NAME = "HD Temperature" DEFAULT_TIMEOUT = 5 SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_DISKS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DISKS, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -43,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hddtemp.update() if not disks: - disks = [next(iter(hddtemp.data)).split('|')[0]] + disks = [next(iter(hddtemp.data)).split("|")[0]] dev = [] for disk in disks: @@ -59,7 +67,7 @@ class HddTempSensor(Entity): """Initialize a HDDTemp sensor.""" self.hddtemp = hddtemp self.disk = disk - self._name = '{} {}'.format(name, disk) + self._name = "{} {}".format(name, disk) self._state = None self._details = None self._unit = None @@ -83,19 +91,16 @@ class HddTempSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" if self._details is not None: - return { - ATTR_DEVICE: self._details[0], - ATTR_MODEL: self._details[1], - } + return {ATTR_DEVICE: self._details[0], ATTR_MODEL: self._details[1]} def update(self): """Get the latest data from HDDTemp daemon and updates the state.""" self.hddtemp.update() if self.hddtemp.data and self.disk in self.hddtemp.data: - self._details = self.hddtemp.data[self.disk].split('|') + self._details = self.hddtemp.data[self.disk].split("|") self._state = self._details[2] - if self._details is not None and self._details[3] == 'F': + if self._details is not None and self._details[3] == "F": self._unit = TEMP_FAHRENHEIT else: self._unit = TEMP_CELSIUS @@ -115,15 +120,17 @@ class HddTempData: def update(self): """Get the latest data from HDDTemp running as daemon.""" try: - connection = Telnet( - host=self.host, port=self.port, timeout=DEFAULT_TIMEOUT) - data = connection.read_all().decode( - 'ascii').lstrip('|').rstrip('|').split('||') - self.data = {data[i].split('|')[0]: data[i] - for i in range(0, len(data), 1)} + connection = Telnet(host=self.host, port=self.port, timeout=DEFAULT_TIMEOUT) + data = ( + connection.read_all() + .decode("ascii") + .lstrip("|") + .rstrip("|") + .split("||") + ) + self.data = {data[i].split("|")[0]: data[i] for i in range(0, len(data), 1)} except ConnectionRefusedError: - _LOGGER.error("HDDTemp is not available at %s:%s", - self.host, self.port) + _LOGGER.error("HDDTemp is not available at %s:%s", self.host, self.port) self.data = None except socket.gaierror: _LOGGER.error("HDDTemp host not found %s:%s", self.host, self.port) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 189cc748d5d..969925182fd 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -9,27 +9,35 @@ import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( - CONF_DEVICES, CONF_HOST, CONF_PLATFORM, EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, - STATE_PLAYING) + CONF_DEVICES, + CONF_HOST, + CONF_PLATFORM, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -DOMAIN = 'hdmi_cec' +DOMAIN = "hdmi_cec" _LOGGER = logging.getLogger(__name__) DEFAULT_DISPLAY_NAME = "HA" -CONF_TYPES = 'types' +CONF_TYPES = "types" -ICON_UNKNOWN = 'mdi:help' -ICON_AUDIO = 'mdi:speaker' -ICON_PLAYER = 'mdi:play' -ICON_TUNER = 'mdi:radio' -ICON_RECORDER = 'mdi:microphone' -ICON_TV = 'mdi:television' +ICON_UNKNOWN = "mdi:help" +ICON_AUDIO = "mdi:speaker" +ICON_PLAYER = "mdi:play" +ICON_TUNER = "mdi:radio" +ICON_RECORDER = "mdi:microphone" +ICON_TV = "mdi:television" ICONS_BY_TYPE = { 0: ICON_TV, 1: ICON_RECORDER, @@ -40,85 +48,100 @@ ICONS_BY_TYPE = { CEC_DEVICES = defaultdict(list) -CMD_UP = 'up' -CMD_DOWN = 'down' -CMD_MUTE = 'mute' -CMD_UNMUTE = 'unmute' -CMD_MUTE_TOGGLE = 'toggle mute' -CMD_PRESS = 'press' -CMD_RELEASE = 'release' +CMD_UP = "up" +CMD_DOWN = "down" +CMD_MUTE = "mute" +CMD_UNMUTE = "unmute" +CMD_MUTE_TOGGLE = "toggle mute" +CMD_PRESS = "press" +CMD_RELEASE = "release" -EVENT_CEC_COMMAND_RECEIVED = 'cec_command_received' -EVENT_CEC_KEYPRESS_RECEIVED = 'cec_keypress_received' +EVENT_CEC_COMMAND_RECEIVED = "cec_command_received" +EVENT_CEC_KEYPRESS_RECEIVED = "cec_keypress_received" -ATTR_PHYSICAL_ADDRESS = 'physical_address' -ATTR_TYPE_ID = 'type_id' -ATTR_VENDOR_NAME = 'vendor_name' -ATTR_VENDOR_ID = 'vendor_id' -ATTR_DEVICE = 'device' -ATTR_TYPE = 'type' -ATTR_KEY = 'key' -ATTR_DUR = 'dur' -ATTR_SRC = 'src' -ATTR_DST = 'dst' -ATTR_CMD = 'cmd' -ATTR_ATT = 'att' -ATTR_RAW = 'raw' -ATTR_DIR = 'dir' -ATTR_ABT = 'abt' -ATTR_NEW = 'new' -ATTR_ON = 'on' -ATTR_OFF = 'off' -ATTR_TOGGLE = 'toggle' +ATTR_PHYSICAL_ADDRESS = "physical_address" +ATTR_TYPE_ID = "type_id" +ATTR_VENDOR_NAME = "vendor_name" +ATTR_VENDOR_ID = "vendor_id" +ATTR_DEVICE = "device" +ATTR_TYPE = "type" +ATTR_KEY = "key" +ATTR_DUR = "dur" +ATTR_SRC = "src" +ATTR_DST = "dst" +ATTR_CMD = "cmd" +ATTR_ATT = "att" +ATTR_RAW = "raw" +ATTR_DIR = "dir" +ATTR_ABT = "abt" +ATTR_NEW = "new" +ATTR_ON = "on" +ATTR_OFF = "off" +ATTR_TOGGLE = "toggle" _VOL_HEX = vol.Any(vol.Coerce(int), lambda x: int(x, 16)) -SERVICE_SEND_COMMAND = 'send_command' -SERVICE_SEND_COMMAND_SCHEMA = vol.Schema({ - vol.Optional(ATTR_CMD): _VOL_HEX, - vol.Optional(ATTR_SRC): _VOL_HEX, - vol.Optional(ATTR_DST): _VOL_HEX, - vol.Optional(ATTR_ATT): _VOL_HEX, - vol.Optional(ATTR_RAW): vol.Coerce(str), -}, extra=vol.PREVENT_EXTRA) +SERVICE_SEND_COMMAND = "send_command" +SERVICE_SEND_COMMAND_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_CMD): _VOL_HEX, + vol.Optional(ATTR_SRC): _VOL_HEX, + vol.Optional(ATTR_DST): _VOL_HEX, + vol.Optional(ATTR_ATT): _VOL_HEX, + vol.Optional(ATTR_RAW): vol.Coerce(str), + }, + extra=vol.PREVENT_EXTRA, +) -SERVICE_VOLUME = 'volume' -SERVICE_VOLUME_SCHEMA = vol.Schema({ - vol.Optional(CMD_UP): vol.Any(CMD_PRESS, CMD_RELEASE, vol.Coerce(int)), - vol.Optional(CMD_DOWN): vol.Any(CMD_PRESS, CMD_RELEASE, vol.Coerce(int)), - vol.Optional(CMD_MUTE): vol.Any(ATTR_ON, ATTR_OFF, ATTR_TOGGLE), -}, extra=vol.PREVENT_EXTRA) +SERVICE_VOLUME = "volume" +SERVICE_VOLUME_SCHEMA = vol.Schema( + { + vol.Optional(CMD_UP): vol.Any(CMD_PRESS, CMD_RELEASE, vol.Coerce(int)), + vol.Optional(CMD_DOWN): vol.Any(CMD_PRESS, CMD_RELEASE, vol.Coerce(int)), + vol.Optional(CMD_MUTE): vol.Any(ATTR_ON, ATTR_OFF, ATTR_TOGGLE), + }, + extra=vol.PREVENT_EXTRA, +) -SERVICE_UPDATE_DEVICES = 'update' -SERVICE_UPDATE_DEVICES_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({}) -}, extra=vol.PREVENT_EXTRA) +SERVICE_UPDATE_DEVICES = "update" +SERVICE_UPDATE_DEVICES_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({})}, extra=vol.PREVENT_EXTRA +) -SERVICE_SELECT_DEVICE = 'select_device' +SERVICE_SELECT_DEVICE = "select_device" -SERVICE_POWER_ON = 'power_on' -SERVICE_STANDBY = 'standby' +SERVICE_POWER_ON = "power_on" +SERVICE_STANDBY = "standby" # pylint: disable=unnecessary-lambda -DEVICE_SCHEMA = vol.Schema({ - vol.All(cv.positive_int): - vol.Any(lambda devices: DEVICE_SCHEMA(devices), cv.string) -}) +DEVICE_SCHEMA = vol.Schema( + { + vol.All(cv.positive_int): vol.Any( + lambda devices: DEVICE_SCHEMA(devices), cv.string + ) + } +) -CONF_DISPLAY_NAME = 'osd_name' +CONF_DISPLAY_NAME = "osd_name" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_DEVICES): - vol.Any(DEVICE_SCHEMA, vol.Schema({ - vol.All(cv.string): vol.Any(cv.string)})), - vol.Optional(CONF_PLATFORM): vol.Any(SWITCH, MEDIA_PLAYER), - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_DISPLAY_NAME): cv.string, - vol.Optional(CONF_TYPES, default={}): - vol.Schema({cv.entity_id: vol.Any(MEDIA_PLAYER, SWITCH)}) - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_DEVICES): vol.Any( + DEVICE_SCHEMA, vol.Schema({vol.All(cv.string): vol.Any(cv.string)}) + ), + vol.Optional(CONF_PLATFORM): vol.Any(SWITCH, MEDIA_PLAYER), + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_DISPLAY_NAME): cv.string, + vol.Optional(CONF_TYPES, default={}): vol.Schema( + {cv.entity_id: vol.Any(MEDIA_PLAYER, SWITCH)} + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def pad_physical_address(addr): @@ -133,6 +156,7 @@ def parse_mapping(mapping, parents=None): for addr, val in mapping.items(): if isinstance(addr, (str,)) and isinstance(val, (str,)): from pycec.network import PhysicalAddress + yield (addr, PhysicalAddress(val)) else: cur = parents + [addr] @@ -146,9 +170,16 @@ def setup(hass: HomeAssistant, base_config): """Set up the CEC capability.""" from pycec.network import HDMINetwork from pycec.commands import CecCommand, KeyReleaseCommand, KeyPressCommand - from pycec.const import KEY_VOLUME_UP, KEY_VOLUME_DOWN, KEY_MUTE_ON, \ - KEY_MUTE_OFF, KEY_MUTE_TOGGLE, ADDR_AUDIOSYSTEM, ADDR_BROADCAST, \ - ADDR_UNREGISTERED + from pycec.const import ( + KEY_VOLUME_UP, + KEY_VOLUME_DOWN, + KEY_MUTE_ON, + KEY_MUTE_OFF, + KEY_MUTE_TOGGLE, + ADDR_AUDIOSYSTEM, + ADDR_BROADCAST, + ADDR_UNREGISTERED, + ) from pycec.cec import CecAdapter from pycec.tcp import TcpAdapter @@ -164,10 +195,12 @@ def setup(hass: HomeAssistant, base_config): loop = ( # Create own thread if more than 1 CPU - hass.loop if multiprocessing.cpu_count() < 2 else None) + hass.loop + if multiprocessing.cpu_count() < 2 + else None + ) host = base_config[DOMAIN].get(CONF_HOST, None) - display_name = base_config[DOMAIN].get( - CONF_DISPLAY_NAME, DEFAULT_DISPLAY_NAME) + display_name = base_config[DOMAIN].get(CONF_DISPLAY_NAME, DEFAULT_DISPLAY_NAME) if host: adapter = TcpAdapter(host, name=display_name, activate_source=False) else: @@ -176,8 +209,11 @@ def setup(hass: HomeAssistant, base_config): def _volume(call): """Increase/decrease volume and mute/unmute system.""" - mute_key_mapping = {ATTR_TOGGLE: KEY_MUTE_TOGGLE, ATTR_ON: KEY_MUTE_ON, - ATTR_OFF: KEY_MUTE_OFF} + mute_key_mapping = { + ATTR_TOGGLE: KEY_MUTE_TOGGLE, + ATTR_ON: KEY_MUTE_ON, + ATTR_OFF: KEY_MUTE_OFF, + } for cmd, att in call.data.items(): if cmd == CMD_UP: _process_volume(KEY_VOLUME_UP, att) @@ -185,10 +221,9 @@ def setup(hass: HomeAssistant, base_config): _process_volume(KEY_VOLUME_DOWN, att) elif cmd == CMD_MUTE: hdmi_network.send_command( - KeyPressCommand(mute_key_mapping[att], - dst=ADDR_AUDIOSYSTEM)) - hdmi_network.send_command( - KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) + KeyPressCommand(mute_key_mapping[att], dst=ADDR_AUDIOSYSTEM) + ) + hdmi_network.send_command(KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) _LOGGER.info("Audio muted") else: _LOGGER.warning("Unknown command %s", cmd) @@ -197,17 +232,14 @@ def setup(hass: HomeAssistant, base_config): if isinstance(att, (str,)): att = att.strip() if att == CMD_PRESS: - hdmi_network.send_command( - KeyPressCommand(cmd, dst=ADDR_AUDIOSYSTEM)) + hdmi_network.send_command(KeyPressCommand(cmd, dst=ADDR_AUDIOSYSTEM)) elif att == CMD_RELEASE: hdmi_network.send_command(KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) else: att = 1 if att == "" else int(att) for _ in range(0, att): - hdmi_network.send_command( - KeyPressCommand(cmd, dst=ADDR_AUDIOSYSTEM)) - hdmi_network.send_command( - KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) + hdmi_network.send_command(KeyPressCommand(cmd, dst=ADDR_AUDIOSYSTEM)) + hdmi_network.send_command(KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) def _tx(call): """Send CEC command.""" @@ -258,11 +290,12 @@ def setup(hass: HomeAssistant, base_config): entity = hass.states.get(addr) _LOGGER.debug("Selecting entity %s", entity) if entity is not None: - addr = entity.attributes['physical_address'] + addr = entity.attributes["physical_address"] _LOGGER.debug("Address acquired: %s", addr) if addr is None: - _LOGGER.error("Device %s has not physical address", - call.data[ATTR_DEVICE]) + _LOGGER.error( + "Device %s has not physical address", call.data[ATTR_DEVICE] + ) return if not isinstance(addr, (PhysicalAddress,)): addr = PhysicalAddress(addr) @@ -279,24 +312,34 @@ def setup(hass: HomeAssistant, base_config): def _new_device(device): """Handle new devices which are detected by HDMI network.""" - key = '{}.{}'.format(DOMAIN, device.name) + key = "{}.{}".format(DOMAIN, device.name) hass.data[key] = device ent_platform = base_config[DOMAIN][CONF_TYPES].get(key, platform) discovery.load_platform( - hass, ent_platform, DOMAIN, discovered={ATTR_NEW: [key]}, - hass_config=base_config) + hass, + ent_platform, + DOMAIN, + discovered={ATTR_NEW: [key]}, + hass_config=base_config, + ) def _shutdown(call): hdmi_network.stop() def _start_cec(event): """Register services and start HDMI network to watch for devices.""" - hass.services.register(DOMAIN, SERVICE_SEND_COMMAND, _tx, - SERVICE_SEND_COMMAND_SCHEMA) - hass.services.register(DOMAIN, SERVICE_VOLUME, _volume, - schema=SERVICE_VOLUME_SCHEMA) - hass.services.register(DOMAIN, SERVICE_UPDATE_DEVICES, _update, - schema=SERVICE_UPDATE_DEVICES_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_SEND_COMMAND, _tx, SERVICE_SEND_COMMAND_SCHEMA + ) + hass.services.register( + DOMAIN, SERVICE_VOLUME, _volume, schema=SERVICE_VOLUME_SCHEMA + ) + hass.services.register( + DOMAIN, + SERVICE_UPDATE_DEVICES, + _update, + schema=SERVICE_UPDATE_DEVICES_SCHEMA, + ) hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on) hass.services.register(DOMAIN, SERVICE_STANDBY, _standby) hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE, _select_device) @@ -323,8 +366,14 @@ class CecDevice(Entity): def update(self): """Update device status.""" device = self._device - from pycec.const import STATUS_PLAY, STATUS_STOP, STATUS_STILL, \ - POWER_OFF, POWER_ON + from pycec.const import ( + STATUS_PLAY, + STATUS_STOP, + STATUS_STILL, + POWER_OFF, + POWER_ON, + ) + if device.power_status in [POWER_OFF, 3]: self._state = STATE_OFF elif device.status == STATUS_PLAY: @@ -351,12 +400,16 @@ class CecDevice(Entity): """Return the name of the device.""" return ( "%s %s" % (self.vendor_name, self._device.osd_name) - if (self._device.osd_name is not None and - self.vendor_name is not None and self.vendor_name != 'Unknown') + if ( + self._device.osd_name is not None + and self.vendor_name is not None + and self.vendor_name != "Unknown" + ) else "%s %d" % (self._device.type_name, self._logical_address) if self._device.osd_name is None - else "%s %d (%s)" % (self._device.type_name, self._logical_address, - self._device.osd_name)) + else "%s %d (%s)" + % (self._device.type_name, self._logical_address, self._device.osd_name) + ) @property def vendor_id(self): @@ -386,9 +439,13 @@ class CecDevice(Entity): @property def icon(self): """Return the icon for device by its type.""" - return (self._icon if self._icon is not None else - ICONS_BY_TYPE.get(self._device.type) - if self._device.type in ICONS_BY_TYPE else ICON_UNKNOWN) + return ( + self._icon + if self._icon is not None + else ICONS_BY_TYPE.get(self._device.type) + if self._device.type in ICONS_BY_TYPE + else ICON_UNKNOWN + ) @property def device_state_attributes(self): diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index 4468fd9d648..379105430bc 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -3,17 +3,30 @@ import logging from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP) + DOMAIN, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_STEP, +) from homeassistant.const import ( - STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +) from . import ATTR_NEW, CecDevice _LOGGER = logging.getLogger(__name__) -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -23,9 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entities = [] for device in discovery_info[ATTR_NEW]: hdmi_device = hass.data.get(device) - entities.append(CecPlayerDevice( - hdmi_device, hdmi_device.logical_address, - )) + entities.append(CecPlayerDevice(hdmi_device, hdmi_device.logical_address)) add_entities(entities, True) @@ -35,33 +46,34 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def __init__(self, device, logical) -> None: """Initialize the HDMI device.""" CecDevice.__init__(self, device, logical) - self.entity_id = "%s.%s_%s" % ( - DOMAIN, 'hdmi', hex(self._logical_address)[2:]) + self.entity_id = "%s.%s_%s" % (DOMAIN, "hdmi", hex(self._logical_address)[2:]) def send_keypress(self, key): """Send keypress to CEC adapter.""" from pycec.commands import KeyPressCommand, KeyReleaseCommand - _LOGGER.debug("Sending keypress %s to device %s", hex(key), - hex(self._logical_address)) - self._device.send_command( - KeyPressCommand(key, dst=self._logical_address)) - self._device.send_command( - KeyReleaseCommand(dst=self._logical_address)) + + _LOGGER.debug( + "Sending keypress %s to device %s", hex(key), hex(self._logical_address) + ) + self._device.send_command(KeyPressCommand(key, dst=self._logical_address)) + self._device.send_command(KeyReleaseCommand(dst=self._logical_address)) def send_playback(self, key): """Send playback status to CEC adapter.""" from pycec.commands import CecCommand - self._device.async_send_command( - CecCommand(key, dst=self._logical_address)) + + self._device.async_send_command(CecCommand(key, dst=self._logical_address)) def mute_volume(self, mute): """Mute volume.""" from pycec.const import KEY_MUTE_TOGGLE + self.send_keypress(KEY_MUTE_TOGGLE) def media_previous_track(self): """Go to previous track.""" from pycec.const import KEY_BACKWARD + self.send_keypress(KEY_BACKWARD) def turn_on(self): @@ -81,6 +93,7 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def media_stop(self): """Stop playback.""" from pycec.const import KEY_STOP + self.send_keypress(KEY_STOP) self._state = STATE_IDLE @@ -91,6 +104,7 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def media_next_track(self): """Skip to next track.""" from pycec.const import KEY_FORWARD + self.send_keypress(KEY_FORWARD) def media_seek(self, position): @@ -104,6 +118,7 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def media_pause(self): """Pause playback.""" from pycec.const import KEY_PAUSE + self.send_keypress(KEY_PAUSE) self._state = STATE_PAUSED @@ -114,18 +129,21 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def media_play(self): """Start playback.""" from pycec.const import KEY_PLAY + self.send_keypress(KEY_PLAY) self._state = STATE_PLAYING def volume_up(self): """Increase volume.""" from pycec.const import KEY_VOLUME_UP + _LOGGER.debug("%s: volume up", self._logical_address) self.send_keypress(KEY_VOLUME_UP) def volume_down(self): """Decrease volume.""" from pycec.const import KEY_VOLUME_DOWN + _LOGGER.debug("%s: volume down", self._logical_address) self.send_keypress(KEY_VOLUME_DOWN) @@ -137,8 +155,14 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): def update(self): """Update device status.""" device = self._device - from pycec.const import STATUS_PLAY, STATUS_STOP, STATUS_STILL, \ - POWER_OFF, POWER_ON + from pycec.const import ( + STATUS_PLAY, + STATUS_STOP, + STATUS_STILL, + POWER_OFF, + POWER_ON, + ) + if device.power_status in [POWER_OFF, 3]: self._state = STATE_OFF elif not self.support_pause: @@ -156,16 +180,31 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice): @property def supported_features(self): """Flag media player features that are supported.""" - from pycec.const import TYPE_RECORDER, TYPE_PLAYBACK, TYPE_TUNER, \ - TYPE_AUDIO + from pycec.const import TYPE_RECORDER, TYPE_PLAYBACK, TYPE_TUNER, TYPE_AUDIO + if self.type_id == TYPE_RECORDER or self.type == TYPE_PLAYBACK: - return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | - SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_PREVIOUS_TRACK | - SUPPORT_NEXT_TRACK) + return ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PLAY_MEDIA + | SUPPORT_PAUSE + | SUPPORT_STOP + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + ) if self.type == TYPE_TUNER: - return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | - SUPPORT_PAUSE | SUPPORT_STOP) + return ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PLAY_MEDIA + | SUPPORT_PAUSE + | SUPPORT_STOP + ) if self.type_id == TYPE_AUDIO: - return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | - SUPPORT_VOLUME_MUTE) + return ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_MUTE + ) return SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index 9fb003f6d6a..53384397cf4 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -8,7 +8,7 @@ from . import ATTR_NEW, CecDevice _LOGGER = logging.getLogger(__name__) -ENTITY_ID_FORMAT = DOMAIN + '.{}' +ENTITY_ID_FORMAT = DOMAIN + ".{}" def setup_platform(hass, config, add_entities, discovery_info=None): @@ -18,9 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entities = [] for device in discovery_info[ATTR_NEW]: hdmi_device = hass.data.get(device) - entities.append(CecSwitchDevice( - hdmi_device, hdmi_device.logical_address, - )) + entities.append(CecSwitchDevice(hdmi_device, hdmi_device.logical_address)) add_entities(entities, True) @@ -30,8 +28,7 @@ class CecSwitchDevice(CecDevice, SwitchDevice): def __init__(self, device, logical) -> None: """Initialize the HDMI device.""" CecDevice.__init__(self, device, logical) - self.entity_id = "%s.%s_%s" % ( - DOMAIN, 'hdmi', hex(self._logical_address)[2:]) + self.entity_id = "%s.%s_%s" % (DOMAIN, "hdmi", hex(self._logical_address)[2:]) def turn_on(self, **kwargs) -> None: """Turn device on.""" diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index 0b92c377d48..c9bed1e9d34 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -4,28 +4,32 @@ import logging import voluptuous as vol from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA -from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE from homeassistant.const import ( - TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID) + TEMP_CELSIUS, + ATTR_TEMPERATURE, + CONF_PORT, + CONF_NAME, + CONF_ID, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_IPADDRESS = 'ipaddress' -CONF_TSTATS = 'tstats' +CONF_IPADDRESS = "ipaddress" +CONF_TSTATS = "tstats" -TSTATS_SCHEMA = vol.Schema({ - vol.Required(CONF_ID): cv.string, - vol.Required(CONF_NAME): cv.string, -}) +TSTATS_SCHEMA = vol.Schema( + {vol.Required(CONF_ID): cv.string, vol.Required(CONF_NAME): cv.string} +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_IPADDRESS): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_TSTATS, default={}): - vol.Schema({cv.string: TSTATS_SCHEMA}), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_IPADDRESS): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_TSTATS, default={}): vol.Schema({cv.string: TSTATS_SCHEMA}), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -39,10 +43,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): serport = connection.connection(ipaddress, port) serport.open() - add_entities([ - HeatmiserV3Thermostat( - heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport) - for tstat in tstats.values()], True) + add_entities( + [ + HeatmiserV3Thermostat( + heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport + ) + for tstat in tstats.values() + ], + True, + ) class HeatmiserV3Thermostat(ClimateDevice): @@ -86,17 +95,12 @@ class HeatmiserV3Thermostat(ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) - self.heatmiser.hmSendAddress( - self._id, - 18, - temperature, - 1, - self.serport) + self.heatmiser.hmSendAddress(self._id, 18, temperature, 1, self.serport) def update(self): """Get the latest data.""" - self.dcb = self.heatmiser.hmReadAddress(self._id, 'prt', self.serport) - low = self.dcb.get('floortemplow ') - high = self.dcb.get('floortemphigh') + self.dcb = self.heatmiser.hmReadAddress(self._id, "prt", self.serport) + low = self.dcb.get("floortemplow ") + high = self.dcb.get("floortemphigh") self._current_temperature = (high * 256 + low) / 10.0 - self._target_temperature = int(self.dcb.get('roomset')) + self._target_temperature = int(self.dcb.get("roomset")) diff --git a/homeassistant/components/heos/.translations/bg.json b/homeassistant/components/heos/.translations/bg.json new file mode 100644 index 00000000000..dea7dd9bb24 --- /dev/null +++ b/homeassistant/components/heos/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 Heos \u0432\u0440\u044a\u0437\u043a\u0430, \u0442\u044a\u0439 \u043a\u0430\u0442\u043e \u0442\u044f \u0449\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0432\u0441\u0438\u0447\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430." + }, + "error": { + "connection_failure": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0441\u043e\u0447\u0435\u043d\u0438\u044f \u0430\u0434\u0440\u0435\u0441." + }, + "step": { + "user": { + "data": { + "access_token": "\u0410\u0434\u0440\u0435\u0441", + "host": "\u0410\u0434\u0440\u0435\u0441" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043c\u0435\u0442\u043e \u043d\u0430 \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 Heos \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (\u0437\u0430 \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0438\u0442\u0430\u043d\u0435 \u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0434\u0430 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u043e \u0441 \u043a\u0430\u0431\u0435\u043b \u043a\u044a\u043c \u043c\u0440\u0435\u0436\u0430\u0442\u0430).", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Heos" + } + }, + "title": "HEOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/da.json b/homeassistant/components/heos/.translations/da.json new file mode 100644 index 00000000000..f2d9441e48a --- /dev/null +++ b/homeassistant/components/heos/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en enkelt Heos-forbindelse, da den underst\u00f8tter alle enheder p\u00e5 netv\u00e6rket." + }, + "error": { + "connection_failure": "Kunne ikke oprette forbindelse til den angivne v\u00e6rt." + }, + "step": { + "user": { + "data": { + "access_token": "V\u00e6rt", + "host": "V\u00e6rt" + }, + "description": "Indtast v\u00e6rtsnavnet eller IP-adressen p\u00e5 en Heos-enhed (helst en tilsluttet via ledning til netv\u00e6rket).", + "title": "Opret forbindelse til HEOS" + } + }, + "title": "HEOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/pt-BR.json b/homeassistant/components/heos/.translations/pt-BR.json index ac860059b5d..5bcd39efd20 100644 --- a/homeassistant/components/heos/.translations/pt-BR.json +++ b/homeassistant/components/heos/.translations/pt-BR.json @@ -1,13 +1,21 @@ { "config": { + "abort": { + "already_setup": "Voc\u00ea s\u00f3 pode configurar uma \u00fanica conex\u00e3o Heos, pois ela suportar\u00e1 todos os dispositivos na rede." + }, + "error": { + "connection_failure": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao host especificado." + }, "step": { "user": { "data": { "access_token": "Host", "host": "Host" }, + "description": "Por favor, digite o nome do host ou o endere\u00e7o IP de um dispositivo Heos (de prefer\u00eancia para conex\u00f5es conectadas por cabo \u00e0 sua rede).", "title": "Conecte-se a Heos" } - } + }, + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 7a6cb36ab7b..a5450253be0 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -7,8 +7,7 @@ from typing import Dict from pyheos import CommandError, Heos, const as heos_const import voluptuous as vol -from homeassistant.components.media_player.const import ( - DOMAIN as MEDIA_PLAYER_DOMAIN) +from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady @@ -19,14 +18,17 @@ from homeassistant.util import Throttle from . import services from .config_flow import format_title from .const import ( - COMMAND_RETRY_ATTEMPTS, COMMAND_RETRY_DELAY, DATA_CONTROLLER_MANAGER, - DATA_SOURCE_MANAGER, DOMAIN, SIGNAL_HEOS_UPDATED) + COMMAND_RETRY_ATTEMPTS, + COMMAND_RETRY_DELAY, + DATA_CONTROLLER_MANAGER, + DATA_SOURCE_MANAGER, + DOMAIN, + SIGNAL_HEOS_UPDATED, +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA +) MIN_UPDATE_SOURCES = timedelta(seconds=1) @@ -43,8 +45,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): # Create new entry based on config hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={'source': 'import'}, - data={CONF_HOST: host})) + DOMAIN, context={"source": "import"}, data={CONF_HOST: host} + ) + ) else: # Check if host needs to be updated entry = entries[0] @@ -73,6 +76,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): # Disconnect when shutting down async def disconnect_controller(event): await controller.disconnect() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect_controller) # Get players and sources @@ -85,12 +89,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): _LOGGER.warning( "%s is not logged in to a HEOS account and will be unable " "to retrieve HEOS favorites: Use the 'heos.sign_in' service " - "to sign-in to a HEOS account", host) + "to sign-in to a HEOS account", + host, + ) inputs = await controller.get_input_sources() except (asyncio.TimeoutError, ConnectionError, CommandError) as error: await controller.disconnect() - _LOGGER.debug("Unable to retrieve players and sources: %s", error, - exc_info=isinstance(error, CommandError)) + _LOGGER.debug( + "Unable to retrieve players and sources: %s", + error, + exc_info=isinstance(error, CommandError), + ) raise ConfigEntryNotReady controller_manager = ControllerManager(hass, controller) @@ -102,13 +111,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.data[DOMAIN] = { DATA_CONTROLLER_MANAGER: controller_manager, DATA_SOURCE_MANAGER: source_manager, - MEDIA_PLAYER_DOMAIN: players + MEDIA_PLAYER_DOMAIN: players, } services.register(hass, controller) - hass.async_create_task(hass.config_entries.async_forward_entry_setup( - entry, MEDIA_PLAYER_DOMAIN)) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, MEDIA_PLAYER_DOMAIN) + ) return True @@ -121,7 +131,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): services.remove(hass) return await hass.config_entries.async_forward_entry_unload( - entry, MEDIA_PLAYER_DOMAIN) + entry, MEDIA_PLAYER_DOMAIN + ) class ControllerManager: @@ -139,13 +150,20 @@ class ControllerManager: """Subscribe to events of interest.""" self._device_registry, self._entity_registry = await asyncio.gather( self._hass.helpers.device_registry.async_get_registry(), - self._hass.helpers.entity_registry.async_get_registry()) + self._hass.helpers.entity_registry.async_get_registry(), + ) # Handle controller events - self._signals.append(self.controller.dispatcher.connect( - heos_const.SIGNAL_CONTROLLER_EVENT, self._controller_event)) + self._signals.append( + self.controller.dispatcher.connect( + heos_const.SIGNAL_CONTROLLER_EVENT, self._controller_event + ) + ) # Handle connection-related events - self._signals.append(self.controller.dispatcher.connect( - heos_const.SIGNAL_HEOS_EVENT, self._heos_event)) + self._signals.append( + self.controller.dispatcher.connect( + heos_const.SIGNAL_HEOS_EVENT, self._heos_event + ) + ) async def disconnect(self): """Disconnect subscriptions.""" @@ -160,8 +178,7 @@ class ControllerManager: if event == heos_const.EVENT_PLAYERS_CHANGED: self.update_ids(data[heos_const.DATA_MAPPED_IDS]) # Update players - self._hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_HEOS_UPDATED) + self._hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED) async def _heos_event(self, event): """Handle connection event.""" @@ -173,38 +190,44 @@ class ControllerManager: except (CommandError, asyncio.TimeoutError, ConnectionError) as ex: _LOGGER.error("Unable to refresh players: %s", ex) # Update players - self._hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_HEOS_UPDATED) + self._hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED) def update_ids(self, mapped_ids: Dict[int, int]): """Update the IDs in the device and entity registry.""" # mapped_ids contains the mapped IDs (new:old) for new_id, old_id in mapped_ids.items(): # update device registry - entry = self._device_registry.async_get_device( - {(DOMAIN, old_id)}, set()) + entry = self._device_registry.async_get_device({(DOMAIN, old_id)}, set()) new_identifiers = {(DOMAIN, new_id)} if entry: self._device_registry.async_update_device( - entry.id, new_identifiers=new_identifiers) - _LOGGER.debug("Updated device %s identifiers to %s", - entry.id, new_identifiers) + entry.id, new_identifiers=new_identifiers + ) + _LOGGER.debug( + "Updated device %s identifiers to %s", entry.id, new_identifiers + ) # update entity registry entity_id = self._entity_registry.async_get_entity_id( - MEDIA_PLAYER_DOMAIN, DOMAIN, str(old_id)) + MEDIA_PLAYER_DOMAIN, DOMAIN, str(old_id) + ) if entity_id: self._entity_registry.async_update_entity( - entity_id, new_unique_id=str(new_id)) - _LOGGER.debug("Updated entity %s unique id to %s", - entity_id, new_id) + entity_id, new_unique_id=str(new_id) + ) + _LOGGER.debug("Updated entity %s unique id to %s", entity_id, new_id) class SourceManager: """Class that manages sources for players.""" - def __init__(self, favorites, inputs, *, - retry_delay: int = COMMAND_RETRY_DELAY, - max_retry_attempts: int = COMMAND_RETRY_ATTEMPTS): + def __init__( + self, + favorites, + inputs, + *, + retry_delay: int = COMMAND_RETRY_DELAY, + max_retry_attempts: int = COMMAND_RETRY_ATTEMPTS, + ): """Init input manager.""" self.retry_delay = retry_delay self.max_retry_attempts = max_retry_attempts @@ -215,21 +238,32 @@ class SourceManager: def _build_source_list(self): """Build a single list of inputs from various types.""" source_list = [] - source_list.extend([favorite.name for favorite - in self.favorites.values()]) + source_list.extend([favorite.name for favorite in self.favorites.values()]) source_list.extend([source.name for source in self.inputs]) return source_list async def play_source(self, source: str, player): """Determine type of source and play it.""" - index = next((index for index, favorite in self.favorites.items() - if favorite.name == source), None) + index = next( + ( + index + for index, favorite in self.favorites.items() + if favorite.name == source + ), + None, + ) if index is not None: await player.play_favorite(index) return - input_source = next((input_source for input_source in self.inputs - if input_source.name == source), None) + input_source = next( + ( + input_source + for input_source in self.inputs + if input_source.name == source + ), + None, + ) if input_source is not None: await player.play_input_source(input_source) return @@ -240,13 +274,24 @@ class SourceManager: """Determine current source from now playing media.""" # Match input by input_name:media_id if now_playing_media.source_id == heos_const.MUSIC_SOURCE_AUX_INPUT: - return next((input_source.name for input_source in self.inputs - if input_source.input_name == - now_playing_media.media_id), None) + return next( + ( + input_source.name + for input_source in self.inputs + if input_source.input_name == now_playing_media.media_id + ), + None, + ) # Try matching favorite by name:station or media_id:album_id - return next((source.name for source in self.favorites.values() - if source.name == now_playing_media.station - or source.media_id == now_playing_media.album_id), None) + return next( + ( + source.name + for source in self.favorites.values() + if source.name == now_playing_media.station + or source.media_id == now_playing_media.album_id + ), + None, + ) def connect_update(self, hass, controller): """ @@ -256,6 +301,7 @@ class SourceManager: physical event therefore throttle it. Retrieving sources immediately after the event may fail so retry. """ + @Throttle(MIN_UPDATE_SOURCES) async def get_sources(): retry_attempts = 0 @@ -266,23 +312,29 @@ class SourceManager: favorites = await controller.get_favorites() inputs = await controller.get_input_sources() return favorites, inputs - except (asyncio.TimeoutError, ConnectionError, CommandError) \ - as error: + except (asyncio.TimeoutError, ConnectionError, CommandError) as error: if retry_attempts < self.max_retry_attempts: retry_attempts += 1 - _LOGGER.debug("Error retrieving sources and will " - "retry: %s", error, - exc_info=isinstance(error, CommandError)) + _LOGGER.debug( + "Error retrieving sources and will " "retry: %s", + error, + exc_info=isinstance(error, CommandError), + ) await asyncio.sleep(self.retry_delay) else: - _LOGGER.error("Unable to update sources: %s", error, - exc_info=isinstance(error, CommandError)) + _LOGGER.error( + "Unable to update sources: %s", + error, + exc_info=isinstance(error, CommandError), + ) return async def update_sources(event, data=None): - if event in (heos_const.EVENT_SOURCES_CHANGED, - heos_const.EVENT_USER_CHANGED, - heos_const.EVENT_CONNECTED): + if event in ( + heos_const.EVENT_SOURCES_CHANGED, + heos_const.EVENT_USER_CHANGED, + heos_const.EVENT_CONNECTED, + ): sources = await get_sources() # If throttled, it will return None if sources: @@ -290,10 +342,9 @@ class SourceManager: self.source_list = self._build_source_list() _LOGGER.debug("Sources updated due to changed event") # Let players know to update - hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_HEOS_UPDATED) + hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED) controller.dispatcher.connect( - heos_const.SIGNAL_CONTROLLER_EVENT, update_sources) - controller.dispatcher.connect( - heos_const.SIGNAL_HEOS_EVENT, update_sources) + heos_const.SIGNAL_CONTROLLER_EVENT, update_sources + ) + controller.dispatcher.connect(heos_const.SIGNAL_HEOS_EVENT, update_sources) diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 8207d40be11..7c7f57a91d7 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -26,29 +26,27 @@ class HeosFlowHandler(config_entries.ConfigFlow): """Handle a discovered Heos device.""" # Store discovered host friendly_name = "{} ({})".format( - discovery_info[CONF_NAME], discovery_info[CONF_HOST]) + discovery_info[CONF_NAME], discovery_info[CONF_HOST] + ) self.hass.data.setdefault(DATA_DISCOVERED_HOSTS, {}) - self.hass.data[DATA_DISCOVERED_HOSTS][friendly_name] \ - = discovery_info[CONF_HOST] + self.hass.data[DATA_DISCOVERED_HOSTS][friendly_name] = discovery_info[CONF_HOST] # Abort if other flows in progress or an entry already exists if self._async_in_progress() or self._async_current_entries(): - return self.async_abort(reason='already_setup') + return self.async_abort(reason="already_setup") # Show selection form - return self.async_show_form(step_id='user') + return self.async_show_form(step_id="user") async def async_step_import(self, user_input=None): """Occurs when an entry is setup through config.""" host = user_input[CONF_HOST] - return self.async_create_entry( - title=format_title(host), - data={CONF_HOST: host}) + return self.async_create_entry(title=format_title(host), data={CONF_HOST: host}) async def async_step_user(self, user_input=None): """Obtain host and validate connection.""" self.hass.data.setdefault(DATA_DISCOVERED_HOSTS, {}) # Only a single entry is needed for all devices if self._async_current_entries(): - return self.async_abort(reason='already_setup') + return self.async_abort(reason="already_setup") # Try connecting to host if provided errors = {} host = None @@ -62,16 +60,18 @@ class HeosFlowHandler(config_entries.ConfigFlow): self.hass.data.pop(DATA_DISCOVERED_HOSTS) return await self.async_step_import({CONF_HOST: host}) except (asyncio.TimeoutError, ConnectionError): - errors[CONF_HOST] = 'connection_failure' + errors[CONF_HOST] = "connection_failure" finally: await heos.disconnect() # Return form - host_type = str if not self.hass.data[DATA_DISCOVERED_HOSTS] \ + host_type = ( + str + if not self.hass.data[DATA_DISCOVERED_HOSTS] else vol.In(list(self.hass.data[DATA_DISCOVERED_HOSTS])) + ) return self.async_show_form( - step_id='user', - data_schema=vol.Schema({ - vol.Required(CONF_HOST, default=host): host_type - }), - errors=errors) + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): host_type}), + errors=errors, + ) diff --git a/homeassistant/components/heos/const.py b/homeassistant/components/heos/const.py index d3e3ccb07c3..503df40ccd4 100644 --- a/homeassistant/components/heos/const.py +++ b/homeassistant/components/heos/const.py @@ -7,7 +7,7 @@ COMMAND_RETRY_DELAY = 1 DATA_CONTROLLER_MANAGER = "controller" DATA_SOURCE_MANAGER = "source_manager" DATA_DISCOVERED_HOSTS = "heos_discovered_hosts" -DOMAIN = 'heos' +DOMAIN = "heos" SERVICE_SIGN_IN = "sign_in" SERVICE_SIGN_OUT = "sign_out" SIGNAL_HEOS_UPDATED = "heos_updated" diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index ff5c2d707f2..a4094a0c216 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -9,28 +9,45 @@ from pyheos import CommandError, const as heos_const from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( - ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_URL, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) + ATTR_MEDIA_ENQUEUE, + DOMAIN, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_URL, + SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow -from .const import ( - DATA_SOURCE_MANAGER, DOMAIN as HEOS_DOMAIN, SIGNAL_HEOS_UPDATED) +from .const import DATA_SOURCE_MANAGER, DOMAIN as HEOS_DOMAIN, SIGNAL_HEOS_UPDATED -BASE_SUPPORTED_FEATURES = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ - SUPPORT_VOLUME_STEP | SUPPORT_CLEAR_PLAYLIST | \ - SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOURCE | \ - SUPPORT_PLAY_MEDIA +BASE_SUPPORTED_FEATURES = ( + SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP + | SUPPORT_CLEAR_PLAYLIST + | SUPPORT_SHUFFLE_SET + | SUPPORT_SELECT_SOURCE + | SUPPORT_PLAY_MEDIA +) PLAY_STATE_TO_STATE = { heos_const.PLAY_STATE_PLAY: STATE_PLAYING, heos_const.PLAY_STATE_STOP: STATE_IDLE, - heos_const.PLAY_STATE_PAUSE: STATE_PAUSED + heos_const.PLAY_STATE_PAUSE: STATE_PAUSED, } CONTROL_TO_SUPPORT = { @@ -38,20 +55,20 @@ CONTROL_TO_SUPPORT = { heos_const.CONTROL_PAUSE: SUPPORT_PAUSE, heos_const.CONTROL_STOP: SUPPORT_STOP, heos_const.CONTROL_PLAY_PREVIOUS: SUPPORT_PREVIOUS_TRACK, - heos_const.CONTROL_PLAY_NEXT: SUPPORT_NEXT_TRACK + heos_const.CONTROL_PLAY_NEXT: SUPPORT_NEXT_TRACK, } _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Platform uses config entry setup.""" pass -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities): +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +): """Add media players for a config entry.""" players = hass.data[HEOS_DOMAIN][DOMAIN] devices = [HeosMediaPlayer(player) for player in players.values()] @@ -60,15 +77,22 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, def log_command_error(command: str): """Return decorator that logs command failure.""" + def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): try: await func(*args, **kwargs) - except (CommandError, asyncio.TimeoutError, ConnectionError, - ValueError) as ex: + except ( + CommandError, + asyncio.TimeoutError, + ConnectionError, + ValueError, + ) as ex: _LOGGER.error("Unable to %s: %s", command, ex) + return wrapper + return decorator @@ -99,12 +123,17 @@ class HeosMediaPlayer(MediaPlayerDevice): """Device added to hass.""" self._source_manager = self.hass.data[HEOS_DOMAIN][DATA_SOURCE_MANAGER] # Update state when attributes of the player change - self._signals.append(self._player.heos.dispatcher.connect( - heos_const.SIGNAL_PLAYER_EVENT, self._player_update)) + self._signals.append( + self._player.heos.dispatcher.connect( + heos_const.SIGNAL_PLAYER_EVENT, self._player_update + ) + ) # Update state when heos changes self._signals.append( self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_HEOS_UPDATED, self._heos_updated)) + SIGNAL_HEOS_UPDATED, self._heos_updated + ) + ) @log_command_error("clear playlist") async def async_clear_playlist(self): @@ -155,8 +184,10 @@ class HeosMediaPlayer(MediaPlayerDevice): index = int(media_id) except ValueError: # Try finding index by name - index = next((index for index, select in selects.items() - if select == media_id), None) + index = next( + (index for index, select in selects.items() if select == media_id), + None, + ) if index is None: raise ValueError("Invalid quick select '{}'".format(media_id)) await self._player.play_quick_select(index) @@ -167,9 +198,11 @@ class HeosMediaPlayer(MediaPlayerDevice): playlist = next((p for p in playlists if p.name == media_id), None) if not playlist: raise ValueError("Invalid playlist '{}'".format(media_id)) - add_queue_option = heos_const.ADD_QUEUE_ADD_TO_END \ - if kwargs.get(ATTR_MEDIA_ENQUEUE) \ + add_queue_option = ( + heos_const.ADD_QUEUE_ADD_TO_END + if kwargs.get(ATTR_MEDIA_ENQUEUE) else heos_const.ADD_QUEUE_REPLACE_AND_PLAY + ) await self._player.add_to_queue(playlist, add_queue_option) return @@ -179,9 +212,14 @@ class HeosMediaPlayer(MediaPlayerDevice): index = int(media_id) except ValueError: # Try finding index by name - index = next((index for index, favorite - in self._source_manager.favorites.items() - if favorite.name == media_id), None) + index = next( + ( + index + for index, favorite in self._source_manager.favorites.items() + if favorite.name == media_id + ), + None, + ) if index is None: raise ValueError("Invalid favorite '{}'".format(media_id)) await self._player.play_favorite(index) @@ -207,10 +245,8 @@ class HeosMediaPlayer(MediaPlayerDevice): async def async_update(self): """Update supported features of the player.""" controls = self._player.now_playing_media.supported_controls - current_support = [CONTROL_TO_SUPPORT[control] - for control in controls] - self._supported_features = reduce(ior, current_support, - BASE_SUPPORTED_FEATURES) + current_support = [CONTROL_TO_SUPPORT[control] for control in controls] + self._supported_features = reduce(ior, current_support, BASE_SUPPORTED_FEATURES) async def async_will_remove_from_hass(self): """Disconnect the device when removed.""" @@ -227,24 +263,22 @@ class HeosMediaPlayer(MediaPlayerDevice): def device_info(self) -> dict: """Get attributes about the device.""" return { - 'identifiers': { - (HEOS_DOMAIN, self._player.player_id) - }, - 'name': self._player.name, - 'model': self._player.model, - 'manufacturer': 'HEOS', - 'sw_version': self._player.version + "identifiers": {(HEOS_DOMAIN, self._player.player_id)}, + "name": self._player.name, + "model": self._player.model, + "manufacturer": "HEOS", + "sw_version": self._player.version, } @property def device_state_attributes(self) -> dict: """Get additional attribute about the state.""" return { - 'media_album_id': self._player.now_playing_media.album_id, - 'media_queue_id': self._player.now_playing_media.queue_id, - 'media_source_id': self._player.now_playing_media.source_id, - 'media_station': self._player.now_playing_media.station, - 'media_type': self._player.now_playing_media.type + "media_album_id": self._player.now_playing_media.album_id, + "media_queue_id": self._player.now_playing_media.queue_id, + "media_source_id": self._player.now_playing_media.source_id, + "media_station": self._player.now_playing_media.station, + "media_type": self._player.now_playing_media.type, } @property @@ -331,8 +365,7 @@ class HeosMediaPlayer(MediaPlayerDevice): @property def source(self) -> str: """Name of the current input source.""" - return self._source_manager.get_current_source( - self._player.now_playing_media) + return self._source_manager.get_current_source(self._player.now_playing_media) @property def source_list(self) -> Sequence[str]: diff --git a/homeassistant/components/heos/services.py b/homeassistant/components/heos/services.py index 5b998f384dc..8f3521399e2 100644 --- a/homeassistant/components/heos/services.py +++ b/homeassistant/components/heos/services.py @@ -10,14 +10,18 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from .const import ( - ATTR_PASSWORD, ATTR_USERNAME, DOMAIN, SERVICE_SIGN_IN, SERVICE_SIGN_OUT) + ATTR_PASSWORD, + ATTR_USERNAME, + DOMAIN, + SERVICE_SIGN_IN, + SERVICE_SIGN_OUT, +) _LOGGER = logging.getLogger(__name__) -HEOS_SIGN_IN_SCHEMA = vol.Schema({ - vol.Required(ATTR_USERNAME): cv.string, - vol.Required(ATTR_PASSWORD): cv.string -}) +HEOS_SIGN_IN_SCHEMA = vol.Schema( + {vol.Required(ATTR_USERNAME): cv.string, vol.Required(ATTR_PASSWORD): cv.string} +) HEOS_SIGN_OUT_SCHEMA = vol.Schema({}) @@ -25,13 +29,17 @@ HEOS_SIGN_OUT_SCHEMA = vol.Schema({}) def register(hass: HomeAssistantType, controller: Heos): """Register HEOS services.""" hass.services.async_register( - DOMAIN, SERVICE_SIGN_IN, + DOMAIN, + SERVICE_SIGN_IN, functools.partial(_sign_in_handler, controller), - schema=HEOS_SIGN_IN_SCHEMA) + schema=HEOS_SIGN_IN_SCHEMA, + ) hass.services.async_register( - DOMAIN, SERVICE_SIGN_OUT, + DOMAIN, + SERVICE_SIGN_OUT, functools.partial(_sign_out_handler, controller), - schema=HEOS_SIGN_OUT_SCHEMA) + schema=HEOS_SIGN_OUT_SCHEMA, + ) def remove(hass: HomeAssistantType): diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index f15d6739615..a9ab242c2fd 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -5,65 +5,77 @@ import voluptuous as vol from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.util.dt import utcnow -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, - ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) + CONF_HOST, + CONF_PORT, + CONF_NAME, + CONF_USERNAME, + CONF_PASSWORD, + CONF_SSL, + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_START, + ATTR_LAST_TRIP_TIME, + CONF_CUSTOMIZE, +) _LOGGER = logging.getLogger(__name__) -CONF_IGNORED = 'ignored' -CONF_DELAY = 'delay' +CONF_IGNORED = "ignored" +CONF_DELAY = "delay" DEFAULT_PORT = 80 DEFAULT_IGNORED = False DEFAULT_DELAY = 0 -ATTR_DELAY = 'delay' +ATTR_DELAY = "delay" DEVICE_CLASS_MAP = { - 'Motion': 'motion', - 'Line Crossing': 'motion', - 'Field Detection': 'motion', - 'Video Loss': None, - 'Tamper Detection': 'motion', - 'Shelter Alarm': None, - 'Disk Full': None, - 'Disk Error': None, - 'Net Interface Broken': 'connectivity', - 'IP Conflict': 'connectivity', - 'Illegal Access': None, - 'Video Mismatch': None, - 'Bad Video': None, - 'PIR Alarm': 'motion', - 'Face Detection': 'motion', - 'Scene Change Detection': 'motion', - 'I/O': None, - 'Unattended Baggage': 'motion', - 'Attended Baggage': 'motion', - 'Recording Failure': None, - 'Exiting Region': 'motion', - 'Entering Region': 'motion', + "Motion": "motion", + "Line Crossing": "motion", + "Field Detection": "motion", + "Video Loss": None, + "Tamper Detection": "motion", + "Shelter Alarm": None, + "Disk Full": None, + "Disk Error": None, + "Net Interface Broken": "connectivity", + "IP Conflict": "connectivity", + "Illegal Access": None, + "Video Mismatch": None, + "Bad Video": None, + "PIR Alarm": "motion", + "Face Detection": "motion", + "Scene Change Detection": "motion", + "I/O": None, + "Unattended Baggage": "motion", + "Attended Baggage": "motion", + "Recording Failure": None, + "Exiting Region": "motion", + "Entering Region": "motion", } -CUSTOMIZE_SCHEMA = vol.Schema({ - vol.Optional(CONF_IGNORED, default=DEFAULT_IGNORED): cv.boolean, - vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int - }) +CUSTOMIZE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_IGNORED, default=DEFAULT_IGNORED): cv.boolean, + vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int, + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CUSTOMIZE, default={}): - vol.Schema({cv.string: CUSTOMIZE_SCHEMA}), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CUSTOMIZE, default={}): vol.Schema( + {cv.string: CUSTOMIZE_SCHEMA} + ), + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -77,11 +89,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): customize = config.get(CONF_CUSTOMIZE) if config.get(CONF_SSL): - protocol = 'https' + protocol = "https" else: - protocol = 'http' + protocol = "http" - url = '{}://{}'.format(protocol, host) + url = "{}://{}".format(protocol, host) data = HikvisionData(hass, url, port, name, username, password) @@ -94,21 +106,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for sensor, channel_list in data.sensors.items(): for channel in channel_list: # Build sensor name, then parse customize config. - if data.type == 'NVR': - sensor_name = '{}_{}'.format( - sensor.replace(' ', '_'), channel[1]) + if data.type == "NVR": + sensor_name = "{}_{}".format(sensor.replace(" ", "_"), channel[1]) else: - sensor_name = sensor.replace(' ', '_') + sensor_name = sensor.replace(" ", "_") custom = customize.get(sensor_name.lower(), {}) ignore = custom.get(CONF_IGNORED) delay = custom.get(CONF_DELAY) - _LOGGER.debug("Entity: %s - %s, Options - Ignore: %s, Delay: %s", - data.name, sensor_name, ignore, delay) + _LOGGER.debug( + "Entity: %s - %s, Options - Ignore: %s, Delay: %s", + data.name, + sensor_name, + ignore, + delay, + ) if not ignore: - entities.append(HikvisionBinarySensor( - hass, sensor, channel[1], data, delay)) + entities.append( + HikvisionBinarySensor(hass, sensor, channel[1], data, delay) + ) add_entities(entities) @@ -119,6 +136,7 @@ class HikvisionData: def __init__(self, hass, url, port, name, username, password): """Initialize the data object.""" from pyhik.hikvision import HikCamera + self._url = url self._port = port self._name = name @@ -126,8 +144,7 @@ class HikvisionData: self._password = password # Establish camera - self.camdata = HikCamera( - self._url, self._port, self._username, self._password) + self.camdata = HikCamera(self._url, self._port, self._username, self._password) if self._name is None: self._name = self.camdata.get_name @@ -178,12 +195,12 @@ class HikvisionBinarySensor(BinarySensorDevice): self._sensor = sensor self._channel = channel - if self._cam.type == 'NVR': - self._name = '{} {} {}'.format(self._cam.name, sensor, channel) + if self._cam.type == "NVR": + self._name = "{} {} {}".format(self._cam.name, sensor, channel) else: - self._name = '{} {}'.format(self._cam.name, sensor) + self._name = "{} {}".format(self._cam.name, sensor) - self._id = '{}.{}.{}'.format(self._cam.cam_id, sensor, channel) + self._id = "{}.{}.{}".format(self._cam.cam_id, sensor, channel) if delay is None: self._delay = 0 @@ -245,14 +262,15 @@ class HikvisionBinarySensor(BinarySensorDevice): def _update_callback(self, msg): """Update the sensor's state, if needed.""" - _LOGGER.debug('Callback signal from: %s', msg) + _LOGGER.debug("Callback signal from: %s", msg) if self._delay > 0 and not self.is_on: # Set timer to wait until updating the state def _delay_update(now): """Timer callback for sensor update.""" - _LOGGER.debug("%s Called delayed (%ssec) update", - self._name, self._delay) + _LOGGER.debug( + "%s Called delayed (%ssec) update", self._name, self._delay + ) self.schedule_update_ha_state() self._timer = None @@ -261,8 +279,8 @@ class HikvisionBinarySensor(BinarySensorDevice): self._timer = None self._timer = track_point_in_utc_time( - self._hass, _delay_update, - utcnow() + timedelta(seconds=self._delay)) + self._hass, _delay_update, utcnow() + timedelta(seconds=self._delay) + ) elif self._delay > 0 and self.is_on: # For delayed sensors kill any callbacks on true events and update diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index 373f84cee0e..05bce5f4eac 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -5,8 +5,14 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, STATE_OFF, - STATE_ON) + CONF_NAME, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + CONF_PORT, + STATE_OFF, + STATE_ON, +) from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv @@ -14,18 +20,20 @@ import homeassistant.helpers.config_validation as cv _LOGGING = logging.getLogger(__name__) -DEFAULT_NAME = 'Hikvision Camera Motion Detection' -DEFAULT_PASSWORD = '12345' +DEFAULT_NAME = "Hikvision Camera Motion Detection" +DEFAULT_PASSWORD = "12345" DEFAULT_PORT = 80 -DEFAULT_USERNAME = 'admin' +DEFAULT_USERNAME = "admin" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -41,8 +49,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: hikvision_cam = hikvision.api.CreateDevice( - host, port=port, username=username, password=password, - is_https=False) + host, port=port, username=username, password=password, is_https=False + ) except MissingParamError as param_err: _LOGGING.error("Missing required param: %s", param_err) return False diff --git a/homeassistant/components/hipchat/notify.py b/homeassistant/components/hipchat/notify.py index f12fd1ffa76..03556db386a 100644 --- a/homeassistant/components/hipchat/notify.py +++ b/homeassistant/components/hipchat/notify.py @@ -6,46 +6,57 @@ import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_ROOM, CONF_TOKEN import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (ATTR_DATA, ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + PLATFORM_SCHEMA, + BaseNotificationService, +) _LOGGER = logging.getLogger(__name__) -CONF_COLOR = 'color' -CONF_NOTIFY = 'notify' -CONF_FORMAT = 'format' +CONF_COLOR = "color" +CONF_NOTIFY = "notify" +CONF_FORMAT = "format" -DEFAULT_COLOR = 'yellow' -DEFAULT_FORMAT = 'text' -DEFAULT_HOST = 'https://api.hipchat.com/' +DEFAULT_COLOR = "yellow" +DEFAULT_FORMAT = "text" +DEFAULT_HOST = "https://api.hipchat.com/" DEFAULT_NOTIFY = False -VALID_COLORS = {'yellow', 'green', 'red', 'purple', 'gray', 'random'} -VALID_FORMATS = {'text', 'html'} +VALID_COLORS = {"yellow", "green", "red", "purple", "gray", "random"} +VALID_FORMATS = {"text", "html"} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ROOM): vol.Coerce(int), - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): vol.In(VALID_COLORS), - vol.Optional(CONF_FORMAT, default=DEFAULT_FORMAT): vol.In(VALID_FORMATS), - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NOTIFY, default=DEFAULT_NOTIFY): cv.boolean, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ROOM): vol.Coerce(int), + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): vol.In(VALID_COLORS), + vol.Optional(CONF_FORMAT, default=DEFAULT_FORMAT): vol.In(VALID_FORMATS), + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NOTIFY, default=DEFAULT_NOTIFY): cv.boolean, + } +) def get_service(hass, config, discovery_info=None): """Get the HipChat notification service.""" return HipchatNotificationService( - config[CONF_TOKEN], config[CONF_ROOM], config[CONF_COLOR], - config[CONF_NOTIFY], config[CONF_FORMAT], config[CONF_HOST]) + config[CONF_TOKEN], + config[CONF_ROOM], + config[CONF_COLOR], + config[CONF_NOTIFY], + config[CONF_FORMAT], + config[CONF_HOST], + ) class HipchatNotificationService(BaseNotificationService): """Implement the notification service for HipChat.""" - def __init__(self, token, default_room, default_color, default_notify, - default_format, host): + def __init__( + self, token, default_room, default_color, default_notify, default_format, host + ): """Initialize the service.""" self._token = token self._default_room = default_room @@ -60,9 +71,11 @@ class HipchatNotificationService(BaseNotificationService): def _get_room(self, room): """Get Room object, creating it if necessary.""" from hipnotify import Room + if room not in self._rooms: self._rooms[room] = Room( - token=self._token, room_id=room, endpoint_url=self._host) + token=self._token, room_id=room, endpoint_url=self._host + ) return self._rooms[room] def send_message(self, message="", **kwargs): @@ -73,19 +86,23 @@ class HipchatNotificationService(BaseNotificationService): if kwargs.get(ATTR_DATA) is not None: data = kwargs.get(ATTR_DATA) - if ((data.get(CONF_COLOR) is not None) - and (data.get(CONF_COLOR) in VALID_COLORS)): + if (data.get(CONF_COLOR) is not None) and ( + data.get(CONF_COLOR) in VALID_COLORS + ): color = data.get(CONF_COLOR) - if ((data.get(CONF_NOTIFY) is not None) - and isinstance(data.get(CONF_NOTIFY), bool)): + if (data.get(CONF_NOTIFY) is not None) and isinstance( + data.get(CONF_NOTIFY), bool + ): notify = data.get(CONF_NOTIFY) - if ((data.get(CONF_FORMAT) is not None) - and (data.get(CONF_FORMAT) in VALID_FORMATS)): + if (data.get(CONF_FORMAT) is not None) and ( + data.get(CONF_FORMAT) in VALID_FORMATS + ): message_format = data.get(CONF_FORMAT) targets = kwargs.get(ATTR_TARGET, [self._default_room]) for target in targets: room = self._get_room(target) - room.notify(msg=message, color=color, notify=notify, - message_format=message_format) + room.notify( + msg=message, color=color, notify=notify, message_format=message_format + ) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index d0dd098638f..d402aceaa40 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -8,7 +8,12 @@ import time import voluptuous as vol from homeassistant.const import ( - HTTP_BAD_REQUEST, CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE) + HTTP_BAD_REQUEST, + CONF_DOMAINS, + CONF_ENTITIES, + CONF_EXCLUDE, + CONF_INCLUDE, +) import homeassistant.util.dt as dt_util from homeassistant.components import recorder, script from homeassistant.components.http import HomeAssistantView @@ -18,21 +23,30 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DOMAIN = 'history' -CONF_ORDER = 'use_include_order' +DOMAIN = "history" +CONF_ORDER = "use_include_order" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: recorder.FILTER_SCHEMA.extend({ - vol.Optional(CONF_ORDER, default=False): cv.boolean, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: recorder.FILTER_SCHEMA.extend( + {vol.Optional(CONF_ORDER, default=False): cv.boolean} + ) + }, + extra=vol.ALLOW_EXTRA, +) -SIGNIFICANT_DOMAINS = ('thermostat', 'climate', 'water_heater') -IGNORE_DOMAINS = ('zone', 'scene',) +SIGNIFICANT_DOMAINS = ("thermostat", "climate", "water_heater") +IGNORE_DOMAINS = ("zone", "scene") -def get_significant_states(hass, start_time, end_time=None, entity_ids=None, - filters=None, include_start_time_state=True): +def get_significant_states( + hass, + start_time, + end_time=None, + entity_ids=None, + filters=None, + include_start_time_state=True, +): """ Return states changes during UTC period start_time - end_time. @@ -45,9 +59,12 @@ def get_significant_states(hass, start_time, end_time=None, entity_ids=None, with session_scope(hass=hass) as session: query = session.query(States).filter( - (States.domain.in_(SIGNIFICANT_DOMAINS) | - (States.last_changed == States.last_updated)) & - (States.last_updated > start_time)) + ( + States.domain.in_(SIGNIFICANT_DOMAINS) + | (States.last_changed == States.last_updated) + ) + & (States.last_updated > start_time) + ) if filters: query = filters.apply(query, entity_ids) @@ -58,29 +75,29 @@ def get_significant_states(hass, start_time, end_time=None, entity_ids=None, query = query.order_by(States.last_updated) states = ( - state for state in execute(query) - if (_is_significant(state) and - not state.attributes.get(ATTR_HIDDEN, False))) + state + for state in execute(query) + if (_is_significant(state) and not state.attributes.get(ATTR_HIDDEN, False)) + ) if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start - _LOGGER.debug( - 'get_significant_states took %fs', elapsed) + _LOGGER.debug("get_significant_states took %fs", elapsed) return states_to_json( - hass, states, start_time, entity_ids, filters, - include_start_time_state) + hass, states, start_time, entity_ids, filters, include_start_time_state + ) -def state_changes_during_period(hass, start_time, end_time=None, - entity_id=None): +def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): """Return states changes during UTC period start_time - end_time.""" from homeassistant.components.recorder.models import States with session_scope(hass=hass) as session: query = session.query(States).filter( - (States.last_changed == States.last_updated) & - (States.last_updated > start_time)) + (States.last_changed == States.last_updated) + & (States.last_updated > start_time) + ) if end_time is not None: query = query.filter(States.last_updated < end_time) @@ -90,8 +107,7 @@ def state_changes_during_period(hass, start_time, end_time=None, entity_ids = [entity_id] if entity_id is not None else None - states = execute( - query.order_by(States.last_updated)) + states = execute(query.order_by(States.last_updated)) return states_to_json(hass, states, start_time, entity_ids) @@ -104,7 +120,8 @@ def get_last_state_changes(hass, number_of_states, entity_id): with session_scope(hass=hass) as session: query = session.query(States).filter( - (States.last_changed == States.last_updated)) + (States.last_changed == States.last_updated) + ) if entity_id is not None: query = query.filter_by(entity_id=entity_id.lower()) @@ -112,16 +129,15 @@ def get_last_state_changes(hass, number_of_states, entity_id): entity_ids = [entity_id] if entity_id is not None else None states = execute( - query.order_by(States.last_updated.desc()).limit(number_of_states)) + query.order_by(States.last_updated.desc()).limit(number_of_states) + ) - return states_to_json(hass, reversed(states), - start_time, - entity_ids, - include_start_time_state=False) + return states_to_json( + hass, reversed(states), start_time, entity_ids, include_start_time_state=False + ) -def get_states(hass, utc_point_in_time, entity_ids=None, run=None, - filters=None): +def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): """Return the states at a specific point in time.""" from homeassistant.components.recorder.models import States @@ -138,13 +154,14 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None, if entity_ids and len(entity_ids) == 1: # Use an entirely different (and extremely fast) query if we only # have a single entity id - most_recent_state_ids = session.query( - States.state_id.label('max_state_id') - ).filter( - (States.last_updated < utc_point_in_time) & - (States.entity_id.in_(entity_ids)) - ).order_by( - States.last_updated.desc()) + most_recent_state_ids = ( + session.query(States.state_id.label("max_state_id")) + .filter( + (States.last_updated < utc_point_in_time) + & (States.entity_id.in_(entity_ids)) + ) + .order_by(States.last_updated.desc()) + ) most_recent_state_ids = most_recent_state_ids.limit(1) @@ -154,53 +171,59 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None, # last recorder run started. most_recent_states_by_date = session.query( - States.entity_id.label('max_entity_id'), - func.max(States.last_updated).label('max_last_updated') + States.entity_id.label("max_entity_id"), + func.max(States.last_updated).label("max_last_updated"), ).filter( - (States.last_updated >= run.start) & - (States.last_updated < utc_point_in_time) + (States.last_updated >= run.start) + & (States.last_updated < utc_point_in_time) ) if entity_ids: - most_recent_states_by_date.filter( - States.entity_id.in_(entity_ids)) + most_recent_states_by_date.filter(States.entity_id.in_(entity_ids)) most_recent_states_by_date = most_recent_states_by_date.group_by( - States.entity_id) + States.entity_id + ) most_recent_states_by_date = most_recent_states_by_date.subquery() most_recent_state_ids = session.query( - func.max(States.state_id).label('max_state_id') - ).join(most_recent_states_by_date, and_( - States.entity_id == most_recent_states_by_date.c.max_entity_id, - States.last_updated == most_recent_states_by_date.c. - max_last_updated)) + func.max(States.state_id).label("max_state_id") + ).join( + most_recent_states_by_date, + and_( + States.entity_id == most_recent_states_by_date.c.max_entity_id, + States.last_updated + == most_recent_states_by_date.c.max_last_updated, + ), + ) - most_recent_state_ids = most_recent_state_ids.group_by( - States.entity_id) + most_recent_state_ids = most_recent_state_ids.group_by(States.entity_id) most_recent_state_ids = most_recent_state_ids.subquery() - query = session.query(States).join( - most_recent_state_ids, - States.state_id == most_recent_state_ids.c.max_state_id - ).filter((~States.domain.in_(IGNORE_DOMAINS))) + query = ( + session.query(States) + .join( + most_recent_state_ids, + States.state_id == most_recent_state_ids.c.max_state_id, + ) + .filter((~States.domain.in_(IGNORE_DOMAINS))) + ) if filters: query = filters.apply(query, entity_ids) - return [state for state in execute(query) - if not state.attributes.get(ATTR_HIDDEN, False)] + return [ + state + for state in execute(query) + if not state.attributes.get(ATTR_HIDDEN, False) + ] def states_to_json( - hass, - states, - start_time, - entity_ids, - filters=None, - include_start_time_state=True): + hass, states, start_time, entity_ids, filters=None, include_start_time_state=True +): """Convert SQL results into JSON friendly data structure. This takes our state list and turns it into a JSON friendly data @@ -211,6 +234,10 @@ def states_to_json( axis correctly. """ result = defaultdict(list) + # Set all entity IDs to empty lists in result set to maintain the order + if entity_ids is not None: + for ent_id in entity_ids: + result[ent_id] = [] # Get the states at the start time timer_start = time.perf_counter() @@ -222,13 +249,14 @@ def states_to_json( if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start - _LOGGER.debug( - 'getting %d first datapoints took %fs', len(result), elapsed) + _LOGGER.debug("getting %d first datapoints took %fs", len(result), elapsed) # Append all changes to it for ent_id, group in groupby(states, lambda state: state.entity_id): result[ent_id].extend(group) - return result + + # Filter out the empty lists if some states had 0 results. + return {key: val for key, val in result.items() if val} def get_state(hass, utc_point_in_time, entity_id, run=None): @@ -253,7 +281,8 @@ async def async_setup(hass, config): hass.http.register_view(HistoryPeriodView(filters, use_include_order)) hass.components.frontend.async_register_built_in_panel( - 'history', 'history', 'hass:poll-box') + "history", "history", "hass:poll-box" + ) return True @@ -261,9 +290,9 @@ async def async_setup(hass, config): class HistoryPeriodView(HomeAssistantView): """Handle history period requests.""" - url = '/api/history/period' - name = 'api:history:view-period' - extra_urls = ['/api/history/period/{datetime}'] + url = "/api/history/period" + name = "api:history:view-period" + extra_urls = ["/api/history/period/{datetime}"] def __init__(self, filters, use_include_order): """Initialize the history period view.""" @@ -277,7 +306,7 @@ class HistoryPeriodView(HomeAssistantView): datetime = dt_util.parse_datetime(datetime) if datetime is None: - return self.json_message('Invalid datetime', HTTP_BAD_REQUEST) + return self.json_message("Invalid datetime", HTTP_BAD_REQUEST) now = dt_util.utcnow() @@ -290,34 +319,38 @@ class HistoryPeriodView(HomeAssistantView): if start_time > now: return self.json([]) - end_time = request.query.get('end_time') + end_time = request.query.get("end_time") if end_time: end_time = dt_util.parse_datetime(end_time) if end_time: end_time = dt_util.as_utc(end_time) else: - return self.json_message('Invalid end_time', HTTP_BAD_REQUEST) + return self.json_message("Invalid end_time", HTTP_BAD_REQUEST) else: end_time = start_time + one_day - entity_ids = request.query.get('filter_entity_id') + entity_ids = request.query.get("filter_entity_id") if entity_ids: - entity_ids = entity_ids.lower().split(',') - include_start_time_state = 'skip_initial_state' not in request.query + entity_ids = entity_ids.lower().split(",") + include_start_time_state = "skip_initial_state" not in request.query - hass = request.app['hass'] + hass = request.app["hass"] result = await hass.async_add_job( - get_significant_states, hass, start_time, end_time, - entity_ids, self.filters, include_start_time_state) + get_significant_states, + hass, + start_time, + end_time, + entity_ids, + self.filters, + include_start_time_state, + ) result = list(result.values()) if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start - _LOGGER.debug( - 'Extracted %d states in %fs', sum(map(len, result)), elapsed) + _LOGGER.debug("Extracted %d states in %fs", sum(map(len, result)), elapsed) # Optionally reorder the result to respect the ordering given # by any entities explicitly included in the configuration. - if self.use_include_order: sorted_result = [] for order_entity in self.filters.included_entities: @@ -375,14 +408,19 @@ class Filters: elif self.excluded_domains and self.included_domains: filter_query = ~States.domain.in_(self.excluded_domains) if self.included_entities: - filter_query &= (States.domain.in_(self.included_domains) | - States.entity_id.in_(self.included_entities)) + filter_query &= States.domain.in_( + self.included_domains + ) | States.entity_id.in_(self.included_entities) else: - filter_query &= (States.domain.in_(self.included_domains) & ~ - States.domain.in_(self.excluded_domains)) + filter_query &= States.domain.in_( + self.included_domains + ) & ~States.domain.in_(self.excluded_domains) # no domain filter just included entities - elif not self.excluded_domains and not self.included_domains and \ - self.included_entities: + elif ( + not self.excluded_domains + and not self.included_domains + and self.included_entities + ): filter_query = States.entity_id.in_(self.included_entities) if filter_query is not None: query = query.filter(filter_query) @@ -398,5 +436,4 @@ def _is_significant(state): Will only test for things that are not filtered out in SQL. """ # scripts that are not cancellable will never change state - return (state.domain != 'script' or - state.attributes.get(script.ATTR_CAN_CANCEL)) + return state.domain != "script" or state.attributes.get(script.ATTR_CAN_CANCEL) diff --git a/homeassistant/components/history_graph/__init__.py b/homeassistant/components/history_graph/__init__.py index 964d47d2502..ad8398c75f5 100644 --- a/homeassistant/components/history_graph/__init__.py +++ b/homeassistant/components/history_graph/__init__.py @@ -10,31 +10,32 @@ from homeassistant.helpers.entity_component import EntityComponent _LOGGER = logging.getLogger(__name__) -DOMAIN = 'history_graph' +DOMAIN = "history_graph" -CONF_HOURS_TO_SHOW = 'hours_to_show' -CONF_REFRESH = 'refresh' +CONF_HOURS_TO_SHOW = "hours_to_show" +CONF_REFRESH = "refresh" ATTR_HOURS_TO_SHOW = CONF_HOURS_TO_SHOW ATTR_REFRESH = CONF_REFRESH -GRAPH_SCHEMA = vol.Schema({ - vol.Required(CONF_ENTITIES): cv.entity_ids, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_HOURS_TO_SHOW, default=24): vol.Range(min=1), - vol.Optional(CONF_REFRESH, default=0): vol.Range(min=0), -}) +GRAPH_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITIES): cv.entity_ids, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_HOURS_TO_SHOW, default=24): vol.Range(min=1), + vol.Optional(CONF_REFRESH, default=0): vol.Range(min=0), + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys(GRAPH_SCHEMA), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: cv.schema_with_slug_keys(GRAPH_SCHEMA)}, extra=vol.ALLOW_EXTRA +) async def async_setup(hass, config): """Load graph configurations.""" - component = EntityComponent( - _LOGGER, DOMAIN, hass) + component = EntityComponent(_LOGGER, DOMAIN, hass) graphs = [] for object_id, cfg in config[DOMAIN].items(): diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index a0a08d4833e..5c59b5f8e97 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -11,53 +11,59 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_ENTITY_ID, CONF_STATE, CONF_TYPE, - EVENT_HOMEASSISTANT_START) + CONF_NAME, + CONF_ENTITY_ID, + CONF_STATE, + CONF_TYPE, + EVENT_HOMEASSISTANT_START, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change _LOGGER = logging.getLogger(__name__) -DOMAIN = 'history_stats' -CONF_START = 'start' -CONF_END = 'end' -CONF_DURATION = 'duration' +DOMAIN = "history_stats" +CONF_START = "start" +CONF_END = "end" +CONF_DURATION = "duration" CONF_PERIOD_KEYS = [CONF_START, CONF_END, CONF_DURATION] -CONF_TYPE_TIME = 'time' -CONF_TYPE_RATIO = 'ratio' -CONF_TYPE_COUNT = 'count' +CONF_TYPE_TIME = "time" +CONF_TYPE_RATIO = "ratio" +CONF_TYPE_COUNT = "count" CONF_TYPE_KEYS = [CONF_TYPE_TIME, CONF_TYPE_RATIO, CONF_TYPE_COUNT] -DEFAULT_NAME = 'unnamed statistics' -UNITS = { - CONF_TYPE_TIME: 'h', - CONF_TYPE_RATIO: '%', - CONF_TYPE_COUNT: '' -} -ICON = 'mdi:chart-line' +DEFAULT_NAME = "unnamed statistics" +UNITS = {CONF_TYPE_TIME: "h", CONF_TYPE_RATIO: "%", CONF_TYPE_COUNT: ""} +ICON = "mdi:chart-line" -ATTR_VALUE = 'value' +ATTR_VALUE = "value" def exactly_two_period_keys(conf): """Ensure exactly 2 of CONF_PERIOD_KEYS are provided.""" if sum(param in conf for param in CONF_PERIOD_KEYS) != 2: - raise vol.Invalid('You must provide exactly 2 of the following:' - ' start, end, duration') + raise vol.Invalid( + "You must provide exactly 2 of the following:" " start, end, duration" + ) return conf -PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_STATE): cv.string, - vol.Optional(CONF_START): cv.template, - vol.Optional(CONF_END): cv.template, - vol.Optional(CONF_DURATION): cv.time_period, - vol.Optional(CONF_TYPE, default=CONF_TYPE_TIME): vol.In(CONF_TYPE_KEYS), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -}), exactly_two_period_keys) +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_STATE): cv.string, + vol.Optional(CONF_START): cv.template, + vol.Optional(CONF_END): cv.template, + vol.Optional(CONF_DURATION): cv.time_period, + vol.Optional(CONF_TYPE, default=CONF_TYPE_TIME): vol.In(CONF_TYPE_KEYS), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } + ), + exactly_two_period_keys, +) # noinspection PyUnusedLocal @@ -75,8 +81,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if template is not None: template.hass = hass - add_entities([HistoryStatsSensor(hass, entity_id, entity_state, start, end, - duration, sensor_type, name)]) + add_entities( + [ + HistoryStatsSensor( + hass, entity_id, entity_state, start, end, duration, sensor_type, name + ) + ] + ) return True @@ -85,8 +96,8 @@ class HistoryStatsSensor(Entity): """Representation of a HistoryStats sensor.""" def __init__( - self, hass, entity_id, entity_state, start, end, duration, - sensor_type, name): + self, hass, entity_id, entity_state, start, end, duration, sensor_type, name + ): """Initialize the HistoryStats sensor.""" self._entity_id = entity_id self._entity_state = entity_state @@ -104,6 +115,7 @@ class HistoryStatsSensor(Entity): @callback def start_refresh(*args): """Register state tracking.""" + @callback def force_refresh(*args): """Force the component to refresh.""" @@ -152,9 +164,7 @@ class HistoryStatsSensor(Entity): return {} hsh = HistoryStatsHelper - return { - ATTR_VALUE: hsh.pretty_duration(self.value), - } + return {ATTR_VALUE: hsh.pretty_duration(self.value)} @property def icon(self): @@ -185,23 +195,25 @@ class HistoryStatsSensor(Entity): now_timestamp = math.floor(dt_util.as_timestamp(now)) # If period has not changed and current time after the period end... - if start_timestamp == p_start_timestamp and \ - end_timestamp == p_end_timestamp and \ - end_timestamp <= now_timestamp: + if ( + start_timestamp == p_start_timestamp + and end_timestamp == p_end_timestamp + and end_timestamp <= now_timestamp + ): # Don't compute anything as the value cannot have changed return # Get history between start and end history_list = history.state_changes_during_period( - self.hass, start, end, str(self._entity_id)) + self.hass, start, end, str(self._entity_id) + ) if self._entity_id not in history_list.keys(): return # Get the first state last_state = history.get_state(self.hass, start, self._entity_id) - last_state = (last_state is not None and - last_state == self._entity_state) + last_state = last_state is not None and last_state == self._entity_state last_time = start_timestamp elapsed = 0 count = 0 @@ -240,16 +252,18 @@ class HistoryStatsSensor(Entity): try: start_rendered = self._start.render() except (TemplateError, TypeError) as ex: - HistoryStatsHelper.handle_template_exception(ex, 'start') + HistoryStatsHelper.handle_template_exception(ex, "start") return start = dt_util.parse_datetime(start_rendered) if start is None: try: - start = dt_util.as_local(dt_util.utc_from_timestamp( - math.floor(float(start_rendered)))) + start = dt_util.as_local( + dt_util.utc_from_timestamp(math.floor(float(start_rendered))) + ) except ValueError: - _LOGGER.error("Parsing error: start must be a datetime" - "or a timestamp") + _LOGGER.error( + "Parsing error: start must be a datetime" "or a timestamp" + ) return # Parse end @@ -257,16 +271,18 @@ class HistoryStatsSensor(Entity): try: end_rendered = self._end.render() except (TemplateError, TypeError) as ex: - HistoryStatsHelper.handle_template_exception(ex, 'end') + HistoryStatsHelper.handle_template_exception(ex, "end") return end = dt_util.parse_datetime(end_rendered) if end is None: try: - end = dt_util.as_local(dt_util.utc_from_timestamp( - math.floor(float(end_rendered)))) + end = dt_util.as_local( + dt_util.utc_from_timestamp(math.floor(float(end_rendered))) + ) except ValueError: - _LOGGER.error("Parsing error: end must be a datetime " - "or a timestamp") + _LOGGER.error( + "Parsing error: end must be a datetime " "or a timestamp" + ) return # Calculate start or end using the duration @@ -296,10 +312,10 @@ class HistoryStatsHelper: hours, seconds = divmod(seconds, 3600) minutes, seconds = divmod(seconds, 60) if days > 0: - return '%dd %dh %dm' % (days, hours, minutes) + return "%dd %dh %dm" % (days, hours, minutes) if hours > 0: - return '%dh %dm' % (hours, minutes) - return '%dm' % minutes + return "%dh %dm" % (hours, minutes) + return "%dm" % minutes @staticmethod def pretty_ratio(value, period): @@ -313,8 +329,7 @@ class HistoryStatsHelper: @staticmethod def handle_template_exception(ex, field): """Log an error nicely if the template cannot be interpreted.""" - if ex.args and ex.args[0].startswith( - "UndefinedError: 'None' has no attribute"): + if ex.args and ex.args[0].startswith("UndefinedError: 'None' has no attribute"): # Common during HA startup - so just a warning _LOGGER.warning(ex) return diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index e6f68d704fd..e3e8975c125 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -7,21 +7,24 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, ) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE _LOGGER = logging.getLogger(__name__) DEFAULT_TYPE = "rogers" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): cv.string, + } +) def get_scanner(_hass, config): @@ -31,7 +34,7 @@ def get_scanner(_hass, config): return scanner if scanner.success_init else None -Device = namedtuple('Device', ['mac', 'name']) +Device = namedtuple("Device", ["mac", "name"]) class HitronCODADeviceScanner(DeviceScanner): @@ -41,16 +44,16 @@ class HitronCODADeviceScanner(DeviceScanner): """Initialize the scanner.""" self.last_results = [] host = config[CONF_HOST] - self._url = 'http://{}/data/getConnectInfo.asp'.format(host) - self._loginurl = 'http://{}/goform/login'.format(host) + self._url = "http://{}/data/getConnectInfo.asp".format(host) + self._loginurl = "http://{}/goform/login".format(host) self._username = config.get(CONF_USERNAME) self._password = config.get(CONF_PASSWORD) if config.get(CONF_TYPE) == "shaw": - self._type = 'pwd' + self._type = "pwd" else: - self._type = 'pws' + self._type = "pws" self._userid = None @@ -65,9 +68,9 @@ class HitronCODADeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the device with the given MAC address.""" - name = next(( - result.name for result in self.last_results - if result.mac == device), None) + name = next( + (result.name for result in self.last_results if result.mac == device), None + ) return name def _login(self): @@ -75,21 +78,16 @@ class HitronCODADeviceScanner(DeviceScanner): _LOGGER.info("Logging in to CODA...") try: - data = [ - ('user', self._username), - (self._type, self._password), - ] + data = [("user", self._username), (self._type, self._password)] res = requests.post(self._loginurl, data=data, timeout=10) except requests.exceptions.Timeout: - _LOGGER.error( - "Connection to the router timed out at URL %s", self._url) + _LOGGER.error("Connection to the router timed out at URL %s", self._url) return False if res.status_code != 200: - _LOGGER.error( - "Connection failed with http code %s", res.status_code) + _LOGGER.error("Connection failed with http code %s", res.status_code) return False try: - self._userid = res.cookies['userid'] + self._userid = res.cookies["userid"] return True except KeyError: _LOGGER.error("Failed to log in to router") @@ -107,16 +105,12 @@ class HitronCODADeviceScanner(DeviceScanner): # doing a request try: - res = requests.get(self._url, timeout=10, cookies={ - 'userid': self._userid - }) + res = requests.get(self._url, timeout=10, cookies={"userid": self._userid}) except requests.exceptions.Timeout: - _LOGGER.error( - "Connection to the router timed out at URL %s", self._url) + _LOGGER.error("Connection to the router timed out at URL %s", self._url) return False if res.status_code != 200: - _LOGGER.error( - "Connection failed with http code %s", res.status_code) + _LOGGER.error("Connection failed with http code %s", res.status_code) return False try: result = res.json() @@ -127,8 +121,8 @@ class HitronCODADeviceScanner(DeviceScanner): # parsing response for info in result: - mac = info['macAddr'] - name = info['hostName'] + mac = info["macAddr"] + name = info["hostName"] # No address = no item :) if mac is None: continue diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 7ad1cc002f9..fc96f2d8c96 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -4,31 +4,35 @@ import logging from pyhiveapi import Pyhiveapi import voluptuous as vol -from homeassistant.const import ( - CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME) +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform _LOGGER = logging.getLogger(__name__) -DOMAIN = 'hive' -DATA_HIVE = 'data_hive' +DOMAIN = "hive" +DATA_HIVE = "data_hive" DEVICETYPES = { - 'binary_sensor': 'device_list_binary_sensor', - 'climate': 'device_list_climate', - 'water_heater': 'device_list_water_heater', - 'light': 'device_list_light', - 'switch': 'device_list_plug', - 'sensor': 'device_list_sensor', + "binary_sensor": "device_list_binary_sensor", + "climate": "device_list_climate", + "water_heater": "device_list_water_heater", + "light": "device_list_light", + "switch": "device_list_plug", + "sensor": "device_list_sensor", } -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=2): cv.positive_int, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=2): cv.positive_int, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) class HiveSession: @@ -54,8 +58,7 @@ def setup(hass, config): password = config[DOMAIN][CONF_PASSWORD] update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] - devicelist = session.core.initialise_api( - username, password, update_interval) + devicelist = session.core.initialise_api(username, password, update_interval) if devicelist is None: _LOGGER.error("Hive API initialization failed") diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 97900c2852e..80aaaf86463 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -3,10 +3,7 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from . import DATA_HIVE, DOMAIN -DEVICETYPE_DEVICE_CLASS = { - 'motionsensor': 'motion', - 'contactsensor': 'opening', -} +DEVICETYPE_DEVICE_CLASS = {"motionsensor": "motion", "contactsensor": "opening"} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -29,9 +26,8 @@ class HiveBinarySensorEntity(BinarySensorDevice): self.node_device_type = hivedevice["Hive_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) - self._unique_id = '{}-{}'.format(self.node_id, self.device_type) + self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) + self._unique_id = "{}-{}".format(self.node_id, self.device_type) self.session.entities.append(self) @property @@ -42,16 +38,11 @@ class HiveBinarySensorEntity(BinarySensorDevice): @property def device_info(self): """Return device information.""" - return { - 'identifiers': { - (DOMAIN, self.unique_id) - }, - 'name': self.name - } + return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} def handle_update(self, updatesource): """Handle the new update request.""" - if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: + if "{}.{}".format(self.device_type, self.node_id) not in updatesource: self.schedule_update_ha_state() @property @@ -72,11 +63,9 @@ class HiveBinarySensorEntity(BinarySensorDevice): @property def is_on(self): """Return true if the binary sensor is on.""" - return self.session.sensor.get_state( - self.node_id, self.node_device_type) + return self.session.sensor.get_state(self.node_id, self.node_device_type) def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) - self.attributes = self.session.attributes.state_attributes( - self.node_id) + self.attributes = self.session.attributes.state_attributes(self.node_id) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index bfc43e3357f..d4a1c915518 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,27 +1,33 @@ """Support for the Hive climate devices.""" from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_BOOST, - SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE) + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_BOOST, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + PRESET_NONE, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from . import DATA_HIVE, DOMAIN HIVE_TO_HASS_STATE = { - 'SCHEDULE': HVAC_MODE_AUTO, - 'MANUAL': HVAC_MODE_HEAT, - 'OFF': HVAC_MODE_OFF, + "SCHEDULE": HVAC_MODE_AUTO, + "MANUAL": HVAC_MODE_HEAT, + "OFF": HVAC_MODE_OFF, } HASS_TO_HIVE_STATE = { - HVAC_MODE_AUTO: 'SCHEDULE', - HVAC_MODE_HEAT: 'MANUAL', - HVAC_MODE_OFF: 'OFF', + HVAC_MODE_AUTO: "SCHEDULE", + HVAC_MODE_HEAT: "MANUAL", + HVAC_MODE_OFF: "OFF", } SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] -SUPPORT_PRESET = [PRESET_BOOST] +SUPPORT_PRESET = [PRESET_NONE, PRESET_BOOST] def setup_platform(hass, config, add_entities, discovery_info=None): @@ -48,9 +54,8 @@ class HiveClimateEntity(ClimateDevice): self.thermostat_node_id = hivedevice["Thermostat_NodeID"] self.session = hivesession self.attributes = {} - self.data_updatesource = '{}.{}'.format( - self.device_type, self.node_id) - self._unique_id = '{}-{}'.format(self.node_id, self.device_type) + self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) + self._unique_id = "{}-{}".format(self.node_id, self.device_type) @property def unique_id(self): @@ -60,12 +65,7 @@ class HiveClimateEntity(ClimateDevice): @property def device_info(self): """Return device information.""" - return { - 'identifiers': { - (DOMAIN, self.unique_id) - }, - 'name': self.name - } + return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} @property def supported_features(self): @@ -74,7 +74,7 @@ class HiveClimateEntity(ClimateDevice): def handle_update(self, updatesource): """Handle the new update request.""" - if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: + if "{}.{}".format(self.device_type, self.node_id) not in updatesource: self.schedule_update_ha_state() @property @@ -82,7 +82,7 @@ class HiveClimateEntity(ClimateDevice): """Return the name of the Climate device.""" friendly_name = "Heating" if self.node_name is not None: - friendly_name = '{} {}'.format(self.node_name, friendly_name) + friendly_name = "{} {}".format(self.node_name, friendly_name) return friendly_name @property @@ -160,15 +160,14 @@ class HiveClimateEntity(ClimateDevice): """Set new target temperature.""" new_temperature = kwargs.get(ATTR_TEMPERATURE) if new_temperature is not None: - self.session.heating.set_target_temperature( - self.node_id, new_temperature) + self.session.heating.set_target_temperature(self.node_id, new_temperature) for entity in self.session.entities: entity.handle_update(self.data_updatesource) def set_preset_mode(self, preset_mode) -> None: """Set new preset mode.""" - if preset_mode is None and self.preset_mode == PRESET_BOOST: + if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST: self.session.heating.turn_boost_off(self.node_id) elif preset_mode == PRESET_BOOST: @@ -185,4 +184,5 @@ class HiveClimateEntity(ClimateDevice): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) self.attributes = self.session.attributes.state_attributes( - self.thermostat_node_id) + self.thermostat_node_id + ) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 67331b12b35..5892e304379 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -1,7 +1,13 @@ """Support for the Hive lights.""" from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + Light, +) import homeassistant.util.color as color_util from . import DATA_HIVE, DOMAIN @@ -27,9 +33,8 @@ class HiveDeviceLight(Light): self.light_device_type = hivedevice["Hive_Light_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = '{}.{}'.format( - self.device_type, self.node_id) - self._unique_id = '{}-{}'.format(self.node_id, self.device_type) + self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) + self._unique_id = "{}-{}".format(self.node_id, self.device_type) self.session.entities.append(self) @property @@ -40,16 +45,11 @@ class HiveDeviceLight(Light): @property def device_info(self): """Return device information.""" - return { - 'identifiers': { - (DOMAIN, self.unique_id) - }, - 'name': self.name - } + return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} def handle_update(self, updatesource): """Handle the new update request.""" - if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: + if "{}.{}".format(self.device_type, self.node_id) not in updatesource: self.schedule_update_ha_state() @property @@ -70,22 +70,28 @@ class HiveDeviceLight(Light): @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" - if self.light_device_type == "tuneablelight" \ - or self.light_device_type == "colourtuneablelight": + if ( + self.light_device_type == "tuneablelight" + or self.light_device_type == "colourtuneablelight" + ): return self.session.light.get_min_color_temp(self.node_id) @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" - if self.light_device_type == "tuneablelight" \ - or self.light_device_type == "colourtuneablelight": + if ( + self.light_device_type == "tuneablelight" + or self.light_device_type == "colourtuneablelight" + ): return self.session.light.get_max_color_temp(self.node_id) @property def color_temp(self): """Return the CT color value in mireds.""" - if self.light_device_type == "tuneablelight" \ - or self.light_device_type == "colourtuneablelight": + if ( + self.light_device_type == "tuneablelight" + or self.light_device_type == "colourtuneablelight" + ): return self.session.light.get_color_temp(self.node_id) @property @@ -107,7 +113,7 @@ class HiveDeviceLight(Light): new_color = None if ATTR_BRIGHTNESS in kwargs: tmp_new_brightness = kwargs.get(ATTR_BRIGHTNESS) - percentage_brightness = ((tmp_new_brightness / 255) * 100) + percentage_brightness = (tmp_new_brightness / 255) * 100 new_brightness = int(round(percentage_brightness / 5.0) * 5.0) if new_brightness == 0: new_brightness = 5 @@ -120,9 +126,13 @@ class HiveDeviceLight(Light): saturation = int(get_new_color[1]) new_color = (hue, saturation, 100) - self.session.light.turn_on(self.node_id, self.light_device_type, - new_brightness, new_color_temp, - new_color) + self.session.light.turn_on( + self.node_id, + self.light_device_type, + new_brightness, + new_color_temp, + new_color, + ) for entity in self.session.entities: entity.handle_update(self.data_updatesource) @@ -140,15 +150,13 @@ class HiveDeviceLight(Light): if self.light_device_type == "warmwhitelight": supported_features = SUPPORT_BRIGHTNESS elif self.light_device_type == "tuneablelight": - supported_features = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP) + supported_features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP elif self.light_device_type == "colourtuneablelight": - supported_features = ( - SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR) + supported_features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR return supported_features def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) - self.attributes = self.session.attributes.state_attributes( - self.node_id) + self.attributes = self.session.attributes.state_attributes(self.node_id) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index b8887d27409..dd3343633d8 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -5,13 +5,13 @@ from homeassistant.helpers.entity import Entity from . import DATA_HIVE, DOMAIN FRIENDLY_NAMES = { - 'Hub_OnlineStatus': 'Hive Hub Status', - 'Hive_OutsideTemperature': 'Outside Temperature', + "Hub_OnlineStatus": "Hive Hub Status", + "Hive_OutsideTemperature": "Outside Temperature", } DEVICETYPE_ICONS = { - 'Hub_OnlineStatus': 'mdi:switch', - 'Hive_OutsideTemperature': 'mdi:thermometer', + "Hub_OnlineStatus": "mdi:switch", + "Hive_OutsideTemperature": "mdi:thermometer", } @@ -21,8 +21,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return session = hass.data.get(DATA_HIVE) - if (discovery_info["HA_DeviceType"] == "Hub_OnlineStatus" or - discovery_info["HA_DeviceType"] == "Hive_OutsideTemperature"): + if ( + discovery_info["HA_DeviceType"] == "Hub_OnlineStatus" + or discovery_info["HA_DeviceType"] == "Hive_OutsideTemperature" + ): add_entities([HiveSensorEntity(session, discovery_info)]) @@ -35,9 +37,8 @@ class HiveSensorEntity(Entity): self.device_type = hivedevice["HA_DeviceType"] self.node_device_type = hivedevice["Hive_DeviceType"] self.session = hivesession - self.data_updatesource = '{}.{}'.format( - self.device_type, self.node_id) - self._unique_id = '{}-{}'.format(self.node_id, self.device_type) + self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) + self._unique_id = "{}-{}".format(self.node_id, self.device_type) self.session.entities.append(self) @property @@ -48,16 +49,11 @@ class HiveSensorEntity(Entity): @property def device_info(self): """Return device information.""" - return { - 'identifiers': { - (DOMAIN, self.unique_id) - }, - 'name': self.name - } + return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} def handle_update(self, updatesource): """Handle the new update request.""" - if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: + if "{}.{}".format(self.device_type, self.node_id) not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index ea4094d573c..4644ccaec00 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -23,9 +23,8 @@ class HiveDevicePlug(SwitchDevice): self.device_type = hivedevice["HA_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = '{}.{}'.format( - self.device_type, self.node_id) - self._unique_id = '{}-{}'.format(self.node_id, self.device_type) + self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) + self._unique_id = "{}-{}".format(self.node_id, self.device_type) self.session.entities.append(self) @property @@ -36,16 +35,11 @@ class HiveDevicePlug(SwitchDevice): @property def device_info(self): """Return device information.""" - return { - 'identifiers': { - (DOMAIN, self.unique_id) - }, - 'name': self.name - } + return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} def handle_update(self, updatesource): """Handle the new update request.""" - if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: + if "{}.{}".format(self.device_type, self.node_id) not in updatesource: self.schedule_update_ha_state() @property @@ -83,5 +77,4 @@ class HiveDevicePlug(SwitchDevice): def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) - self.attributes = self.session.attributes.state_attributes( - self.node_id) + self.attributes = self.session.attributes.state_attributes(self.node_id) diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 943abde5dc7..f186d804d34 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -2,23 +2,20 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.components.water_heater import ( - STATE_ECO, STATE_ON, STATE_OFF, SUPPORT_OPERATION_MODE, WaterHeaterDevice) + STATE_ECO, + STATE_ON, + STATE_OFF, + SUPPORT_OPERATION_MODE, + WaterHeaterDevice, +) from . import DATA_HIVE, DOMAIN -SUPPORT_FLAGS_HEATER = (SUPPORT_OPERATION_MODE) +SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE -HIVE_TO_HASS_STATE = { - 'SCHEDULE': STATE_ECO, - 'ON': STATE_ON, - 'OFF': STATE_OFF, -} +HIVE_TO_HASS_STATE = {"SCHEDULE": STATE_ECO, "ON": STATE_ON, "OFF": STATE_OFF} -HASS_TO_HIVE_STATE = { - STATE_ECO: 'SCHEDULE', - STATE_ON: 'ON', - STATE_OFF: 'OFF', -} +HASS_TO_HIVE_STATE = {STATE_ECO: "SCHEDULE", STATE_ON: "ON", STATE_OFF: "OFF"} SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF] @@ -45,9 +42,8 @@ class HiveWaterHeater(WaterHeaterDevice): self.node_name = hivedevice["Hive_NodeName"] self.device_type = hivedevice["HA_DeviceType"] self.session = hivesession - self.data_updatesource = '{}.{}'.format( - self.device_type, self.node_id) - self._unique_id = '{}-{}'.format(self.node_id, self.device_type) + self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) + self._unique_id = "{}-{}".format(self.node_id, self.device_type) self._unit_of_measurement = TEMP_CELSIUS @property @@ -58,12 +54,7 @@ class HiveWaterHeater(WaterHeaterDevice): @property def device_info(self): """Return device information.""" - return { - 'identifiers': { - (DOMAIN, self.unique_id) - }, - 'name': self.name - } + return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} @property def supported_features(self): @@ -72,12 +63,12 @@ class HiveWaterHeater(WaterHeaterDevice): def handle_update(self, updatesource): """Handle the new update request.""" - if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: + if "{}.{}".format(self.device_type, self.node_id) not in updatesource: self.schedule_update_ha_state() @property def name(self): - """Return the name of the water heater """ + """Return the name of the water heater.""" if self.node_name is None: self.node_name = "Hot Water" return self.node_name @@ -89,7 +80,7 @@ class HiveWaterHeater(WaterHeaterDevice): @property def current_operation(self): - """ Return current operation. """ + """Return current operation.""" return HIVE_TO_HASS_STATE[self.session.hotwater.get_mode(self.node_id)] @property diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index 79de0bd18be..f174b00613b 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -4,49 +4,63 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_HOST, CONF_PORT, - EVENT_HOMEASSISTANT_STOP, CONF_SWITCHES, CONF_NAME) + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, + CONF_SWITCHES, + CONF_NAME, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect) + async_dispatcher_send, + async_dispatcher_connect, +) _LOGGER = logging.getLogger(__name__) -DATA_DEVICE_REGISTER = 'hlk_sw16_device_register' +DATA_DEVICE_REGISTER = "hlk_sw16_device_register" DEFAULT_RECONNECT_INTERVAL = 10 CONNECTION_TIMEOUT = 10 DEFAULT_PORT = 8080 -DOMAIN = 'hlk_sw16' +DOMAIN = "hlk_sw16" -SIGNAL_AVAILABILITY = 'hlk_sw16_device_available_{}' +SIGNAL_AVAILABILITY = "hlk_sw16_device_available_{}" -SWITCH_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, -}) +SWITCH_SCHEMA = vol.Schema({vol.Optional(CONF_NAME): cv.string}) RELAY_ID = vol.All( - vol.Any(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f'), - vol.Coerce(str)) + vol.Any(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "a", "b", "c", "d", "e", "f"), vol.Coerce(str) +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.string: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Required(CONF_SWITCHES): vol.Schema({RELAY_ID: SWITCH_SCHEMA}), - }), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + cv.string: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_SWITCHES): vol.Schema( + {RELAY_ID: SWITCH_SCHEMA} + ), + } + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): """Set up the HLK-SW16 switch.""" # Allow platform to specify function to register new unknown devices from hlk_sw16 import create_hlk_sw16_connection + hass.data[DATA_DEVICE_REGISTER] = {} def add_device(device): @@ -58,20 +72,18 @@ async def async_setup(hass, config): @callback def disconnected(): """Schedule reconnect after connection has been lost.""" - _LOGGER.warning('HLK-SW16 %s disconnected', device) - async_dispatcher_send(hass, SIGNAL_AVAILABILITY.format(device), - False) + _LOGGER.warning("HLK-SW16 %s disconnected", device) + async_dispatcher_send(hass, SIGNAL_AVAILABILITY.format(device), False) @callback def reconnected(): """Schedule reconnect after connection has been lost.""" - _LOGGER.warning('HLK-SW16 %s connected', device) - async_dispatcher_send(hass, SIGNAL_AVAILABILITY.format(device), - True) + _LOGGER.warning("HLK-SW16 %s connected", device) + async_dispatcher_send(hass, SIGNAL_AVAILABILITY.format(device), True) async def connect(): """Set up connection and hook it into HA for reconnect/shutdown.""" - _LOGGER.info('Initiating HLK-SW16 connection to %s', device) + _LOGGER.info("Initiating HLK-SW16 connection to %s", device) client = await create_hlk_sw16_connection( host=host, @@ -80,21 +92,22 @@ async def async_setup(hass, config): reconnect_callback=reconnected, loop=hass.loop, timeout=CONNECTION_TIMEOUT, - reconnect_interval=DEFAULT_RECONNECT_INTERVAL) + reconnect_interval=DEFAULT_RECONNECT_INTERVAL, + ) hass.data[DATA_DEVICE_REGISTER][device] = client # Load platforms hass.async_create_task( - async_load_platform(hass, 'switch', DOMAIN, - (switches, device), - config)) + async_load_platform(hass, "switch", DOMAIN, (switches, device), config) + ) # handle shutdown of HLK-SW16 asyncio transport - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, - lambda x: client.stop()) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, lambda x: client.stop() + ) - _LOGGER.info('Connected to HLK-SW16 device: %s', device) + _LOGGER.info("Connected to HLK-SW16 device: %s", device) hass.loop.create_task(connect()) @@ -121,8 +134,7 @@ class SW16Device(Entity): @callback def handle_event_callback(self, event): """Propagate changes through ha.""" - _LOGGER.debug("Relay %s new state callback: %r", - self._device_port, event) + _LOGGER.debug("Relay %s new state callback: %r", self._device_port, event) self._is_on = event self.async_schedule_update_ha_state() @@ -148,9 +160,12 @@ class SW16Device(Entity): async def async_added_to_hass(self): """Register update callback.""" - self._client.register_status_callback(self.handle_event_callback, - self._device_port) + self._client.register_status_callback( + self.handle_event_callback, self._device_port + ) self._is_on = await self._client.status(self._device_port) - async_dispatcher_connect(self.hass, - SIGNAL_AVAILABILITY.format(self._device_id), - self._availability_callback) + async_dispatcher_connect( + self.hass, + SIGNAL_AVAILABILITY.format(self._device_id), + self._availability_callback, + ) diff --git a/homeassistant/components/hlk_sw16/switch.py b/homeassistant/components/hlk_sw16/switch.py index b7353f037c1..e9c190678a6 100644 --- a/homeassistant/components/hlk_sw16/switch.py +++ b/homeassistant/components/hlk_sw16/switch.py @@ -22,8 +22,7 @@ def devices_from_config(hass, domain_config): return devices -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HLK-SW16 platform.""" async_add_entities(devices_from_config(hass, discovery_info)) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 2bcacb48bd1..2bd0a62cebb 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -12,24 +12,30 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.helpers import intent from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, - SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, - RESTART_EXIT_CODE, ATTR_LATITUDE, ATTR_LONGITUDE) + ATTR_ENTITY_ID, + SERVICE_TURN_ON, + SERVICE_TURN_OFF, + SERVICE_TOGGLE, + SERVICE_HOMEASSISTANT_STOP, + SERVICE_HOMEASSISTANT_RESTART, + RESTART_EXIT_CODE, + ATTR_LATITUDE, + ATTR_LONGITUDE, +) from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) DOMAIN = ha.DOMAIN -SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config' -SERVICE_CHECK_CONFIG = 'check_config' -SERVICE_UPDATE_ENTITY = 'update_entity' -SERVICE_SET_LOCATION = 'set_location' -SCHEMA_UPDATE_ENTITY = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids -}) +SERVICE_RELOAD_CORE_CONFIG = "reload_core_config" +SERVICE_CHECK_CONFIG = "check_config" +SERVICE_UPDATE_ENTITY = "update_entity" +SERVICE_SET_LOCATION = "set_location" +SCHEMA_UPDATE_ENTITY = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: """Set up general services related to Home Assistant.""" + async def async_handle_turn_service(service): """Handle calls to homeassistant.turn_on/off.""" entity_ids = await async_extract_entity_ids(hass, service) @@ -37,13 +43,14 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: # Generic turn on/off method requires entity id if not entity_ids: _LOGGER.error( - "homeassistant/%s cannot be called without entity_id", - service.service) + "homeassistant/%s cannot be called without entity_id", service.service + ) return # Group entity_ids by domain. groupby requires sorted data. - by_domain = it.groupby(sorted(entity_ids), - lambda item: ha.split_entity_id(item)[0]) + by_domain = it.groupby( + sorted(entity_ids), lambda item: ha.split_entity_id(item)[0] + ) tasks = [] @@ -62,24 +69,30 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: # ent_ids is a generator, convert it to a list. data[ATTR_ENTITY_ID] = list(ent_ids) - tasks.append(hass.services.async_call( - domain, service.service, data, blocking)) + tasks.append( + hass.services.async_call(domain, service.service, data, blocking) + ) await asyncio.wait(tasks) - hass.services.async_register( - ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service) - hass.services.async_register( - ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service) - hass.services.async_register( - ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service) - hass.helpers.intent.async_register(intent.ServiceIntentHandler( - intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on")) - hass.helpers.intent.async_register(intent.ServiceIntentHandler( - intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, - "Turned {} off")) - hass.helpers.intent.async_register(intent.ServiceIntentHandler( - intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}")) + hass.services.async_register(ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service) + hass.services.async_register(ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service) + hass.services.async_register(ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on" + ) + ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, "Turned {} off" + ) + ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}" + ) + ) async def async_handle_core_service(call): """Service handler for handling core services.""" @@ -96,7 +109,9 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: _LOGGER.error(errors) hass.components.persistent_notification.async_create( "Config error. See dev-info panel for details.", - "Config validating", "{0}.check_config".format(ha.DOMAIN)) + "Config validating", + "{0}.check_config".format(ha.DOMAIN), + ) return if call.service == SERVICE_HOMEASSISTANT_RESTART: @@ -104,21 +119,29 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: async def async_handle_update_service(call): """Service handler for updating an entity.""" - tasks = [hass.helpers.entity_component.async_update_entity(entity) - for entity in call.data[ATTR_ENTITY_ID]] + tasks = [ + hass.helpers.entity_component.async_update_entity(entity) + for entity in call.data[ATTR_ENTITY_ID] + ] if tasks: await asyncio.wait(tasks) hass.services.async_register( - ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service) + ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service + ) hass.services.async_register( - ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service) + ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service + ) hass.services.async_register( - ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service) + ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service + ) hass.services.async_register( - ha.DOMAIN, SERVICE_UPDATE_ENTITY, async_handle_update_service, - schema=SCHEMA_UPDATE_ENTITY) + ha.DOMAIN, + SERVICE_UPDATE_ENTITY, + async_handle_update_service, + schema=SCHEMA_UPDATE_ENTITY, + ) async def async_handle_reload_config(call): """Service handler for reloading core config.""" @@ -129,8 +152,7 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: return # auth only processed during startup - await conf_util.async_process_ha_core_config( - hass, conf.get(ha.DOMAIN) or {}) + await conf_util.async_process_ha_core_config(hass, conf.get(ha.DOMAIN) or {}) hass.helpers.service.async_register_admin_service( ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config @@ -139,15 +161,14 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: async def async_set_location(call): """Service handler to set location.""" await hass.config.async_update( - latitude=call.data[ATTR_LATITUDE], - longitude=call.data[ATTR_LONGITUDE], + latitude=call.data[ATTR_LATITUDE], longitude=call.data[ATTR_LONGITUDE] ) hass.helpers.service.async_register_admin_service( - ha.DOMAIN, SERVICE_SET_LOCATION, async_set_location, vol.Schema({ - ATTR_LATITUDE: cv.latitude, - ATTR_LONGITUDE: cv.longitude, - }) + ha.DOMAIN, + SERVICE_SET_LOCATION, + async_set_location, + vol.Schema({ATTR_LATITUDE: cv.latitude, ATTR_LONGITUDE: cv.longitude}), ) return True diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 617b5624110..de8a4dc88e7 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -4,39 +4,48 @@ from collections import namedtuple import voluptuous as vol from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_STATE, CONF_ENTITIES, CONF_NAME, CONF_PLATFORM, - STATE_OFF, STATE_ON) + ATTR_ENTITY_ID, + ATTR_STATE, + CONF_ENTITIES, + CONF_NAME, + CONF_PLATFORM, + STATE_OFF, + STATE_ON, +) from homeassistant.core import State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.state import HASS_DOMAIN, async_reproduce_state from homeassistant.components.scene import STATES, Scene -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): HASS_DOMAIN, - vol.Required(STATES): vol.All( - cv.ensure_list, - [ - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ENTITIES): { - cv.entity_id: vol.Any(str, bool, dict) - }, - } - ] - ), -}, extra=vol.ALLOW_EXTRA) +PLATFORM_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): HASS_DOMAIN, + vol.Required(STATES): vol.All( + cv.ensure_list, + [ + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ENTITIES): { + cv.entity_id: vol.Any(str, bool, dict) + }, + } + ], + ), + }, + extra=vol.ALLOW_EXTRA, +) -SCENECONFIG = namedtuple('SceneConfig', [CONF_NAME, STATES]) +SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up home assistant scene entries.""" scene_config = config.get(STATES) - async_add_entities(HomeAssistantScene( - hass, _process_config(scene)) for scene in scene_config) + async_add_entities( + HomeAssistantScene(hass, _process_config(scene)) for scene in scene_config + ) return True @@ -87,11 +96,8 @@ class HomeAssistantScene(Scene): @property def device_state_attributes(self): """Return the scene state attributes.""" - return { - ATTR_ENTITY_ID: list(self.scene_config.states.keys()), - } + return {ATTR_ENTITY_ID: list(self.scene_config.states.keys())} async def async_activate(self): """Activate scene. Try to get entities into requested state.""" - await async_reproduce_state( - self.hass, self.scene_config.states.values(), True) + await async_reproduce_state(self.hass, self.scene_config.states.values(), True) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 90fdde84f3b..d8aafb8e238 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -8,26 +8,57 @@ import voluptuous as vol from homeassistant.components import cover from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.const import ( - ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, - CONF_TYPE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, + CONF_IP_ADDRESS, + CONF_NAME, + CONF_PORT, + CONF_TYPE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( - BRIDGE_NAME, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, - CONF_FILTER, CONF_SAFE_MODE, DEFAULT_AUTO_START, DEFAULT_PORT, - DEFAULT_SAFE_MODE, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, - DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, - SERVICE_HOMEKIT_RESET_ACCESSORY, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, - TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) + BRIDGE_NAME, + CONF_AUTO_START, + CONF_ENTITY_CONFIG, + CONF_FEATURE_LIST, + CONF_FILTER, + CONF_SAFE_MODE, + DEFAULT_AUTO_START, + DEFAULT_PORT, + DEFAULT_SAFE_MODE, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, + DEVICE_CLASS_PM25, + DOMAIN, + HOMEKIT_FILE, + SERVICE_HOMEKIT_START, + SERVICE_HOMEKIT_RESET_ACCESSORY, + TYPE_FAUCET, + TYPE_OUTLET, + TYPE_SHOWER, + TYPE_SPRINKLER, + TYPE_SWITCH, + TYPE_VALVE, +) from .util import ( - show_setup_message, validate_entity_config, validate_media_player_features) + show_setup_message, + validate_entity_config, + validate_media_player_features, +) _LOGGER = logging.getLogger(__name__) @@ -41,35 +72,41 @@ STATUS_STOPPED = 2 STATUS_WAIT = 3 SWITCH_TYPES = { - TYPE_FAUCET: 'Valve', - TYPE_OUTLET: 'Outlet', - TYPE_SHOWER: 'Valve', - TYPE_SPRINKLER: 'Valve', - TYPE_SWITCH: 'Switch', - TYPE_VALVE: 'Valve'} + TYPE_FAUCET: "Valve", + TYPE_OUTLET: "Outlet", + TYPE_SHOWER: "Valve", + TYPE_SPRINKLER: "Valve", + TYPE_SWITCH: "Switch", + TYPE_VALVE: "Valve", +} -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All({ - vol.Optional(CONF_NAME, default=BRIDGE_NAME): - vol.All(cv.string, vol.Length(min=3, max=25)), - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_IP_ADDRESS): - vol.All(ipaddress.ip_address, cv.string), - vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, - vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, - vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, - vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, - }) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + { + vol.Optional(CONF_NAME, default=BRIDGE_NAME): vol.All( + cv.string, vol.Length(min=3, max=25) + ), + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, + vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, + vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids -}) +RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema( + {vol.Required(ATTR_ENTITY_ID): cv.entity_ids} +) async def async_setup(hass, config): """Set up the HomeKit component.""" - _LOGGER.debug('Begin setup HomeKit') + _LOGGER.debug("Begin setup HomeKit") conf = config[DOMAIN] name = conf[CONF_NAME] @@ -80,24 +117,29 @@ async def async_setup(hass, config): entity_filter = conf[CONF_FILTER] entity_config = conf[CONF_ENTITY_CONFIG] - homekit = HomeKit(hass, name, port, ip_address, entity_filter, - entity_config, safe_mode) + homekit = HomeKit( + hass, name, port, ip_address, entity_filter, entity_config, safe_mode + ) await hass.async_add_executor_job(homekit.setup) def handle_homekit_reset_accessory(service): """Handle start HomeKit service call.""" if homekit.status != STATUS_RUNNING: _LOGGER.warning( - 'HomeKit is not running. Either it is waiting to be ' - 'started or has been stopped.') + "HomeKit is not running. Either it is waiting to be " + "started or has been stopped." + ) return - entity_ids = service.data.get('entity_id') + entity_ids = service.data.get("entity_id") homekit.reset_accessories(entity_ids) - hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_RESET_ACCESSORY, - handle_homekit_reset_accessory, - schema=RESET_ACCESSORY_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, + SERVICE_HOMEKIT_RESET_ACCESSORY, + handle_homekit_reset_accessory, + schema=RESET_ACCESSORY_SERVICE_SCHEMA, + ) if auto_start: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start) @@ -107,13 +149,15 @@ async def async_setup(hass, config): """Handle start HomeKit service call.""" if homekit.status != STATUS_READY: _LOGGER.warning( - 'HomeKit is not ready. Either it is already running or has ' - 'been stopped.') + "HomeKit is not ready. Either it is already running or has " + "been stopped." + ) return homekit.start() - hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_START, - handle_homekit_service_start) + hass.services.async_register( + DOMAIN, SERVICE_HOMEKIT_START, handle_homekit_service_start + ) return True @@ -121,85 +165,86 @@ async def async_setup(hass, config): def get_accessory(hass, driver, state, aid, config): """Take state and return an accessory object if supported.""" if not aid: - _LOGGER.warning('The entity "%s" is not supported, since it ' - 'generates an invalid aid, please change it.', - state.entity_id) + _LOGGER.warning( + 'The entity "%s" is not supported, since it ' + "generates an invalid aid, please change it.", + state.entity_id, + ) return None a_type = None name = config.get(CONF_NAME, state.name) - if state.domain == 'alarm_control_panel': - a_type = 'SecuritySystem' + if state.domain == "alarm_control_panel": + a_type = "SecuritySystem" - elif state.domain in ('binary_sensor', 'device_tracker', 'person'): - a_type = 'BinarySensor' + elif state.domain in ("binary_sensor", "device_tracker", "person"): + a_type = "BinarySensor" - elif state.domain == 'climate': - a_type = 'Thermostat' + elif state.domain == "climate": + a_type = "Thermostat" - elif state.domain == 'cover': + elif state.domain == "cover": device_class = state.attributes.get(ATTR_DEVICE_CLASS) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if device_class == 'garage' and \ - features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): - a_type = 'GarageDoorOpener' + if device_class == "garage" and features & ( + cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE + ): + a_type = "GarageDoorOpener" elif features & cover.SUPPORT_SET_POSITION: - a_type = 'WindowCovering' + a_type = "WindowCovering" elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): - a_type = 'WindowCoveringBasic' + a_type = "WindowCoveringBasic" - elif state.domain == 'fan': - a_type = 'Fan' + elif state.domain == "fan": + a_type = "Fan" - elif state.domain == 'light': - a_type = 'Light' + elif state.domain == "light": + a_type = "Light" - elif state.domain == 'lock': - a_type = 'Lock' + elif state.domain == "lock": + a_type = "Lock" - elif state.domain == 'media_player': + elif state.domain == "media_player": device_class = state.attributes.get(ATTR_DEVICE_CLASS) feature_list = config.get(CONF_FEATURE_LIST) if device_class == DEVICE_CLASS_TV: - a_type = 'TelevisionMediaPlayer' + a_type = "TelevisionMediaPlayer" else: - if feature_list and \ - validate_media_player_features(state, feature_list): - a_type = 'MediaPlayer' + if feature_list and validate_media_player_features(state, feature_list): + a_type = "MediaPlayer" - elif state.domain == 'sensor': + elif state.domain == "sensor": device_class = state.attributes.get(ATTR_DEVICE_CLASS) unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if device_class == DEVICE_CLASS_TEMPERATURE or \ - unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT): - a_type = 'TemperatureSensor' - elif device_class == DEVICE_CLASS_HUMIDITY and unit == '%': - a_type = 'HumiditySensor' - elif device_class == DEVICE_CLASS_PM25 \ - or DEVICE_CLASS_PM25 in state.entity_id: - a_type = 'AirQualitySensor' + if device_class == DEVICE_CLASS_TEMPERATURE or unit in ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + ): + a_type = "TemperatureSensor" + elif device_class == DEVICE_CLASS_HUMIDITY and unit == "%": + a_type = "HumiditySensor" + elif device_class == DEVICE_CLASS_PM25 or DEVICE_CLASS_PM25 in state.entity_id: + a_type = "AirQualitySensor" elif device_class == DEVICE_CLASS_CO: - a_type = 'CarbonMonoxideSensor' - elif device_class == DEVICE_CLASS_CO2 \ - or DEVICE_CLASS_CO2 in state.entity_id: - a_type = 'CarbonDioxideSensor' - elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'): - a_type = 'LightSensor' + a_type = "CarbonMonoxideSensor" + elif device_class == DEVICE_CLASS_CO2 or DEVICE_CLASS_CO2 in state.entity_id: + a_type = "CarbonDioxideSensor" + elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", "lx"): + a_type = "LightSensor" - elif state.domain == 'switch': + elif state.domain == "switch": switch_type = config.get(CONF_TYPE, TYPE_SWITCH) a_type = SWITCH_TYPES[switch_type] - elif state.domain in ('automation', 'input_boolean', 'remote', 'scene', - 'script'): - a_type = 'Switch' + elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"): + a_type = "Switch" - elif state.domain == 'water_heater': - a_type = 'WaterHeater' + elif state.domain == "water_heater": + a_type = "WaterHeater" if a_type is None: return None @@ -210,17 +255,18 @@ def get_accessory(hass, driver, state, aid, config): def generate_aid(entity_id): """Generate accessory aid with zlib adler32.""" - aid = adler32(entity_id.encode('utf-8')) + aid = adler32(entity_id.encode("utf-8")) if aid in (0, 1): return None return aid -class HomeKit(): +class HomeKit: """Class to handle all actions between HomeKit and Home Assistant.""" - def __init__(self, hass, name, port, ip_address, entity_filter, - entity_config, safe_mode): + def __init__( + self, hass, name, port, ip_address, entity_filter, entity_config, safe_mode + ): """Initialize a HomeKit object.""" self.hass = hass self._name = name @@ -238,16 +284,16 @@ class HomeKit(): """Set up bridge and accessory driver.""" from .accessories import HomeBridge, HomeDriver - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self.stop) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) - self.driver = HomeDriver(self.hass, address=ip_addr, - port=self._port, persist_file=path) + self.driver = HomeDriver( + self.hass, address=ip_addr, port=self._port, persist_file=path + ) self.bridge = HomeBridge(self.hass, self.driver, self._name) if self._safe_mode: - _LOGGER.debug('Safe_mode selected') + _LOGGER.debug("Safe_mode selected") self.driver.safe_mode = True def reset_accessories(self, entity_ids): @@ -256,8 +302,9 @@ class HomeKit(): for entity_id in entity_ids: aid = generate_aid(entity_id) if aid not in self.bridge.accessories: - _LOGGER.warning('Could not reset accessory. entity_id ' - 'not found %s', entity_id) + _LOGGER.warning( + "Could not reset accessory. entity_id " "not found %s", entity_id + ) continue acc = self.remove_bridge_accessory(aid) removed.append(acc) @@ -292,9 +339,16 @@ class HomeKit(): # pylint: disable=unused-import from . import ( # noqa F401 - type_covers, type_fans, type_lights, type_locks, - type_media_players, type_security_systems, type_sensors, - type_switches, type_thermostats) + type_covers, + type_fans, + type_lights, + type_locks, + type_media_players, + type_security_systems, + type_sensors, + type_switches, + type_thermostats, + ) for state in self.hass.states.all(): self.add_bridge_accessory(state) @@ -304,10 +358,12 @@ class HomeKit(): show_setup_message(self.hass, self.driver.state.pincode) if len(self.bridge.accessories) > MAX_DEVICES: - _LOGGER.warning('You have exceeded the device limit, which might ' - 'cause issues. Consider using the filter option.') + _LOGGER.warning( + "You have exceeded the device limit, which might " + "cause issues. Consider using the filter option." + ) - _LOGGER.debug('Driver start') + _LOGGER.debug("Driver start") self.hass.add_job(self.driver.start) self.status = STATUS_RUNNING @@ -317,5 +373,5 @@ class HomeKit(): return self.status = STATUS_STOPPED - _LOGGER.debug('Driver stop') + _LOGGER.debug("Driver stop") self.hass.add_job(self.driver.stop) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 13dfc90841f..84f0b7894c4 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -9,19 +9,35 @@ from pyhap.accessory_driver import AccessoryDriver from pyhap.const import CATEGORY_OTHER from homeassistant.const import ( - ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_SERVICE, - __version__) + ATTR_BATTERY_CHARGING, + ATTR_BATTERY_LEVEL, + ATTR_ENTITY_ID, + ATTR_SERVICE, + __version__, +) from homeassistant.core import callback as ha_callback, split_entity_id from homeassistant.helpers.event import ( - async_track_state_change, track_point_in_utc_time) + async_track_state_change, + track_point_in_utc_time, +) from homeassistant.util import dt as dt_util from .const import ( - ATTR_DISPLAY_NAME, ATTR_VALUE, BRIDGE_MODEL, BRIDGE_SERIAL_NUMBER, - CHAR_BATTERY_LEVEL, CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, - CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, DEBOUNCE_TIMEOUT, - DEFAULT_LOW_BATTERY_THRESHOLD, EVENT_HOMEKIT_CHANGED, MANUFACTURER, - SERV_BATTERY_SERVICE) + ATTR_DISPLAY_NAME, + ATTR_VALUE, + BRIDGE_MODEL, + BRIDGE_SERIAL_NUMBER, + CHAR_BATTERY_LEVEL, + CHAR_CHARGING_STATE, + CHAR_STATUS_LOW_BATTERY, + CONF_LINKED_BATTERY_SENSOR, + CONF_LOW_BATTERY_THRESHOLD, + DEBOUNCE_TIMEOUT, + DEFAULT_LOW_BATTERY_THRESHOLD, + EVENT_HOMEKIT_CHANGED, + MANUFACTURER, + SERV_BATTERY_SERVICE, +) from .util import convert_to_float, dismiss_setup_message, show_setup_message _LOGGER = logging.getLogger(__name__) @@ -29,6 +45,7 @@ _LOGGER = logging.getLogger(__name__) def debounce(func): """Decorate function to debounce callbacks from HomeKit.""" + @ha_callback def call_later_listener(self, *args): """Handle call_later callback.""" @@ -43,11 +60,14 @@ def debounce(func): if debounce_params: debounce_params[0]() # remove listener remove_listener = track_point_in_utc_time( - self.hass, partial(call_later_listener, self), - dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT)) + self.hass, + partial(call_later_listener, self), + dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT), + ) self.debounce[func.__name__] = (remove_listener, *args) - logger.debug('%s: Start %s timeout', self.entity_id, - func.__name__.replace('set_', '')) + logger.debug( + "%s: Start %s timeout", self.entity_id, func.__name__.replace("set_", "") + ) name = getmodule(func).__name__ logger = logging.getLogger(name) @@ -57,14 +77,18 @@ def debounce(func): class HomeAccessory(Accessory): """Adapter class for Accessory.""" - def __init__(self, hass, driver, name, entity_id, aid, config, - category=CATEGORY_OTHER): + def __init__( + self, hass, driver, name, entity_id, aid, config, category=CATEGORY_OTHER + ): """Initialize a Accessory object.""" super().__init__(driver, name, aid=aid) model = split_entity_id(entity_id)[0].replace("_", " ").title() self.set_info_service( - firmware_revision=__version__, manufacturer=MANUFACTURER, - model=model, serial_number=entity_id) + firmware_revision=__version__, + manufacturer=MANUFACTURER, + model=model, + serial_number=entity_id, + ) self.category = category self.config = config or {} self.entity_id = entity_id @@ -72,30 +96,28 @@ class HomeAccessory(Accessory): self.debounce = {} self._support_battery_level = False self._support_battery_charging = True - self.linked_battery_sensor = \ - self.config.get(CONF_LINKED_BATTERY_SENSOR) - self.low_battery_threshold = \ - self.config.get(CONF_LOW_BATTERY_THRESHOLD, - DEFAULT_LOW_BATTERY_THRESHOLD) + self.linked_battery_sensor = self.config.get(CONF_LINKED_BATTERY_SENSOR) + self.low_battery_threshold = self.config.get( + CONF_LOW_BATTERY_THRESHOLD, DEFAULT_LOW_BATTERY_THRESHOLD + ) """Add battery service if available""" - battery_found = self.hass.states.get(self.entity_id).attributes \ - .get(ATTR_BATTERY_LEVEL) + battery_found = self.hass.states.get(self.entity_id).attributes.get( + ATTR_BATTERY_LEVEL + ) if self.linked_battery_sensor: - battery_found = self.hass.states.get( - self.linked_battery_sensor).state + battery_found = self.hass.states.get(self.linked_battery_sensor).state if battery_found is None: return - _LOGGER.debug('%s: Found battery level', self.entity_id) + _LOGGER.debug("%s: Found battery level", self.entity_id) self._support_battery_level = True serv_battery = self.add_preload_service(SERV_BATTERY_SERVICE) - self._char_battery = serv_battery.configure_char( - CHAR_BATTERY_LEVEL, value=0) - self._char_charging = serv_battery.configure_char( - CHAR_CHARGING_STATE, value=2) + self._char_battery = serv_battery.configure_char(CHAR_BATTERY_LEVEL, value=0) + self._char_charging = serv_battery.configure_char(CHAR_CHARGING_STATE, value=2) self._char_low_battery = serv_battery.configure_char( - CHAR_STATUS_LOW_BATTERY, value=0) + CHAR_STATUS_LOW_BATTERY, value=0 + ) async def run(self): """Handle accessory driver started event. @@ -111,22 +133,21 @@ class HomeAccessory(Accessory): """ state = self.hass.states.get(self.entity_id) self.hass.async_add_job(self.update_state_callback, None, None, state) - async_track_state_change( - self.hass, self.entity_id, self.update_state_callback) + async_track_state_change(self.hass, self.entity_id, self.update_state_callback) if self.linked_battery_sensor: battery_state = self.hass.states.get(self.linked_battery_sensor) - self.hass.async_add_job(self.update_linked_battery, None, None, - battery_state) + self.hass.async_add_job( + self.update_linked_battery, None, None, battery_state + ) async_track_state_change( - self.hass, self.linked_battery_sensor, - self.update_linked_battery) + self.hass, self.linked_battery_sensor, self.update_linked_battery + ) @ha_callback - def update_state_callback(self, entity_id=None, old_state=None, - new_state=None): + def update_state_callback(self, entity_id=None, old_state=None, new_state=None): """Handle state change listener callback.""" - _LOGGER.debug('New_state: %s', new_state) + _LOGGER.debug("New_state: %s", new_state) if new_state is None: return if self._support_battery_level and not self.linked_battery_sensor: @@ -134,8 +155,7 @@ class HomeAccessory(Accessory): self.hass.async_add_executor_job(self.update_state, new_state) @ha_callback - def update_linked_battery(self, entity_id=None, old_state=None, - new_state=None): + def update_linked_battery(self, entity_id=None, old_state=None, new_state=None): """Handle linked battery sensor state change listener callback.""" self.hass.async_add_executor_job(self.update_battery, new_state) @@ -144,17 +164,14 @@ class HomeAccessory(Accessory): Only call this function if self._support_battery_level is True. """ - battery_level = convert_to_float( - new_state.attributes.get(ATTR_BATTERY_LEVEL)) + battery_level = convert_to_float(new_state.attributes.get(ATTR_BATTERY_LEVEL)) if self.linked_battery_sensor: battery_level = convert_to_float(new_state.state) if battery_level is None: return self._char_battery.set_value(battery_level) - self._char_low_battery.set_value( - battery_level < self.low_battery_threshold) - _LOGGER.debug('%s: Updated battery level to %d', self.entity_id, - battery_level) + self._char_low_battery.set_value(battery_level < self.low_battery_threshold) + _LOGGER.debug("%s: Updated battery level to %d", self.entity_id, battery_level) if not self._support_battery_charging: return charging = new_state.attributes.get(ATTR_BATTERY_CHARGING) @@ -163,8 +180,7 @@ class HomeAccessory(Accessory): return hk_charging = 1 if charging is True else 0 self._char_charging.set_value(hk_charging) - _LOGGER.debug('%s: Updated battery charging to %d', self.entity_id, - hk_charging) + _LOGGER.debug("%s: Updated battery charging to %d", self.entity_id, hk_charging) def update_state(self, new_state): """Handle state change to update HomeKit value. @@ -175,11 +191,9 @@ class HomeAccessory(Accessory): def call_service(self, domain, service, service_data, value=None): """Fire event and call service for changes from HomeKit.""" - self.hass.add_job( - self.async_call_service, domain, service, service_data, value) + self.hass.add_job(self.async_call_service, domain, service, service_data, value) - async def async_call_service(self, domain, service, service_data, - value=None): + async def async_call_service(self, domain, service, service_data, value=None): """Fire event and call service for changes from HomeKit. This method must be run in the event loop. @@ -188,7 +202,7 @@ class HomeAccessory(Accessory): ATTR_ENTITY_ID: self.entity_id, ATTR_DISPLAY_NAME: self.display_name, ATTR_SERVICE: service, - ATTR_VALUE: value + ATTR_VALUE: value, } self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data) @@ -202,8 +216,11 @@ class HomeBridge(Bridge): """Initialize a Bridge object.""" super().__init__(driver, name) self.set_info_service( - firmware_revision=__version__, manufacturer=MANUFACTURER, - model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER) + firmware_revision=__version__, + manufacturer=MANUFACTURER, + model=BRIDGE_MODEL, + serial_number=BRIDGE_SERIAL_NUMBER, + ) self.hass = hass def setup_message(self): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index ce0659ddc73..d225225237f 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,23 +1,23 @@ """Constants used be the HomeKit component.""" # #### Misc #### DEBOUNCE_TIMEOUT = 0.5 -DOMAIN = 'homekit' -HOMEKIT_FILE = '.homekit.state' +DOMAIN = "homekit" +HOMEKIT_FILE = ".homekit.state" HOMEKIT_NOTIFY_ID = 4663548 # #### Attributes #### -ATTR_DISPLAY_NAME = 'display_name' -ATTR_VALUE = 'value' +ATTR_DISPLAY_NAME = "display_name" +ATTR_VALUE = "value" # #### Config #### -CONF_AUTO_START = 'auto_start' -CONF_ENTITY_CONFIG = 'entity_config' -CONF_FEATURE = 'feature' -CONF_FEATURE_LIST = 'feature_list' -CONF_FILTER = 'filter' -CONF_LINKED_BATTERY_SENSOR = 'linked_battery_sensor' -CONF_LOW_BATTERY_THRESHOLD = 'low_battery_threshold' -CONF_SAFE_MODE = 'safe_mode' +CONF_AUTO_START = "auto_start" +CONF_ENTITY_CONFIG = "entity_config" +CONF_FEATURE = "feature" +CONF_FEATURE_LIST = "feature_list" +CONF_FILTER = "filter" +CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor" +CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold" +CONF_SAFE_MODE = "safe_mode" # #### Config Defaults #### DEFAULT_AUTO_START = True @@ -26,146 +26,146 @@ DEFAULT_PORT = 51827 DEFAULT_SAFE_MODE = False # #### Features #### -FEATURE_ON_OFF = 'on_off' -FEATURE_PLAY_PAUSE = 'play_pause' -FEATURE_PLAY_STOP = 'play_stop' -FEATURE_TOGGLE_MUTE = 'toggle_mute' +FEATURE_ON_OFF = "on_off" +FEATURE_PLAY_PAUSE = "play_pause" +FEATURE_PLAY_STOP = "play_stop" +FEATURE_TOGGLE_MUTE = "toggle_mute" # #### HomeKit Component Event #### -EVENT_HOMEKIT_CHANGED = 'homekit_state_change' +EVENT_HOMEKIT_CHANGED = "homekit_state_change" # #### HomeKit Component Services #### -SERVICE_HOMEKIT_START = 'start' -SERVICE_HOMEKIT_RESET_ACCESSORY = 'reset_accessory' +SERVICE_HOMEKIT_START = "start" +SERVICE_HOMEKIT_RESET_ACCESSORY = "reset_accessory" # #### String Constants #### -BRIDGE_MODEL = 'Bridge' -BRIDGE_NAME = 'Home Assistant Bridge' -BRIDGE_SERIAL_NUMBER = 'homekit.bridge' -MANUFACTURER = 'Home Assistant' +BRIDGE_MODEL = "Bridge" +BRIDGE_NAME = "Home Assistant Bridge" +BRIDGE_SERIAL_NUMBER = "homekit.bridge" +MANUFACTURER = "Home Assistant" # #### Switch Types #### -TYPE_FAUCET = 'faucet' -TYPE_OUTLET = 'outlet' -TYPE_SHOWER = 'shower' -TYPE_SPRINKLER = 'sprinkler' -TYPE_SWITCH = 'switch' -TYPE_VALVE = 'valve' +TYPE_FAUCET = "faucet" +TYPE_OUTLET = "outlet" +TYPE_SHOWER = "shower" +TYPE_SPRINKLER = "sprinkler" +TYPE_SWITCH = "switch" +TYPE_VALVE = "valve" # #### Services #### -SERV_ACCESSORY_INFO = 'AccessoryInformation' -SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' -SERV_BATTERY_SERVICE = 'BatteryService' -SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' -SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' -SERV_CONTACT_SENSOR = 'ContactSensor' -SERV_FANV2 = 'Fanv2' -SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener' -SERV_HUMIDITY_SENSOR = 'HumiditySensor' -SERV_INPUT_SOURCE = 'InputSource' -SERV_LEAK_SENSOR = 'LeakSensor' -SERV_LIGHT_SENSOR = 'LightSensor' -SERV_LIGHTBULB = 'Lightbulb' -SERV_LOCK = 'LockMechanism' -SERV_MOTION_SENSOR = 'MotionSensor' -SERV_OCCUPANCY_SENSOR = 'OccupancySensor' -SERV_OUTLET = 'Outlet' -SERV_SECURITY_SYSTEM = 'SecuritySystem' -SERV_SMOKE_SENSOR = 'SmokeSensor' -SERV_SWITCH = 'Switch' -SERV_TELEVISION = 'Television' -SERV_TELEVISION_SPEAKER = 'TelevisionSpeaker' -SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' -SERV_THERMOSTAT = 'Thermostat' -SERV_VALVE = 'Valve' -SERV_WINDOW_COVERING = 'WindowCovering' +SERV_ACCESSORY_INFO = "AccessoryInformation" +SERV_AIR_QUALITY_SENSOR = "AirQualitySensor" +SERV_BATTERY_SERVICE = "BatteryService" +SERV_CARBON_DIOXIDE_SENSOR = "CarbonDioxideSensor" +SERV_CARBON_MONOXIDE_SENSOR = "CarbonMonoxideSensor" +SERV_CONTACT_SENSOR = "ContactSensor" +SERV_FANV2 = "Fanv2" +SERV_GARAGE_DOOR_OPENER = "GarageDoorOpener" +SERV_HUMIDITY_SENSOR = "HumiditySensor" +SERV_INPUT_SOURCE = "InputSource" +SERV_LEAK_SENSOR = "LeakSensor" +SERV_LIGHT_SENSOR = "LightSensor" +SERV_LIGHTBULB = "Lightbulb" +SERV_LOCK = "LockMechanism" +SERV_MOTION_SENSOR = "MotionSensor" +SERV_OCCUPANCY_SENSOR = "OccupancySensor" +SERV_OUTLET = "Outlet" +SERV_SECURITY_SYSTEM = "SecuritySystem" +SERV_SMOKE_SENSOR = "SmokeSensor" +SERV_SWITCH = "Switch" +SERV_TELEVISION = "Television" +SERV_TELEVISION_SPEAKER = "TelevisionSpeaker" +SERV_TEMPERATURE_SENSOR = "TemperatureSensor" +SERV_THERMOSTAT = "Thermostat" +SERV_VALVE = "Valve" +SERV_WINDOW_COVERING = "WindowCovering" # #### Characteristics #### -CHAR_ACTIVE = 'Active' -CHAR_ACTIVE_IDENTIFIER = 'ActiveIdentifier' -CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' -CHAR_AIR_QUALITY = 'AirQuality' -CHAR_BATTERY_LEVEL = 'BatteryLevel' -CHAR_BRIGHTNESS = 'Brightness' -CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' -CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' -CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' -CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected' -CHAR_CARBON_MONOXIDE_LEVEL = 'CarbonMonoxideLevel' -CHAR_CARBON_MONOXIDE_PEAK_LEVEL = 'CarbonMonoxidePeakLevel' -CHAR_CHARGING_STATE = 'ChargingState' -CHAR_COLOR_TEMPERATURE = 'ColorTemperature' -CHAR_CONFIGURED_NAME = 'ConfiguredName' -CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' -CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' -CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel' -CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState' -CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' -CHAR_CURRENT_POSITION = 'CurrentPosition' -CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' -CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' -CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' -CHAR_CURRENT_VISIBILITY_STATE = 'CurrentVisibilityState' -CHAR_FIRMWARE_REVISION = 'FirmwareRevision' -CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' -CHAR_HUE = 'Hue' -CHAR_IDENTIFIER = 'Identifier' -CHAR_IN_USE = 'InUse' -CHAR_INPUT_SOURCE_TYPE = 'InputSourceType' -CHAR_IS_CONFIGURED = 'IsConfigured' -CHAR_LEAK_DETECTED = 'LeakDetected' -CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' -CHAR_LOCK_TARGET_STATE = 'LockTargetState' -CHAR_LINK_QUALITY = 'LinkQuality' -CHAR_MANUFACTURER = 'Manufacturer' -CHAR_MODEL = 'Model' -CHAR_MOTION_DETECTED = 'MotionDetected' -CHAR_MUTE = 'Mute' -CHAR_NAME = 'Name' -CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' -CHAR_ON = 'On' -CHAR_OUTLET_IN_USE = 'OutletInUse' -CHAR_POSITION_STATE = 'PositionState' -CHAR_REMOTE_KEY = 'RemoteKey' -CHAR_ROTATION_DIRECTION = 'RotationDirection' -CHAR_ROTATION_SPEED = 'RotationSpeed' -CHAR_SATURATION = 'Saturation' -CHAR_SERIAL_NUMBER = 'SerialNumber' -CHAR_SLEEP_DISCOVER_MODE = 'SleepDiscoveryMode' -CHAR_SMOKE_DETECTED = 'SmokeDetected' -CHAR_STATUS_LOW_BATTERY = 'StatusLowBattery' -CHAR_SWING_MODE = 'SwingMode' -CHAR_TARGET_DOOR_STATE = 'TargetDoorState' -CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' -CHAR_TARGET_POSITION = 'TargetPosition' -CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' -CHAR_TARGET_TEMPERATURE = 'TargetTemperature' -CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' -CHAR_VALVE_TYPE = 'ValveType' -CHAR_VOLUME = 'Volume' -CHAR_VOLUME_SELECTOR = 'VolumeSelector' -CHAR_VOLUME_CONTROL_TYPE = 'VolumeControlType' +CHAR_ACTIVE = "Active" +CHAR_ACTIVE_IDENTIFIER = "ActiveIdentifier" +CHAR_AIR_PARTICULATE_DENSITY = "AirParticulateDensity" +CHAR_AIR_QUALITY = "AirQuality" +CHAR_BATTERY_LEVEL = "BatteryLevel" +CHAR_BRIGHTNESS = "Brightness" +CHAR_CARBON_DIOXIDE_DETECTED = "CarbonDioxideDetected" +CHAR_CARBON_DIOXIDE_LEVEL = "CarbonDioxideLevel" +CHAR_CARBON_DIOXIDE_PEAK_LEVEL = "CarbonDioxidePeakLevel" +CHAR_CARBON_MONOXIDE_DETECTED = "CarbonMonoxideDetected" +CHAR_CARBON_MONOXIDE_LEVEL = "CarbonMonoxideLevel" +CHAR_CARBON_MONOXIDE_PEAK_LEVEL = "CarbonMonoxidePeakLevel" +CHAR_CHARGING_STATE = "ChargingState" +CHAR_COLOR_TEMPERATURE = "ColorTemperature" +CHAR_CONFIGURED_NAME = "ConfiguredName" +CHAR_CONTACT_SENSOR_STATE = "ContactSensorState" +CHAR_COOLING_THRESHOLD_TEMPERATURE = "CoolingThresholdTemperature" +CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = "CurrentAmbientLightLevel" +CHAR_CURRENT_DOOR_STATE = "CurrentDoorState" +CHAR_CURRENT_HEATING_COOLING = "CurrentHeatingCoolingState" +CHAR_CURRENT_POSITION = "CurrentPosition" +CHAR_CURRENT_HUMIDITY = "CurrentRelativeHumidity" +CHAR_CURRENT_SECURITY_STATE = "SecuritySystemCurrentState" +CHAR_CURRENT_TEMPERATURE = "CurrentTemperature" +CHAR_CURRENT_VISIBILITY_STATE = "CurrentVisibilityState" +CHAR_FIRMWARE_REVISION = "FirmwareRevision" +CHAR_HEATING_THRESHOLD_TEMPERATURE = "HeatingThresholdTemperature" +CHAR_HUE = "Hue" +CHAR_IDENTIFIER = "Identifier" +CHAR_IN_USE = "InUse" +CHAR_INPUT_SOURCE_TYPE = "InputSourceType" +CHAR_IS_CONFIGURED = "IsConfigured" +CHAR_LEAK_DETECTED = "LeakDetected" +CHAR_LOCK_CURRENT_STATE = "LockCurrentState" +CHAR_LOCK_TARGET_STATE = "LockTargetState" +CHAR_LINK_QUALITY = "LinkQuality" +CHAR_MANUFACTURER = "Manufacturer" +CHAR_MODEL = "Model" +CHAR_MOTION_DETECTED = "MotionDetected" +CHAR_MUTE = "Mute" +CHAR_NAME = "Name" +CHAR_OCCUPANCY_DETECTED = "OccupancyDetected" +CHAR_ON = "On" +CHAR_OUTLET_IN_USE = "OutletInUse" +CHAR_POSITION_STATE = "PositionState" +CHAR_REMOTE_KEY = "RemoteKey" +CHAR_ROTATION_DIRECTION = "RotationDirection" +CHAR_ROTATION_SPEED = "RotationSpeed" +CHAR_SATURATION = "Saturation" +CHAR_SERIAL_NUMBER = "SerialNumber" +CHAR_SLEEP_DISCOVER_MODE = "SleepDiscoveryMode" +CHAR_SMOKE_DETECTED = "SmokeDetected" +CHAR_STATUS_LOW_BATTERY = "StatusLowBattery" +CHAR_SWING_MODE = "SwingMode" +CHAR_TARGET_DOOR_STATE = "TargetDoorState" +CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState" +CHAR_TARGET_POSITION = "TargetPosition" +CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState" +CHAR_TARGET_TEMPERATURE = "TargetTemperature" +CHAR_TEMP_DISPLAY_UNITS = "TemperatureDisplayUnits" +CHAR_VALVE_TYPE = "ValveType" +CHAR_VOLUME = "Volume" +CHAR_VOLUME_SELECTOR = "VolumeSelector" +CHAR_VOLUME_CONTROL_TYPE = "VolumeControlType" # #### Properties #### -PROP_MAX_VALUE = 'maxValue' -PROP_MIN_VALUE = 'minValue' -PROP_MIN_STEP = 'minStep' -PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} +PROP_MAX_VALUE = "maxValue" +PROP_MIN_VALUE = "minValue" +PROP_MIN_STEP = "minStep" +PROP_CELSIUS = {"minValue": -273, "maxValue": 999} # #### Device Classes #### -DEVICE_CLASS_CO = 'co' -DEVICE_CLASS_CO2 = 'co2' -DEVICE_CLASS_DOOR = 'door' -DEVICE_CLASS_GARAGE_DOOR = 'garage_door' -DEVICE_CLASS_GAS = 'gas' -DEVICE_CLASS_MOISTURE = 'moisture' -DEVICE_CLASS_MOTION = 'motion' -DEVICE_CLASS_OCCUPANCY = 'occupancy' -DEVICE_CLASS_OPENING = 'opening' -DEVICE_CLASS_PM25 = 'pm25' -DEVICE_CLASS_SMOKE = 'smoke' -DEVICE_CLASS_WINDOW = 'window' +DEVICE_CLASS_CO = "co" +DEVICE_CLASS_CO2 = "co2" +DEVICE_CLASS_DOOR = "door" +DEVICE_CLASS_GARAGE_DOOR = "garage_door" +DEVICE_CLASS_GAS = "gas" +DEVICE_CLASS_MOISTURE = "moisture" +DEVICE_CLASS_MOTION = "motion" +DEVICE_CLASS_OCCUPANCY = "occupancy" +DEVICE_CLASS_OPENING = "opening" +DEVICE_CLASS_PM25 = "pm25" +DEVICE_CLASS_SMOKE = "smoke" +DEVICE_CLASS_WINDOW = "window" # #### Thresholds #### THRESHOLD_CO = 25 diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 5273480b6ce..3a33207e70e 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -4,23 +4,38 @@ import logging from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING from homeassistant.components.cover import ( - ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN, + SUPPORT_STOP, +) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_CLOSE_COVER, - SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, - STATE_CLOSED, STATE_OPEN) + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + STATE_CLOSED, + STATE_OPEN, +) from . import TYPES from .accessories import HomeAccessory, debounce from .const import ( - CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_POSITION_STATE, - CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, SERV_GARAGE_DOOR_OPENER, - SERV_WINDOW_COVERING) + CHAR_CURRENT_DOOR_STATE, + CHAR_CURRENT_POSITION, + CHAR_POSITION_STATE, + CHAR_TARGET_DOOR_STATE, + CHAR_TARGET_POSITION, + SERV_GARAGE_DOOR_OPENER, + SERV_WINDOW_COVERING, +) _LOGGER = logging.getLogger(__name__) -@TYPES.register('GarageDoorOpener') +@TYPES.register("GarageDoorOpener") class GarageDoorOpener(HomeAccessory): """Generate a Garage Door Opener accessory for a cover entity. @@ -35,13 +50,15 @@ class GarageDoorOpener(HomeAccessory): serv_garage_door = self.add_preload_service(SERV_GARAGE_DOOR_OPENER) self.char_current_state = serv_garage_door.configure_char( - CHAR_CURRENT_DOOR_STATE, value=0) + CHAR_CURRENT_DOOR_STATE, value=0 + ) self.char_target_state = serv_garage_door.configure_char( - CHAR_TARGET_DOOR_STATE, value=0, setter_callback=self.set_state) + CHAR_TARGET_DOOR_STATE, value=0, setter_callback=self.set_state + ) def set_state(self, value): """Change garage state if call came from HomeKit.""" - _LOGGER.debug('%s: Set state to %d', self.entity_id, value) + _LOGGER.debug("%s: Set state to %d", self.entity_id, value) self._flag_state = True params = {ATTR_ENTITY_ID: self.entity_id} @@ -65,7 +82,7 @@ class GarageDoorOpener(HomeAccessory): self._flag_state = False -@TYPES.register('WindowCovering') +@TYPES.register("WindowCovering") class WindowCovering(HomeAccessory): """Generate a Window accessory for a cover entity. @@ -79,14 +96,16 @@ class WindowCovering(HomeAccessory): serv_cover = self.add_preload_service(SERV_WINDOW_COVERING) self.char_current_position = serv_cover.configure_char( - CHAR_CURRENT_POSITION, value=0) + CHAR_CURRENT_POSITION, value=0 + ) self.char_target_position = serv_cover.configure_char( - CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) + CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover + ) @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set position to %d', self.entity_id, value) + _LOGGER.debug("%s: Set position to %d", self.entity_id, value) self._homekit_target = value params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value} @@ -97,13 +116,15 @@ class WindowCovering(HomeAccessory): current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if isinstance(current_position, int): self.char_current_position.set_value(current_position) - if self._homekit_target is None or \ - abs(current_position - self._homekit_target) < 6: + if ( + self._homekit_target is None + or abs(current_position - self._homekit_target) < 6 + ): self.char_target_position.set_value(current_position) self._homekit_target = None -@TYPES.register('WindowCoveringBasic') +@TYPES.register("WindowCoveringBasic") class WindowCoveringBasic(HomeAccessory): """Generate a Window accessory for a cover entity. @@ -114,22 +135,26 @@ class WindowCoveringBasic(HomeAccessory): def __init__(self, *args): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=CATEGORY_WINDOW_COVERING) - features = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_SUPPORTED_FEATURES) + features = self.hass.states.get(self.entity_id).attributes.get( + ATTR_SUPPORTED_FEATURES + ) self._supports_stop = features & SUPPORT_STOP serv_cover = self.add_preload_service(SERV_WINDOW_COVERING) self.char_current_position = serv_cover.configure_char( - CHAR_CURRENT_POSITION, value=0) + CHAR_CURRENT_POSITION, value=0 + ) self.char_target_position = serv_cover.configure_char( - CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) + CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover + ) self.char_position_state = serv_cover.configure_char( - CHAR_POSITION_STATE, value=2) + CHAR_POSITION_STATE, value=2 + ) @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set position to %d', self.entity_id, value) + _LOGGER.debug("%s: Set position to %d", self.entity_id, value) if self._supports_stop: if value > 70: diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index d2777a296dc..e3fa6c42c58 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -4,25 +4,44 @@ import logging from pyhap.const import CATEGORY_FAN from homeassistant.components.fan import ( - ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_SPEED, ATTR_SPEED_LIST, - DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, - SERVICE_SET_DIRECTION, SERVICE_SET_SPEED, SUPPORT_DIRECTION, - SUPPORT_OSCILLATE, SUPPORT_SET_SPEED) + ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_SPEED, + ATTR_SPEED_LIST, + DIRECTION_FORWARD, + DIRECTION_REVERSE, + DOMAIN, + SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, + SERVICE_SET_SPEED, + SUPPORT_DIRECTION, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, +) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_OFF, STATE_ON) + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) from . import TYPES from .accessories import HomeAccessory, debounce from .const import ( - CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, - SERV_FANV2) + CHAR_ACTIVE, + CHAR_ROTATION_DIRECTION, + CHAR_ROTATION_SPEED, + CHAR_SWING_MODE, + SERV_FANV2, +) from .util import HomeKitSpeedMapping _LOGGER = logging.getLogger(__name__) -@TYPES.register('Fan') +@TYPES.register("Fan") class Fan(HomeAccessory): """Generate a Fan accessory for a fan entity. @@ -32,27 +51,32 @@ class Fan(HomeAccessory): def __init__(self, *args): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_FAN) - self._flag = {CHAR_ACTIVE: False, - CHAR_ROTATION_DIRECTION: False, - CHAR_SWING_MODE: False} + self._flag = { + CHAR_ACTIVE: False, + CHAR_ROTATION_DIRECTION: False, + CHAR_SWING_MODE: False, + } self._state = 0 chars = [] - features = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_SUPPORTED_FEATURES) + features = self.hass.states.get(self.entity_id).attributes.get( + ATTR_SUPPORTED_FEATURES + ) if features & SUPPORT_DIRECTION: chars.append(CHAR_ROTATION_DIRECTION) if features & SUPPORT_OSCILLATE: chars.append(CHAR_SWING_MODE) if features & SUPPORT_SET_SPEED: - speed_list = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_SPEED_LIST) + speed_list = self.hass.states.get(self.entity_id).attributes.get( + ATTR_SPEED_LIST + ) self.speed_mapping = HomeKitSpeedMapping(speed_list) chars.append(CHAR_ROTATION_SPEED) serv_fan = self.add_preload_service(SERV_FANV2, chars) self.char_active = serv_fan.configure_char( - CHAR_ACTIVE, value=0, setter_callback=self.set_state) + CHAR_ACTIVE, value=0, setter_callback=self.set_state + ) self.char_direction = None self.char_speed = None @@ -60,20 +84,22 @@ class Fan(HomeAccessory): if CHAR_ROTATION_DIRECTION in chars: self.char_direction = serv_fan.configure_char( - CHAR_ROTATION_DIRECTION, value=0, - setter_callback=self.set_direction) + CHAR_ROTATION_DIRECTION, value=0, setter_callback=self.set_direction + ) if CHAR_ROTATION_SPEED in chars: self.char_speed = serv_fan.configure_char( - CHAR_ROTATION_SPEED, value=0, setter_callback=self.set_speed) + CHAR_ROTATION_SPEED, value=0, setter_callback=self.set_speed + ) if CHAR_SWING_MODE in chars: self.char_swing = serv_fan.configure_char( - CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating) + CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating + ) def set_state(self, value): """Set state if call came from HomeKit.""" - _LOGGER.debug('%s: Set state to %d', self.entity_id, value) + _LOGGER.debug("%s: Set state to %d", self.entity_id, value) self._flag[CHAR_ACTIVE] = True service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF params = {ATTR_ENTITY_ID: self.entity_id} @@ -81,7 +107,7 @@ class Fan(HomeAccessory): def set_direction(self, value): """Set state if call came from HomeKit.""" - _LOGGER.debug('%s: Set direction to %d', self.entity_id, value) + _LOGGER.debug("%s: Set direction to %d", self.entity_id, value) self._flag[CHAR_ROTATION_DIRECTION] = True direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction} @@ -89,20 +115,18 @@ class Fan(HomeAccessory): def set_oscillating(self, value): """Set state if call came from HomeKit.""" - _LOGGER.debug('%s: Set oscillating to %d', self.entity_id, value) + _LOGGER.debug("%s: Set oscillating to %d", self.entity_id, value) self._flag[CHAR_SWING_MODE] = True oscillating = value == 1 - params = {ATTR_ENTITY_ID: self.entity_id, - ATTR_OSCILLATING: oscillating} + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_OSCILLATING: oscillating} self.call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating) @debounce def set_speed(self, value): """Set state if call came from HomeKit.""" - _LOGGER.debug('%s: Set speed to %d', self.entity_id, value) + _LOGGER.debug("%s: Set speed to %d", self.entity_id, value) speed = self.speed_mapping.speed_to_states(value) - params = {ATTR_ENTITY_ID: self.entity_id, - ATTR_SPEED: speed} + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_SPEED: speed} self.call_service(DOMAIN, SERVICE_SET_SPEED, params, speed) def update_state(self, new_state): @@ -111,16 +135,17 @@ class Fan(HomeAccessory): state = new_state.state if state in (STATE_ON, STATE_OFF): self._state = 1 if state == STATE_ON else 0 - if not self._flag[CHAR_ACTIVE] and \ - self.char_active.value != self._state: + if not self._flag[CHAR_ACTIVE] and self.char_active.value != self._state: self.char_active.set_value(self._state) self._flag[CHAR_ACTIVE] = False # Handle Direction if self.char_direction is not None: direction = new_state.attributes.get(ATTR_DIRECTION) - if not self._flag[CHAR_ROTATION_DIRECTION] and \ - direction in (DIRECTION_FORWARD, DIRECTION_REVERSE): + if not self._flag[CHAR_ROTATION_DIRECTION] and direction in ( + DIRECTION_FORWARD, + DIRECTION_REVERSE, + ): hk_direction = 1 if direction == DIRECTION_REVERSE else 0 if self.char_direction.value != hk_direction: self.char_direction.set_value(hk_direction) @@ -130,15 +155,13 @@ class Fan(HomeAccessory): if self.char_speed is not None: speed = new_state.attributes.get(ATTR_SPEED) hk_speed_value = self.speed_mapping.speed_to_homekit(speed) - if hk_speed_value is not None and \ - self.char_speed.value != hk_speed_value: + if hk_speed_value is not None and self.char_speed.value != hk_speed_value: self.char_speed.set_value(hk_speed_value) # Handle Oscillating if self.char_swing is not None: oscillating = new_state.attributes.get(ATTR_OSCILLATING) - if not self._flag[CHAR_SWING_MODE] and \ - oscillating in (True, False): + if not self._flag[CHAR_SWING_MODE] and oscillating in (True, False): hk_oscillating = 1 if oscillating else 0 if self.char_swing.value != hk_oscillating: self.char_swing.set_value(hk_oscillating) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index f549958f755..fce81d0adf7 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -4,25 +4,45 @@ import logging from pyhap.const import CATEGORY_LIGHTBULB from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, - ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, SUPPORT_COLOR_TEMP) + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, + DOMAIN, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, +) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_OFF, STATE_ON) + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) from . import TYPES from .accessories import HomeAccessory, debounce from .const import ( - CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, CHAR_ON, - CHAR_SATURATION, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_LIGHTBULB) + CHAR_BRIGHTNESS, + CHAR_COLOR_TEMPERATURE, + CHAR_HUE, + CHAR_ON, + CHAR_SATURATION, + PROP_MAX_VALUE, + PROP_MIN_VALUE, + SERV_LIGHTBULB, +) _LOGGER = logging.getLogger(__name__) -RGB_COLOR = 'rgb_color' +RGB_COLOR = "rgb_color" -@TYPES.register('Light') +@TYPES.register("Light") class Light(HomeAccessory): """Generate a Light accessory for a light entity. @@ -32,14 +52,20 @@ class Light(HomeAccessory): def __init__(self, *args): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) - self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, - CHAR_HUE: False, CHAR_SATURATION: False, - CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False} + self._flag = { + CHAR_ON: False, + CHAR_BRIGHTNESS: False, + CHAR_HUE: False, + CHAR_SATURATION: False, + CHAR_COLOR_TEMPERATURE: False, + RGB_COLOR: False, + } self._state = 0 self.chars = [] - self._features = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_SUPPORTED_FEATURES) + self._features = self.hass.states.get(self.entity_id).attributes.get( + ATTR_SUPPORTED_FEATURES + ) if self._features & SUPPORT_BRIGHTNESS: self.chars.append(CHAR_BRIGHTNESS) if self._features & SUPPORT_COLOR_TEMP: @@ -52,34 +78,41 @@ class Light(HomeAccessory): serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) self.char_on = serv_light.configure_char( - CHAR_ON, value=self._state, setter_callback=self.set_state) + CHAR_ON, value=self._state, setter_callback=self.set_state + ) if CHAR_BRIGHTNESS in self.chars: self.char_brightness = serv_light.configure_char( - CHAR_BRIGHTNESS, value=0, setter_callback=self.set_brightness) + CHAR_BRIGHTNESS, value=0, setter_callback=self.set_brightness + ) if CHAR_COLOR_TEMPERATURE in self.chars: - min_mireds = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_MIN_MIREDS, 153) - max_mireds = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_MAX_MIREDS, 500) + min_mireds = self.hass.states.get(self.entity_id).attributes.get( + ATTR_MIN_MIREDS, 153 + ) + max_mireds = self.hass.states.get(self.entity_id).attributes.get( + ATTR_MAX_MIREDS, 500 + ) self.char_color_temperature = serv_light.configure_char( - CHAR_COLOR_TEMPERATURE, value=min_mireds, - properties={PROP_MIN_VALUE: min_mireds, - PROP_MAX_VALUE: max_mireds}, - setter_callback=self.set_color_temperature) + CHAR_COLOR_TEMPERATURE, + value=min_mireds, + properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, + setter_callback=self.set_color_temperature, + ) if CHAR_HUE in self.chars: self.char_hue = serv_light.configure_char( - CHAR_HUE, value=0, setter_callback=self.set_hue) + CHAR_HUE, value=0, setter_callback=self.set_hue + ) if CHAR_SATURATION in self.chars: self.char_saturation = serv_light.configure_char( - CHAR_SATURATION, value=75, setter_callback=self.set_saturation) + CHAR_SATURATION, value=75, setter_callback=self.set_saturation + ) def set_state(self, value): """Set state if call came from HomeKit.""" if self._state == value: return - _LOGGER.debug('%s: Set state to %d', self.entity_id, value) + _LOGGER.debug("%s: Set state to %d", self.entity_id, value) self._flag[CHAR_ON] = True params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF @@ -88,48 +121,55 @@ class Light(HomeAccessory): @debounce def set_brightness(self, value): """Set brightness if call came from HomeKit.""" - _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) + _LOGGER.debug("%s: Set brightness to %d", self.entity_id, value) self._flag[CHAR_BRIGHTNESS] = True if value == 0: self.set_state(0) # Turn off light return params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} - self.call_service(DOMAIN, SERVICE_TURN_ON, params, - 'brightness at {}%'.format(value)) + self.call_service( + DOMAIN, SERVICE_TURN_ON, params, "brightness at {}%".format(value) + ) def set_color_temperature(self, value): """Set color temperature if call came from HomeKit.""" - _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) + _LOGGER.debug("%s: Set color temp to %s", self.entity_id, value) self._flag[CHAR_COLOR_TEMPERATURE] = True params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} - self.call_service(DOMAIN, SERVICE_TURN_ON, params, - 'color temperature at {}'.format(value)) + self.call_service( + DOMAIN, SERVICE_TURN_ON, params, "color temperature at {}".format(value) + ) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" - _LOGGER.debug('%s: Set saturation to %d', self.entity_id, value) + _LOGGER.debug("%s: Set saturation to %d", self.entity_id, value) self._flag[CHAR_SATURATION] = True self._saturation = value self.set_color() def set_hue(self, value): """Set hue if call came from HomeKit.""" - _LOGGER.debug('%s: Set hue to %d', self.entity_id, value) + _LOGGER.debug("%s: Set hue to %d", self.entity_id, value) self._flag[CHAR_HUE] = True self._hue = value self.set_color() def set_color(self): """Set color if call came from HomeKit.""" - if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ - self._flag[CHAR_SATURATION]: + if ( + self._features & SUPPORT_COLOR + and self._flag[CHAR_HUE] + and self._flag[CHAR_SATURATION] + ): color = (self._hue, self._saturation) - _LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color) - self._flag.update({ - CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) + _LOGGER.debug("%s: Set hs_color to %s", self.entity_id, color) + self._flag.update( + {CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True} + ) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} - self.call_service(DOMAIN, SERVICE_TURN_ON, params, - 'set color at {}'.format(color)) + self.call_service( + DOMAIN, SERVICE_TURN_ON, params, "set color at {}".format(color) + ) def update_state(self, new_state): """Update light after state change.""" @@ -153,20 +193,23 @@ class Light(HomeAccessory): # Handle color temperature if CHAR_COLOR_TEMPERATURE in self.chars: color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP) - if not self._flag[CHAR_COLOR_TEMPERATURE] \ - and isinstance(color_temperature, int) and \ - self.char_color_temperature.value != color_temperature: + if ( + not self._flag[CHAR_COLOR_TEMPERATURE] + and isinstance(color_temperature, int) + and self.char_color_temperature.value != color_temperature + ): self.char_color_temperature.set_value(color_temperature) self._flag[CHAR_COLOR_TEMPERATURE] = False # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: - hue, saturation = new_state.attributes.get( - ATTR_HS_COLOR, (None, None)) - if not self._flag[RGB_COLOR] and ( - hue != self._hue or saturation != self._saturation) and \ - isinstance(hue, (int, float)) and \ - isinstance(saturation, (int, float)): + hue, saturation = new_state.attributes.get(ATTR_HS_COLOR, (None, None)) + if ( + not self._flag[RGB_COLOR] + and (hue != self._hue or saturation != self._saturation) + and isinstance(hue, (int, float)) + and isinstance(saturation, (int, float)) + ): self.char_hue.set_value(hue) self.char_saturation.set_value(saturation) self._hue, self._saturation = (hue, saturation) diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 4ed1cebd207..3a7211ab2ad 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -3,9 +3,8 @@ import logging from pyhap.const import CATEGORY_DOOR_LOCK -from homeassistant.components.lock import ( - ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED) -from homeassistant.const import ATTR_CODE, STATE_UNKNOWN +from homeassistant.components.lock import DOMAIN, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN from . import TYPES from .accessories import HomeAccessory @@ -22,13 +21,10 @@ HASS_TO_HOMEKIT = { HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} -STATE_TO_SERVICE = { - STATE_LOCKED: 'lock', - STATE_UNLOCKED: 'unlock', -} +STATE_TO_SERVICE = {STATE_LOCKED: "lock", STATE_UNLOCKED: "unlock"} -@TYPES.register('Lock') +@TYPES.register("Lock") class Lock(HomeAccessory): """Generate a Lock accessory for a lock entity. @@ -43,11 +39,13 @@ class Lock(HomeAccessory): serv_lock_mechanism = self.add_preload_service(SERV_LOCK) self.char_current_state = serv_lock_mechanism.configure_char( - CHAR_LOCK_CURRENT_STATE, - value=HASS_TO_HOMEKIT[STATE_UNKNOWN]) + CHAR_LOCK_CURRENT_STATE, value=HASS_TO_HOMEKIT[STATE_UNKNOWN] + ) self.char_target_state = serv_lock_mechanism.configure_char( - CHAR_LOCK_TARGET_STATE, value=HASS_TO_HOMEKIT[STATE_LOCKED], - setter_callback=self.set_state) + CHAR_LOCK_TARGET_STATE, + value=HASS_TO_HOMEKIT[STATE_LOCKED], + setter_callback=self.set_state, + ) def set_state(self, value): """Set lock state to value if call came from HomeKit.""" @@ -68,8 +66,12 @@ class Lock(HomeAccessory): if hass_state in HASS_TO_HOMEKIT: current_lock_state = HASS_TO_HOMEKIT[hass_state] self.char_current_state.set_value(current_lock_state) - _LOGGER.debug("%s: Updated current state to %s (%d)", - self.entity_id, hass_state, current_lock_state) + _LOGGER.debug( + "%s: Updated current state to %s (%d)", + self.entity_id, + hass_state, + current_lock_state, + ) # LockTargetState only supports locked and unlocked if hass_state in (STATE_LOCKED, STATE_UNLOCKED): diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index b0c4be35e1b..d9b24782610 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -4,27 +4,66 @@ import logging from pyhap.const import CATEGORY_SWITCH, CATEGORY_TELEVISION from homeassistant.components.media_player import ( - ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_VOLUME_MUTED, - ATTR_MEDIA_VOLUME_LEVEL, SERVICE_SELECT_SOURCE, DOMAIN, SUPPORT_PAUSE, - SUPPORT_PLAY, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - SUPPORT_SELECT_SOURCE) + ATTR_INPUT_SOURCE, + ATTR_INPUT_SOURCE_LIST, + ATTR_MEDIA_VOLUME_MUTED, + ATTR_MEDIA_VOLUME_LEVEL, + SERVICE_SELECT_SOURCE, + DOMAIN, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, + SUPPORT_SELECT_SOURCE, +) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_MEDIA_PAUSE, - SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_STOP, - SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_UP, - SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET, STATE_OFF, STATE_PLAYING, - STATE_PAUSED, STATE_UNKNOWN) + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PLAY_PAUSE, + SERVICE_MEDIA_STOP, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_UP, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_SET, + STATE_OFF, + STATE_PLAYING, + STATE_PAUSED, + STATE_UNKNOWN, +) from . import TYPES from .accessories import HomeAccessory from .const import ( - CHAR_ACTIVE, CHAR_ACTIVE_IDENTIFIER, CHAR_CONFIGURED_NAME, - CHAR_CURRENT_VISIBILITY_STATE, CHAR_IDENTIFIER, CHAR_INPUT_SOURCE_TYPE, - CHAR_IS_CONFIGURED, CHAR_NAME, CHAR_SLEEP_DISCOVER_MODE, CHAR_MUTE, - CHAR_ON, CHAR_REMOTE_KEY, CHAR_VOLUME_CONTROL_TYPE, CHAR_VOLUME_SELECTOR, - CHAR_VOLUME, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, - FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, SERV_SWITCH, SERV_TELEVISION, - SERV_TELEVISION_SPEAKER, SERV_INPUT_SOURCE) + CHAR_ACTIVE, + CHAR_ACTIVE_IDENTIFIER, + CHAR_CONFIGURED_NAME, + CHAR_CURRENT_VISIBILITY_STATE, + CHAR_IDENTIFIER, + CHAR_INPUT_SOURCE_TYPE, + CHAR_IS_CONFIGURED, + CHAR_NAME, + CHAR_SLEEP_DISCOVER_MODE, + CHAR_MUTE, + CHAR_ON, + CHAR_REMOTE_KEY, + CHAR_VOLUME_CONTROL_TYPE, + CHAR_VOLUME_SELECTOR, + CHAR_VOLUME, + CONF_FEATURE_LIST, + FEATURE_ON_OFF, + FEATURE_PLAY_PAUSE, + FEATURE_PLAY_STOP, + FEATURE_TOGGLE_MUTE, + SERV_SWITCH, + SERV_TELEVISION, + SERV_TELEVISION_SPEAKER, + SERV_INPUT_SOURCE, +) _LOGGER = logging.getLogger(__name__) @@ -45,24 +84,32 @@ MEDIA_PLAYER_KEYS = { } MODE_FRIENDLY_NAME = { - FEATURE_ON_OFF: 'Power', - FEATURE_PLAY_PAUSE: 'Play/Pause', - FEATURE_PLAY_STOP: 'Play/Stop', - FEATURE_TOGGLE_MUTE: 'Mute', + FEATURE_ON_OFF: "Power", + FEATURE_PLAY_PAUSE: "Play/Pause", + FEATURE_PLAY_STOP: "Play/Stop", + FEATURE_TOGGLE_MUTE: "Mute", } -@TYPES.register('MediaPlayer') +@TYPES.register("MediaPlayer") class MediaPlayer(HomeAccessory): """Generate a Media Player accessory.""" def __init__(self, *args): """Initialize a Switch accessory object.""" super().__init__(*args, category=CATEGORY_SWITCH) - self._flag = {FEATURE_ON_OFF: False, FEATURE_PLAY_PAUSE: False, - FEATURE_PLAY_STOP: False, FEATURE_TOGGLE_MUTE: False} - self.chars = {FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None, - FEATURE_PLAY_STOP: None, FEATURE_TOGGLE_MUTE: None} + self._flag = { + FEATURE_ON_OFF: False, + FEATURE_PLAY_PAUSE: False, + FEATURE_PLAY_STOP: False, + FEATURE_TOGGLE_MUTE: False, + } + self.chars = { + FEATURE_ON_OFF: None, + FEATURE_PLAY_PAUSE: None, + FEATURE_PLAY_STOP: None, + FEATURE_TOGGLE_MUTE: None, + } feature_list = self.config[CONF_FEATURE_LIST] if FEATURE_ON_OFF in feature_list: @@ -70,37 +117,40 @@ class MediaPlayer(HomeAccessory): serv_on_off = self.add_preload_service(SERV_SWITCH, CHAR_NAME) serv_on_off.configure_char(CHAR_NAME, value=name) self.chars[FEATURE_ON_OFF] = serv_on_off.configure_char( - CHAR_ON, value=False, setter_callback=self.set_on_off) + CHAR_ON, value=False, setter_callback=self.set_on_off + ) if FEATURE_PLAY_PAUSE in feature_list: name = self.generate_service_name(FEATURE_PLAY_PAUSE) serv_play_pause = self.add_preload_service(SERV_SWITCH, CHAR_NAME) serv_play_pause.configure_char(CHAR_NAME, value=name) self.chars[FEATURE_PLAY_PAUSE] = serv_play_pause.configure_char( - CHAR_ON, value=False, setter_callback=self.set_play_pause) + CHAR_ON, value=False, setter_callback=self.set_play_pause + ) if FEATURE_PLAY_STOP in feature_list: name = self.generate_service_name(FEATURE_PLAY_STOP) serv_play_stop = self.add_preload_service(SERV_SWITCH, CHAR_NAME) serv_play_stop.configure_char(CHAR_NAME, value=name) self.chars[FEATURE_PLAY_STOP] = serv_play_stop.configure_char( - CHAR_ON, value=False, setter_callback=self.set_play_stop) + CHAR_ON, value=False, setter_callback=self.set_play_stop + ) if FEATURE_TOGGLE_MUTE in feature_list: name = self.generate_service_name(FEATURE_TOGGLE_MUTE) serv_toggle_mute = self.add_preload_service(SERV_SWITCH, CHAR_NAME) serv_toggle_mute.configure_char(CHAR_NAME, value=name) self.chars[FEATURE_TOGGLE_MUTE] = serv_toggle_mute.configure_char( - CHAR_ON, value=False, setter_callback=self.set_toggle_mute) + CHAR_ON, value=False, setter_callback=self.set_toggle_mute + ) def generate_service_name(self, mode): """Generate name for individual service.""" - return '{} {}'.format(self.display_name, MODE_FRIENDLY_NAME[mode]) + return "{} {}".format(self.display_name, MODE_FRIENDLY_NAME[mode]) def set_on_off(self, value): """Move switch state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set switch state for "on_off" to %s', - self.entity_id, value) + _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value) self._flag[FEATURE_ON_OFF] = True service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF params = {ATTR_ENTITY_ID: self.entity_id} @@ -108,8 +158,9 @@ class MediaPlayer(HomeAccessory): def set_play_pause(self, value): """Move switch state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set switch state for "play_pause" to %s', - self.entity_id, value) + _LOGGER.debug( + '%s: Set switch state for "play_pause" to %s', self.entity_id, value + ) self._flag[FEATURE_PLAY_PAUSE] = True service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE params = {ATTR_ENTITY_ID: self.entity_id} @@ -117,8 +168,9 @@ class MediaPlayer(HomeAccessory): def set_play_stop(self, value): """Move switch state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set switch state for "play_stop" to %s', - self.entity_id, value) + _LOGGER.debug( + '%s: Set switch state for "play_stop" to %s', self.entity_id, value + ) self._flag[FEATURE_PLAY_STOP] = True service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP params = {ATTR_ENTITY_ID: self.entity_id} @@ -126,11 +178,11 @@ class MediaPlayer(HomeAccessory): def set_toggle_mute(self, value): """Move switch state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set switch state for "toggle_mute" to %s', - self.entity_id, value) + _LOGGER.debug( + '%s: Set switch state for "toggle_mute" to %s', self.entity_id, value + ) self._flag[FEATURE_TOGGLE_MUTE] = True - params = {ATTR_ENTITY_ID: self.entity_id, - ATTR_MEDIA_VOLUME_MUTED: value} + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_MUTED: value} self.call_service(DOMAIN, SERVICE_VOLUME_MUTE, params) def update_state(self, new_state): @@ -138,39 +190,49 @@ class MediaPlayer(HomeAccessory): current_state = new_state.state if self.chars[FEATURE_ON_OFF]: - hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, 'None') + hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, "None") if not self._flag[FEATURE_ON_OFF]: - _LOGGER.debug('%s: Set current state for "on_off" to %s', - self.entity_id, hk_state) + _LOGGER.debug( + '%s: Set current state for "on_off" to %s', self.entity_id, hk_state + ) self.chars[FEATURE_ON_OFF].set_value(hk_state) self._flag[FEATURE_ON_OFF] = False if self.chars[FEATURE_PLAY_PAUSE]: hk_state = current_state == STATE_PLAYING if not self._flag[FEATURE_PLAY_PAUSE]: - _LOGGER.debug('%s: Set current state for "play_pause" to %s', - self.entity_id, hk_state) + _LOGGER.debug( + '%s: Set current state for "play_pause" to %s', + self.entity_id, + hk_state, + ) self.chars[FEATURE_PLAY_PAUSE].set_value(hk_state) self._flag[FEATURE_PLAY_PAUSE] = False if self.chars[FEATURE_PLAY_STOP]: hk_state = current_state == STATE_PLAYING if not self._flag[FEATURE_PLAY_STOP]: - _LOGGER.debug('%s: Set current state for "play_stop" to %s', - self.entity_id, hk_state) + _LOGGER.debug( + '%s: Set current state for "play_stop" to %s', + self.entity_id, + hk_state, + ) self.chars[FEATURE_PLAY_STOP].set_value(hk_state) self._flag[FEATURE_PLAY_STOP] = False if self.chars[FEATURE_TOGGLE_MUTE]: current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) if not self._flag[FEATURE_TOGGLE_MUTE]: - _LOGGER.debug('%s: Set current state for "toggle_mute" to %s', - self.entity_id, current_state) + _LOGGER.debug( + '%s: Set current state for "toggle_mute" to %s', + self.entity_id, + current_state, + ) self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state) self._flag[FEATURE_TOGGLE_MUTE] = False -@TYPES.register('TelevisionMediaPlayer') +@TYPES.register("TelevisionMediaPlayer") class TelevisionMediaPlayer(HomeAccessory): """Generate a Television Media Player accessory.""" @@ -178,8 +240,11 @@ class TelevisionMediaPlayer(HomeAccessory): """Initialize a Switch accessory object.""" super().__init__(*args, category=CATEGORY_TELEVISION) - self._flag = {CHAR_ACTIVE: False, CHAR_ACTIVE_IDENTIFIER: False, - CHAR_MUTE: False} + self._flag = { + CHAR_ACTIVE: False, + CHAR_ACTIVE_IDENTIFIER: False, + CHAR_MUTE: False, + } self.support_select_source = False self.sources = [] @@ -187,15 +252,16 @@ class TelevisionMediaPlayer(HomeAccessory): # Add additional characteristics if volume or input selection supported self.chars_tv = [] self.chars_speaker = [] - features = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_SUPPORTED_FEATURES, 0) + features = self.hass.states.get(self.entity_id).attributes.get( + ATTR_SUPPORTED_FEATURES, 0 + ) if features & (SUPPORT_PLAY | SUPPORT_PAUSE): self.chars_tv.append(CHAR_REMOTE_KEY) if features & SUPPORT_VOLUME_MUTE or features & SUPPORT_VOLUME_STEP: - self.chars_speaker.extend((CHAR_NAME, CHAR_ACTIVE, - CHAR_VOLUME_CONTROL_TYPE, - CHAR_VOLUME_SELECTOR)) + self.chars_speaker.extend( + (CHAR_NAME, CHAR_ACTIVE, CHAR_VOLUME_CONTROL_TYPE, CHAR_VOLUME_SELECTOR) + ) if features & SUPPORT_VOLUME_SET: self.chars_speaker.append(CHAR_VOLUME) @@ -207,60 +273,66 @@ class TelevisionMediaPlayer(HomeAccessory): serv_tv.configure_char(CHAR_CONFIGURED_NAME, value=self.display_name) serv_tv.configure_char(CHAR_SLEEP_DISCOVER_MODE, value=True) self.char_active = serv_tv.configure_char( - CHAR_ACTIVE, setter_callback=self.set_on_off) + CHAR_ACTIVE, setter_callback=self.set_on_off + ) if CHAR_REMOTE_KEY in self.chars_tv: self.char_remote_key = serv_tv.configure_char( - CHAR_REMOTE_KEY, setter_callback=self.set_remote_key) + CHAR_REMOTE_KEY, setter_callback=self.set_remote_key + ) if CHAR_VOLUME_SELECTOR in self.chars_speaker: serv_speaker = self.add_preload_service( - SERV_TELEVISION_SPEAKER, self.chars_speaker) + SERV_TELEVISION_SPEAKER, self.chars_speaker + ) serv_tv.add_linked_service(serv_speaker) - name = '{} {}'.format(self.display_name, 'Volume') + name = "{} {}".format(self.display_name, "Volume") serv_speaker.configure_char(CHAR_NAME, value=name) serv_speaker.configure_char(CHAR_ACTIVE, value=1) self.char_mute = serv_speaker.configure_char( - CHAR_MUTE, value=False, setter_callback=self.set_mute) + CHAR_MUTE, value=False, setter_callback=self.set_mute + ) volume_control_type = 1 if CHAR_VOLUME in self.chars_speaker else 2 - serv_speaker.configure_char(CHAR_VOLUME_CONTROL_TYPE, - value=volume_control_type) + serv_speaker.configure_char( + CHAR_VOLUME_CONTROL_TYPE, value=volume_control_type + ) self.char_volume_selector = serv_speaker.configure_char( - CHAR_VOLUME_SELECTOR, setter_callback=self.set_volume_step) + CHAR_VOLUME_SELECTOR, setter_callback=self.set_volume_step + ) if CHAR_VOLUME in self.chars_speaker: self.char_volume = serv_speaker.configure_char( - CHAR_VOLUME, setter_callback=self.set_volume) + CHAR_VOLUME, setter_callback=self.set_volume + ) if self.support_select_source: - self.sources = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_INPUT_SOURCE_LIST, []) + self.sources = self.hass.states.get(self.entity_id).attributes.get( + ATTR_INPUT_SOURCE_LIST, [] + ) self.char_input_source = serv_tv.configure_char( - CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source) + CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source + ) for index, source in enumerate(self.sources): serv_input = self.add_preload_service( - SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME]) + SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME] + ) serv_tv.add_linked_service(serv_input) - serv_input.configure_char( - CHAR_CONFIGURED_NAME, value=source) + serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source) serv_input.configure_char(CHAR_NAME, value=source) serv_input.configure_char(CHAR_IDENTIFIER, value=index) serv_input.configure_char(CHAR_IS_CONFIGURED, value=True) input_type = 3 if "hdmi" in source.lower() else 0 - serv_input.configure_char(CHAR_INPUT_SOURCE_TYPE, - value=input_type) - serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, - value=False) - _LOGGER.debug('%s: Added source %s.', self.entity_id, source) + serv_input.configure_char(CHAR_INPUT_SOURCE_TYPE, value=input_type) + serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False) + _LOGGER.debug("%s: Added source %s.", self.entity_id, source) def set_on_off(self, value): """Move switch state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set switch state for "on_off" to %s', - self.entity_id, value) + _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value) self._flag[CHAR_ACTIVE] = True service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF params = {ATTR_ENTITY_ID: self.entity_id} @@ -268,49 +340,48 @@ class TelevisionMediaPlayer(HomeAccessory): def set_mute(self, value): """Move switch state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set switch state for "toggle_mute" to %s', - self.entity_id, value) + _LOGGER.debug( + '%s: Set switch state for "toggle_mute" to %s', self.entity_id, value + ) self._flag[CHAR_MUTE] = True - params = {ATTR_ENTITY_ID: self.entity_id, - ATTR_MEDIA_VOLUME_MUTED: value} + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_MUTED: value} self.call_service(DOMAIN, SERVICE_VOLUME_MUTE, params) def set_volume(self, value): """Send volume step value if call came from HomeKit.""" - _LOGGER.debug('%s: Set volume to %s', self.entity_id, value) - params = {ATTR_ENTITY_ID: self.entity_id, - ATTR_MEDIA_VOLUME_LEVEL: value} + _LOGGER.debug("%s: Set volume to %s", self.entity_id, value) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_LEVEL: value} self.call_service(DOMAIN, SERVICE_VOLUME_SET, params) def set_volume_step(self, value): """Send volume step value if call came from HomeKit.""" - _LOGGER.debug('%s: Step volume by %s', - self.entity_id, value) + _LOGGER.debug("%s: Step volume by %s", self.entity_id, value) service = SERVICE_VOLUME_DOWN if value else SERVICE_VOLUME_UP params = {ATTR_ENTITY_ID: self.entity_id} self.call_service(DOMAIN, service, params) def set_input_source(self, value): """Send input set value if call came from HomeKit.""" - _LOGGER.debug('%s: Set current input to %s', - self.entity_id, value) + _LOGGER.debug("%s: Set current input to %s", self.entity_id, value) source = self.sources[value] self._flag[CHAR_ACTIVE_IDENTIFIER] = True - params = {ATTR_ENTITY_ID: self.entity_id, - ATTR_INPUT_SOURCE: source} + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_INPUT_SOURCE: source} self.call_service(DOMAIN, SERVICE_SELECT_SOURCE, params) def set_remote_key(self, value): """Send remote key value if call came from HomeKit.""" - _LOGGER.debug('%s: Set remote key to %s', self.entity_id, value) + _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value) service = MEDIA_PLAYER_KEYS.get(value) if service: # Handle Play Pause if service == SERVICE_MEDIA_PLAY_PAUSE: state = self.hass.states.get(self.entity_id).state if state in (STATE_PLAYING, STATE_PAUSED): - service = SERVICE_MEDIA_PLAY if state == STATE_PAUSED \ + service = ( + SERVICE_MEDIA_PLAY + if state == STATE_PAUSED else SERVICE_MEDIA_PAUSE + ) params = {ATTR_ENTITY_ID: self.entity_id} self.call_service(DOMAIN, service, params) @@ -321,18 +392,21 @@ class TelevisionMediaPlayer(HomeAccessory): # Power state television hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN) if not self._flag[CHAR_ACTIVE]: - _LOGGER.debug('%s: Set current active state to %s', - self.entity_id, hk_state) + _LOGGER.debug( + "%s: Set current active state to %s", self.entity_id, hk_state + ) self.char_active.set_value(hk_state) self._flag[CHAR_ACTIVE] = False # Set mute state if CHAR_VOLUME_SELECTOR in self.chars_speaker: - current_mute_state = new_state.attributes.get( - ATTR_MEDIA_VOLUME_MUTED) + current_mute_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) if not self._flag[CHAR_MUTE]: - _LOGGER.debug('%s: Set current mute state to %s', - self.entity_id, current_mute_state) + _LOGGER.debug( + "%s: Set current mute state to %s", + self.entity_id, + current_mute_state, + ) self.char_mute.set_value(current_mute_state) self._flag[CHAR_MUTE] = False @@ -340,13 +414,16 @@ class TelevisionMediaPlayer(HomeAccessory): if self.support_select_source: source_name = new_state.attributes.get(ATTR_INPUT_SOURCE) if self.sources and not self._flag[CHAR_ACTIVE_IDENTIFIER]: - _LOGGER.debug('%s: Set current input to %s', self.entity_id, - source_name) + _LOGGER.debug( + "%s: Set current input to %s", self.entity_id, source_name + ) if source_name in self.sources: index = self.sources.index(source_name) self.char_input_source.set_value(index) else: - _LOGGER.warning('%s: Sources out of sync. ' - 'Restart HomeAssistant', self.entity_id) + _LOGGER.warning( + "%s: Sources out of sync. " "Restart HomeAssistant", + self.entity_id, + ) self.char_input_source.set_value(0) self._flag[CHAR_ACTIVE_IDENTIFIER] = False diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 10befb4af61..345709eb7da 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -5,16 +5,26 @@ from pyhap.const import CATEGORY_ALARM_SYSTEM from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, - SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED) + ATTR_CODE, + ATTR_ENTITY_ID, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) from . import TYPES from .accessories import HomeAccessory from .const import ( - CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE, - SERV_SECURITY_SYSTEM) + CHAR_CURRENT_SECURITY_STATE, + CHAR_TARGET_SECURITY_STATE, + SERV_SECURITY_SYSTEM, +) _LOGGER = logging.getLogger(__name__) @@ -36,7 +46,7 @@ STATE_TO_SERVICE = { } -@TYPES.register('SecuritySystem') +@TYPES.register("SecuritySystem") class SecuritySystem(HomeAccessory): """Generate an SecuritySystem accessory for an alarm control panel.""" @@ -48,15 +58,15 @@ class SecuritySystem(HomeAccessory): serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) self.char_current_state = serv_alarm.configure_char( - CHAR_CURRENT_SECURITY_STATE, value=3) + CHAR_CURRENT_SECURITY_STATE, value=3 + ) self.char_target_state = serv_alarm.configure_char( - CHAR_TARGET_SECURITY_STATE, value=3, - setter_callback=self.set_security_state) + CHAR_TARGET_SECURITY_STATE, value=3, setter_callback=self.set_security_state + ) def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set security state to %d', - self.entity_id, value) + _LOGGER.debug("%s: Set security state to %d", self.entity_id, value) self._flag_state = True hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] @@ -72,11 +82,14 @@ class SecuritySystem(HomeAccessory): if hass_state in HASS_TO_HOMEKIT: current_security_state = HASS_TO_HOMEKIT[hass_state] self.char_current_state.set_value(current_security_state) - _LOGGER.debug('%s: Updated current state to %s (%d)', - self.entity_id, hass_state, current_security_state) + _LOGGER.debug( + "%s: Updated current state to %s (%d)", + self.entity_id, + hass_state, + current_security_state, + ) # SecuritySystemTargetState does not support triggered - if not self._flag_state and \ - hass_state != STATE_ALARM_TRIGGERED: + if not self._flag_state and hass_state != STATE_ALARM_TRIGGERED: self.char_target_state.set_value(current_security_state) self._flag_state = False diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 0d7dd94d014..87c8d5247a5 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -4,39 +4,66 @@ import logging from pyhap.const import CATEGORY_SENSOR from homeassistant.const import ( - ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_HOME, STATE_ON, - TEMP_CELSIUS) + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + STATE_HOME, + STATE_ON, + TEMP_CELSIUS, +) from . import TYPES from .accessories import HomeAccessory from .const import ( - CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY, - CHAR_CARBON_DIOXIDE_DETECTED, CHAR_CARBON_DIOXIDE_LEVEL, - CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CARBON_MONOXIDE_DETECTED, - CHAR_CARBON_MONOXIDE_LEVEL, CHAR_CARBON_MONOXIDE_PEAK_LEVEL, - CHAR_CONTACT_SENSOR_STATE, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, - CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED, - CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED, - DEVICE_CLASS_CO2, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, - DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, - DEVICE_CLASS_WINDOW, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, - SERV_CARBON_DIOXIDE_SENSOR, SERV_CARBON_MONOXIDE_SENSOR, - SERV_CONTACT_SENSOR, SERV_HUMIDITY_SENSOR, SERV_LEAK_SENSOR, - SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR, - SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR, THRESHOLD_CO, THRESHOLD_CO2) -from .util import ( - convert_to_float, density_to_air_quality, temperature_to_homekit) + CHAR_AIR_PARTICULATE_DENSITY, + CHAR_AIR_QUALITY, + CHAR_CARBON_DIOXIDE_DETECTED, + CHAR_CARBON_DIOXIDE_LEVEL, + CHAR_CARBON_DIOXIDE_PEAK_LEVEL, + CHAR_CARBON_MONOXIDE_DETECTED, + CHAR_CARBON_MONOXIDE_LEVEL, + CHAR_CARBON_MONOXIDE_PEAK_LEVEL, + CHAR_CONTACT_SENSOR_STATE, + CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, + CHAR_CURRENT_HUMIDITY, + CHAR_CURRENT_TEMPERATURE, + CHAR_LEAK_DETECTED, + CHAR_MOTION_DETECTED, + CHAR_OCCUPANCY_DETECTED, + CHAR_SMOKE_DETECTED, + DEVICE_CLASS_CO2, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_WINDOW, + PROP_CELSIUS, + SERV_AIR_QUALITY_SENSOR, + SERV_CARBON_DIOXIDE_SENSOR, + SERV_CARBON_MONOXIDE_SENSOR, + SERV_CONTACT_SENSOR, + SERV_HUMIDITY_SENSOR, + SERV_LEAK_SENSOR, + SERV_LIGHT_SENSOR, + SERV_MOTION_SENSOR, + SERV_OCCUPANCY_SENSOR, + SERV_SMOKE_SENSOR, + SERV_TEMPERATURE_SENSOR, + THRESHOLD_CO, + THRESHOLD_CO2, +) +from .util import convert_to_float, density_to_air_quality, temperature_to_homekit _LOGGER = logging.getLogger(__name__) BINARY_SENSOR_SERVICE_MAP = { - DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, - CHAR_CARBON_DIOXIDE_DETECTED), + DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED), DEVICE_CLASS_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), DEVICE_CLASS_GARAGE_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), - DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR, - CHAR_CARBON_MONOXIDE_DETECTED), + DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED), DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED), DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED), DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED), @@ -46,7 +73,7 @@ BINARY_SENSOR_SERVICE_MAP = { } -@TYPES.register('TemperatureSensor') +@TYPES.register("TemperatureSensor") class TemperatureSensor(HomeAccessory): """Generate a TemperatureSensor accessory for a temperature sensor. @@ -58,7 +85,8 @@ class TemperatureSensor(HomeAccessory): super().__init__(*args, category=CATEGORY_SENSOR) serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR) self.char_temp = serv_temp.configure_char( - CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS) + CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS + ) def update_state(self, new_state): """Update temperature after state changed.""" @@ -67,11 +95,12 @@ class TemperatureSensor(HomeAccessory): if temperature: temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature) - _LOGGER.debug('%s: Current temperature set to %d°C', - self.entity_id, temperature) + _LOGGER.debug( + "%s: Current temperature set to %d°C", self.entity_id, temperature + ) -@TYPES.register('HumiditySensor') +@TYPES.register("HumiditySensor") class HumiditySensor(HomeAccessory): """Generate a HumiditySensor accessory as humidity sensor.""" @@ -80,18 +109,18 @@ class HumiditySensor(HomeAccessory): super().__init__(*args, category=CATEGORY_SENSOR) serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR) self.char_humidity = serv_humidity.configure_char( - CHAR_CURRENT_HUMIDITY, value=0) + CHAR_CURRENT_HUMIDITY, value=0 + ) def update_state(self, new_state): """Update accessory after state change.""" humidity = convert_to_float(new_state.state) if humidity: self.char_humidity.set_value(humidity) - _LOGGER.debug('%s: Percent set to %d%%', - self.entity_id, humidity) + _LOGGER.debug("%s: Percent set to %d%%", self.entity_id, humidity) -@TYPES.register('AirQualitySensor') +@TYPES.register("AirQualitySensor") class AirQualitySensor(HomeAccessory): """Generate a AirQualitySensor accessory as air quality sensor.""" @@ -100,11 +129,12 @@ class AirQualitySensor(HomeAccessory): super().__init__(*args, category=CATEGORY_SENSOR) serv_air_quality = self.add_preload_service( - SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY]) - self.char_quality = serv_air_quality.configure_char( - CHAR_AIR_QUALITY, value=0) + SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY] + ) + self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0) self.char_density = serv_air_quality.configure_char( - CHAR_AIR_PARTICULATE_DENSITY, value=0) + CHAR_AIR_PARTICULATE_DENSITY, value=0 + ) def update_state(self, new_state): """Update accessory after state change.""" @@ -112,10 +142,10 @@ class AirQualitySensor(HomeAccessory): if density: self.char_density.set_value(density) self.char_quality.set_value(density_to_air_quality(density)) - _LOGGER.debug('%s: Set to %d', self.entity_id, density) + _LOGGER.debug("%s: Set to %d", self.entity_id, density) -@TYPES.register('CarbonMonoxideSensor') +@TYPES.register("CarbonMonoxideSensor") class CarbonMonoxideSensor(HomeAccessory): """Generate a CarbonMonoxidSensor accessory as CO sensor.""" @@ -123,14 +153,17 @@ class CarbonMonoxideSensor(HomeAccessory): """Initialize a CarbonMonoxideSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_co = self.add_preload_service(SERV_CARBON_MONOXIDE_SENSOR, [ - CHAR_CARBON_MONOXIDE_LEVEL, CHAR_CARBON_MONOXIDE_PEAK_LEVEL]) - self.char_level = serv_co.configure_char( - CHAR_CARBON_MONOXIDE_LEVEL, value=0) + serv_co = self.add_preload_service( + SERV_CARBON_MONOXIDE_SENSOR, + [CHAR_CARBON_MONOXIDE_LEVEL, CHAR_CARBON_MONOXIDE_PEAK_LEVEL], + ) + self.char_level = serv_co.configure_char(CHAR_CARBON_MONOXIDE_LEVEL, value=0) self.char_peak = serv_co.configure_char( - CHAR_CARBON_MONOXIDE_PEAK_LEVEL, value=0) + CHAR_CARBON_MONOXIDE_PEAK_LEVEL, value=0 + ) self.char_detected = serv_co.configure_char( - CHAR_CARBON_MONOXIDE_DETECTED, value=0) + CHAR_CARBON_MONOXIDE_DETECTED, value=0 + ) def update_state(self, new_state): """Update accessory after state change.""" @@ -140,10 +173,10 @@ class CarbonMonoxideSensor(HomeAccessory): if value > self.char_peak.value: self.char_peak.set_value(value) self.char_detected.set_value(value > THRESHOLD_CO) - _LOGGER.debug('%s: Set to %d', self.entity_id, value) + _LOGGER.debug("%s: Set to %d", self.entity_id, value) -@TYPES.register('CarbonDioxideSensor') +@TYPES.register("CarbonDioxideSensor") class CarbonDioxideSensor(HomeAccessory): """Generate a CarbonDioxideSensor accessory as CO2 sensor.""" @@ -151,14 +184,17 @@ class CarbonDioxideSensor(HomeAccessory): """Initialize a CarbonDioxideSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_co2 = self.add_preload_service(SERV_CARBON_DIOXIDE_SENSOR, [ - CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL]) - self.char_level = serv_co2.configure_char( - CHAR_CARBON_DIOXIDE_LEVEL, value=0) + serv_co2 = self.add_preload_service( + SERV_CARBON_DIOXIDE_SENSOR, + [CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL], + ) + self.char_level = serv_co2.configure_char(CHAR_CARBON_DIOXIDE_LEVEL, value=0) self.char_peak = serv_co2.configure_char( - CHAR_CARBON_DIOXIDE_PEAK_LEVEL, value=0) + CHAR_CARBON_DIOXIDE_PEAK_LEVEL, value=0 + ) self.char_detected = serv_co2.configure_char( - CHAR_CARBON_DIOXIDE_DETECTED, value=0) + CHAR_CARBON_DIOXIDE_DETECTED, value=0 + ) def update_state(self, new_state): """Update accessory after state change.""" @@ -168,10 +204,10 @@ class CarbonDioxideSensor(HomeAccessory): if value > self.char_peak.value: self.char_peak.set_value(value) self.char_detected.set_value(value > THRESHOLD_CO2) - _LOGGER.debug('%s: Set to %d', self.entity_id, value) + _LOGGER.debug("%s: Set to %d", self.entity_id, value) -@TYPES.register('LightSensor') +@TYPES.register("LightSensor") class LightSensor(HomeAccessory): """Generate a LightSensor accessory as light sensor.""" @@ -181,28 +217,32 @@ class LightSensor(HomeAccessory): serv_light = self.add_preload_service(SERV_LIGHT_SENSOR) self.char_light = serv_light.configure_char( - CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, value=0) + CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, value=0 + ) def update_state(self, new_state): """Update accessory after state change.""" luminance = convert_to_float(new_state.state) if luminance: self.char_light.set_value(luminance) - _LOGGER.debug('%s: Set to %d', self.entity_id, luminance) + _LOGGER.debug("%s: Set to %d", self.entity_id, luminance) -@TYPES.register('BinarySensor') +@TYPES.register("BinarySensor") class BinarySensor(HomeAccessory): """Generate a BinarySensor accessory as binary sensor.""" def __init__(self, *args): """Initialize a BinarySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - device_class = self.hass.states.get(self.entity_id).attributes \ - .get(ATTR_DEVICE_CLASS) - service_char = BINARY_SENSOR_SERVICE_MAP[device_class] \ - if device_class in BINARY_SENSOR_SERVICE_MAP \ + device_class = self.hass.states.get(self.entity_id).attributes.get( + ATTR_DEVICE_CLASS + ) + service_char = ( + BINARY_SENSOR_SERVICE_MAP[device_class] + if device_class in BINARY_SENSOR_SERVICE_MAP else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] + ) service = self.add_preload_service(service_char[0]) self.char_detected = service.configure_char(service_char[1], value=0) @@ -212,4 +252,4 @@ class BinarySensor(HomeAccessory): state = new_state.state detected = state in (STATE_ON, STATE_HOME) self.char_detected.set_value(detected) - _LOGGER.debug('%s: Set to %d', self.entity_id, detected) + _LOGGER.debug("%s: Set to %d", self.entity_id, detected) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 7629e33a4d7..66d3037b894 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -2,22 +2,41 @@ import logging from pyhap.const import ( - CATEGORY_FAUCET, CATEGORY_OUTLET, CATEGORY_SHOWER_HEAD, CATEGORY_SPRINKLER, - CATEGORY_SWITCH) + CATEGORY_FAUCET, + CATEGORY_OUTLET, + CATEGORY_SHOWER_HEAD, + CATEGORY_SPRINKLER, + CATEGORY_SWITCH, +) from homeassistant.components.script import ATTR_CAN_CANCEL from homeassistant.components.switch import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_TYPE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) + ATTR_ENTITY_ID, + CONF_TYPE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) from homeassistant.core import split_entity_id from homeassistant.helpers.event import call_later from . import TYPES from .accessories import HomeAccessory from .const import ( - CHAR_ACTIVE, CHAR_IN_USE, CHAR_ON, CHAR_OUTLET_IN_USE, CHAR_VALVE_TYPE, - SERV_OUTLET, SERV_SWITCH, SERV_VALVE, TYPE_FAUCET, TYPE_SHOWER, - TYPE_SPRINKLER, TYPE_VALVE) + CHAR_ACTIVE, + CHAR_IN_USE, + CHAR_ON, + CHAR_OUTLET_IN_USE, + CHAR_VALVE_TYPE, + SERV_OUTLET, + SERV_SWITCH, + SERV_VALVE, + TYPE_FAUCET, + TYPE_SHOWER, + TYPE_SPRINKLER, + TYPE_VALVE, +) _LOGGER = logging.getLogger(__name__) @@ -29,7 +48,7 @@ VALVE_TYPE = { } -@TYPES.register('Outlet') +@TYPES.register("Outlet") class Outlet(HomeAccessory): """Generate an Outlet accessory.""" @@ -40,14 +59,15 @@ class Outlet(HomeAccessory): serv_outlet = self.add_preload_service(SERV_OUTLET) self.char_on = serv_outlet.configure_char( - CHAR_ON, value=False, setter_callback=self.set_state) + CHAR_ON, value=False, setter_callback=self.set_state + ) self.char_outlet_in_use = serv_outlet.configure_char( - CHAR_OUTLET_IN_USE, value=True) + CHAR_OUTLET_IN_USE, value=True + ) def set_state(self, value): """Move switch state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set switch state to %s', - self.entity_id, value) + _LOGGER.debug("%s: Set switch state to %s", self.entity_id, value) self._flag_state = True params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF @@ -55,15 +75,14 @@ class Outlet(HomeAccessory): def update_state(self, new_state): """Update switch state after state changed.""" - current_state = (new_state.state == STATE_ON) + current_state = new_state.state == STATE_ON if not self._flag_state: - _LOGGER.debug('%s: Set current state to %s', - self.entity_id, current_state) + _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) self.char_on.set_value(current_state) self._flag_state = False -@TYPES.register('Switch') +@TYPES.register("Switch") class Switch(HomeAccessory): """Generate a Switch accessory.""" @@ -73,33 +92,32 @@ class Switch(HomeAccessory): self._domain = split_entity_id(self.entity_id)[0] self._flag_state = False - self.activate_only = self.is_activate( - self.hass.states.get(self.entity_id)) + self.activate_only = self.is_activate(self.hass.states.get(self.entity_id)) serv_switch = self.add_preload_service(SERV_SWITCH) self.char_on = serv_switch.configure_char( - CHAR_ON, value=False, setter_callback=self.set_state) + CHAR_ON, value=False, setter_callback=self.set_state + ) def is_activate(self, state): """Check if entity is activate only.""" can_cancel = state.attributes.get(ATTR_CAN_CANCEL) - if self._domain == 'scene': + if self._domain == "scene": return True - if self._domain == 'script' and not can_cancel: + if self._domain == "script" and not can_cancel: return True return False def reset_switch(self, *args): """Reset switch to emulate activate click.""" - _LOGGER.debug('%s: Reset switch to off', self.entity_id) + _LOGGER.debug("%s: Reset switch to off", self.entity_id) self.char_on.set_value(0) def set_state(self, value): """Move switch state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set switch state to %s', - self.entity_id, value) + _LOGGER.debug("%s: Set switch state to %s", self.entity_id, value) if self.activate_only and value == 0: - _LOGGER.debug('%s: Ignoring turn_off call', self.entity_id) + _LOGGER.debug("%s: Ignoring turn_off call", self.entity_id) return self._flag_state = True params = {ATTR_ENTITY_ID: self.entity_id} @@ -113,19 +131,19 @@ class Switch(HomeAccessory): """Update switch state after state changed.""" self.activate_only = self.is_activate(new_state) if self.activate_only: - _LOGGER.debug('%s: Ignore state change, entity is activate only', - self.entity_id) + _LOGGER.debug( + "%s: Ignore state change, entity is activate only", self.entity_id + ) return - current_state = (new_state.state == STATE_ON) + current_state = new_state.state == STATE_ON if not self._flag_state: - _LOGGER.debug('%s: Set current state to %s', - self.entity_id, current_state) + _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) self.char_on.set_value(current_state) self._flag_state = False -@TYPES.register('Valve') +@TYPES.register("Valve") class Valve(HomeAccessory): """Generate a Valve accessory.""" @@ -138,16 +156,16 @@ class Valve(HomeAccessory): serv_valve = self.add_preload_service(SERV_VALVE) self.char_active = serv_valve.configure_char( - CHAR_ACTIVE, value=False, setter_callback=self.set_state) - self.char_in_use = serv_valve.configure_char( - CHAR_IN_USE, value=False) + CHAR_ACTIVE, value=False, setter_callback=self.set_state + ) + self.char_in_use = serv_valve.configure_char(CHAR_IN_USE, value=False) self.char_valve_type = serv_valve.configure_char( - CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type][1]) + CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type][1] + ) def set_state(self, value): """Move value state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set switch state to %s', - self.entity_id, value) + _LOGGER.debug("%s: Set switch state to %s", self.entity_id, value) self._flag_state = True self.char_in_use.set_value(value) params = {ATTR_ENTITY_ID: self.entity_id} @@ -156,10 +174,9 @@ class Valve(HomeAccessory): def update_state(self, new_state): """Update switch state after state changed.""" - current_state = (new_state.state == STATE_ON) + current_state = new_state.state == STATE_ON if not self._flag_state: - _LOGGER.debug('%s: Set current state to %s', - self.entity_id, current_state) + _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) self.char_active.set_value(current_state) self.char_in_use.set_value(current_state) self._flag_state = False diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 8032e00db66..e00912d340e 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -4,39 +4,70 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate.const import ( - ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTIONS, ATTR_HVAC_MODE, ATTR_MAX_TEMP, - ATTR_MIN_TEMP, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_STEP, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, - DOMAIN as DOMAIN_CLIMATE, HVAC_MODE_COOL, HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTIONS, + ATTR_HVAC_MODE, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_STEP, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + DOMAIN as DOMAIN_CLIMATE, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, - SUPPORT_TARGET_TEMPERATURE_RANGE) + SUPPORT_TARGET_TEMPERATURE_RANGE, +) from homeassistant.components.water_heater import ( DOMAIN as DOMAIN_WATER_HEATER, - SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_WATER_HEATER) + SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_WATER_HEATER, +) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, TEMP_CELSIUS, - TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from . import TYPES from .accessories import HomeAccessory, debounce from .const import ( - CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, - CHAR_CURRENT_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE, - CHAR_TARGET_HEATING_COOLING, CHAR_TARGET_TEMPERATURE, - CHAR_TEMP_DISPLAY_UNITS, DEFAULT_MAX_TEMP_WATER_HEATER, - DEFAULT_MIN_TEMP_WATER_HEATER, PROP_MAX_VALUE, PROP_MIN_STEP, - PROP_MIN_VALUE, SERV_THERMOSTAT) + CHAR_COOLING_THRESHOLD_TEMPERATURE, + CHAR_CURRENT_HEATING_COOLING, + CHAR_CURRENT_TEMPERATURE, + CHAR_HEATING_THRESHOLD_TEMPERATURE, + CHAR_TARGET_HEATING_COOLING, + CHAR_TARGET_TEMPERATURE, + CHAR_TEMP_DISPLAY_UNITS, + DEFAULT_MAX_TEMP_WATER_HEATER, + DEFAULT_MIN_TEMP_WATER_HEATER, + PROP_MAX_VALUE, + PROP_MIN_STEP, + PROP_MIN_VALUE, + SERV_THERMOSTAT, +) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()} -HC_HASS_TO_HOMEKIT = {HVAC_MODE_OFF: 0, HVAC_MODE_HEAT: 1, - HVAC_MODE_COOL: 2, HVAC_MODE_HEAT_COOL: 3} +HC_HASS_TO_HOMEKIT = { + HVAC_MODE_OFF: 0, + HVAC_MODE_HEAT: 1, + HVAC_MODE_COOL: 2, + HVAC_MODE_HEAT_COOL: 3, +} HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} HC_HASS_TO_HOMEKIT_ACTION = { @@ -47,7 +78,7 @@ HC_HASS_TO_HOMEKIT_ACTION = { } -@TYPES.register('Thermostat') +@TYPES.register("Thermostat") class Thermostat(HomeAccessory): """Generate a Thermostat accessory for a climate.""" @@ -60,132 +91,161 @@ class Thermostat(HomeAccessory): self._flag_coolingthresh = False self._flag_heatingthresh = False min_temp, max_temp = self.get_temperature_range() - temp_step = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_TARGET_TEMP_STEP, 0.5) + temp_step = self.hass.states.get(self.entity_id).attributes.get( + ATTR_TARGET_TEMP_STEP, 0.5 + ) # Add additional characteristics if auto mode is supported self.chars = [] - features = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_SUPPORTED_FEATURES, 0) + features = self.hass.states.get(self.entity_id).attributes.get( + ATTR_SUPPORTED_FEATURES, 0 + ) if features & SUPPORT_TARGET_TEMPERATURE_RANGE: - self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE, - CHAR_HEATING_THRESHOLD_TEMPERATURE)) + self.chars.extend( + (CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) + ) serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) # Current and target mode characteristics self.char_current_heat_cool = serv_thermostat.configure_char( - CHAR_CURRENT_HEATING_COOLING, value=0) + CHAR_CURRENT_HEATING_COOLING, value=0 + ) self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=0, - setter_callback=self.set_heat_cool) + CHAR_TARGET_HEATING_COOLING, value=0, setter_callback=self.set_heat_cool + ) # Current and target temperature characteristics self.char_current_temp = serv_thermostat.configure_char( - CHAR_CURRENT_TEMPERATURE, value=21.0) + CHAR_CURRENT_TEMPERATURE, value=21.0 + ) self.char_target_temp = serv_thermostat.configure_char( - CHAR_TARGET_TEMPERATURE, value=21.0, - properties={PROP_MIN_VALUE: min_temp, - PROP_MAX_VALUE: max_temp, - PROP_MIN_STEP: temp_step}, - setter_callback=self.set_target_temperature) + CHAR_TARGET_TEMPERATURE, + value=21.0, + properties={ + PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp, + PROP_MIN_STEP: temp_step, + }, + setter_callback=self.set_target_temperature, + ) # Display units characteristic self.char_display_units = serv_thermostat.configure_char( - CHAR_TEMP_DISPLAY_UNITS, value=0) + CHAR_TEMP_DISPLAY_UNITS, value=0 + ) # If the device supports it: high and low temperature characteristics self.char_cooling_thresh_temp = None self.char_heating_thresh_temp = None if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars: self.char_cooling_thresh_temp = serv_thermostat.configure_char( - CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0, - properties={PROP_MIN_VALUE: min_temp, - PROP_MAX_VALUE: max_temp, - PROP_MIN_STEP: temp_step}, - setter_callback=self.set_cooling_threshold) + CHAR_COOLING_THRESHOLD_TEMPERATURE, + value=23.0, + properties={ + PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp, + PROP_MIN_STEP: temp_step, + }, + setter_callback=self.set_cooling_threshold, + ) if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: self.char_heating_thresh_temp = serv_thermostat.configure_char( - CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0, - properties={PROP_MIN_VALUE: min_temp, - PROP_MAX_VALUE: max_temp, - PROP_MIN_STEP: temp_step}, - setter_callback=self.set_heating_threshold) + CHAR_HEATING_THRESHOLD_TEMPERATURE, + value=19.0, + properties={ + PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp, + PROP_MIN_STEP: temp_step, + }, + setter_callback=self.set_heating_threshold, + ) def get_temperature_range(self): """Return min and max temperature range.""" - max_temp = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_MAX_TEMP) - max_temp = temperature_to_homekit(max_temp, self._unit) if max_temp \ + max_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MAX_TEMP) + max_temp = ( + temperature_to_homekit(max_temp, self._unit) + if max_temp else DEFAULT_MAX_TEMP + ) max_temp = round(max_temp * 2) / 2 - min_temp = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_MIN_TEMP) - min_temp = temperature_to_homekit(min_temp, self._unit) if min_temp \ + min_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MIN_TEMP) + min_temp = ( + temperature_to_homekit(min_temp, self._unit) + if min_temp else DEFAULT_MIN_TEMP + ) min_temp = round(min_temp * 2) / 2 return min_temp, max_temp def set_heat_cool(self, value): """Change operation mode to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) + _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) self._flag_heat_cool = True hass_value = HC_HOMEKIT_TO_HASS[value] - params = { - ATTR_ENTITY_ID: self.entity_id, - ATTR_HVAC_MODE: hass_value - } + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HVAC_MODE: hass_value} self.call_service( - DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE_THERMOSTAT, params, - hass_value) + DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE_THERMOSTAT, params, hass_value + ) @debounce def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set cooling threshold temperature to %.1f°C', - self.entity_id, value) + _LOGGER.debug( + "%s: Set cooling threshold temperature to %.1f°C", self.entity_id, value + ) self._flag_coolingthresh = True low = self.char_heating_thresh_temp.value temperature = temperature_to_states(value, self._unit) params = { ATTR_ENTITY_ID: self.entity_id, ATTR_TARGET_TEMP_HIGH: temperature, - ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)} + ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit), + } self.call_service( - DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, - params, 'cooling threshold {}{}'.format(temperature, self._unit)) + DOMAIN_CLIMATE, + SERVICE_SET_TEMPERATURE_THERMOSTAT, + params, + "cooling threshold {}{}".format(temperature, self._unit), + ) @debounce def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set heating threshold temperature to %.1f°C', - self.entity_id, value) + _LOGGER.debug( + "%s: Set heating threshold temperature to %.1f°C", self.entity_id, value + ) self._flag_heatingthresh = True high = self.char_cooling_thresh_temp.value temperature = temperature_to_states(value, self._unit) params = { ATTR_ENTITY_ID: self.entity_id, ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit), - ATTR_TARGET_TEMP_LOW: temperature} + ATTR_TARGET_TEMP_LOW: temperature, + } self.call_service( - DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, - params, 'heating threshold {}{}'.format(temperature, self._unit)) + DOMAIN_CLIMATE, + SERVICE_SET_TEMPERATURE_THERMOSTAT, + params, + "heating threshold {}{}".format(temperature, self._unit), + ) @debounce def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set target temperature to %.1f°C', - self.entity_id, value) + _LOGGER.debug("%s: Set target temperature to %.1f°C", self.entity_id, value) self._flag_temperature = True temperature = temperature_to_states(value, self._unit) - params = { - ATTR_ENTITY_ID: self.entity_id, - ATTR_TEMPERATURE: temperature} + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TEMPERATURE: temperature} self.call_service( - DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, - params, '{}{}'.format(temperature, self._unit)) + DOMAIN_CLIMATE, + SERVICE_SET_TEMPERATURE_THERMOSTAT, + params, + "{}{}".format(temperature, self._unit), + ) def update_state(self, new_state): """Update thermostat state after state changed.""" @@ -207,8 +267,7 @@ class Thermostat(HomeAccessory): if self.char_cooling_thresh_temp: cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) if isinstance(cooling_thresh, (int, float)): - cooling_thresh = temperature_to_homekit(cooling_thresh, - self._unit) + cooling_thresh = temperature_to_homekit(cooling_thresh, self._unit) if not self._flag_coolingthresh: self.char_cooling_thresh_temp.set_value(cooling_thresh) self._flag_coolingthresh = False @@ -217,8 +276,7 @@ class Thermostat(HomeAccessory): if self.char_heating_thresh_temp: heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) if isinstance(heating_thresh, (int, float)): - heating_thresh = temperature_to_homekit(heating_thresh, - self._unit) + heating_thresh = temperature_to_homekit(heating_thresh, self._unit) if not self._flag_heatingthresh: self.char_heating_thresh_temp.set_value(heating_thresh) self._flag_heatingthresh = False @@ -231,18 +289,18 @@ class Thermostat(HomeAccessory): hvac_mode = new_state.state if hvac_mode and hvac_mode in HC_HASS_TO_HOMEKIT: if not self._flag_heat_cool: - self.char_target_heat_cool.set_value( - HC_HASS_TO_HOMEKIT[hvac_mode]) + self.char_target_heat_cool.set_value(HC_HASS_TO_HOMEKIT[hvac_mode]) self._flag_heat_cool = False # Set current operation mode for supported thermostats hvac_action = new_state.attributes.get(ATTR_HVAC_ACTIONS) if hvac_action: self.char_current_heat_cool.set_value( - HC_HASS_TO_HOMEKIT_ACTION[hvac_action]) + HC_HASS_TO_HOMEKIT_ACTION[hvac_action] + ) -@TYPES.register('WaterHeater') +@TYPES.register("WaterHeater") class WaterHeater(HomeAccessory): """Generate a WaterHeater accessory for a water_heater.""" @@ -257,42 +315,53 @@ class WaterHeater(HomeAccessory): serv_thermostat = self.add_preload_service(SERV_THERMOSTAT) self.char_current_heat_cool = serv_thermostat.configure_char( - CHAR_CURRENT_HEATING_COOLING, value=1) + CHAR_CURRENT_HEATING_COOLING, value=1 + ) self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=1, - setter_callback=self.set_heat_cool) + CHAR_TARGET_HEATING_COOLING, value=1, setter_callback=self.set_heat_cool + ) self.char_current_temp = serv_thermostat.configure_char( - CHAR_CURRENT_TEMPERATURE, value=50.0) + CHAR_CURRENT_TEMPERATURE, value=50.0 + ) self.char_target_temp = serv_thermostat.configure_char( - CHAR_TARGET_TEMPERATURE, value=50.0, - properties={PROP_MIN_VALUE: min_temp, - PROP_MAX_VALUE: max_temp, - PROP_MIN_STEP: 0.5}, - setter_callback=self.set_target_temperature) + CHAR_TARGET_TEMPERATURE, + value=50.0, + properties={ + PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp, + PROP_MIN_STEP: 0.5, + }, + setter_callback=self.set_target_temperature, + ) self.char_display_units = serv_thermostat.configure_char( - CHAR_TEMP_DISPLAY_UNITS, value=0) + CHAR_TEMP_DISPLAY_UNITS, value=0 + ) def get_temperature_range(self): """Return min and max temperature range.""" - max_temp = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_MAX_TEMP) - max_temp = temperature_to_homekit(max_temp, self._unit) if max_temp \ + max_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MAX_TEMP) + max_temp = ( + temperature_to_homekit(max_temp, self._unit) + if max_temp else DEFAULT_MAX_TEMP_WATER_HEATER + ) max_temp = round(max_temp * 2) / 2 - min_temp = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_MIN_TEMP) - min_temp = temperature_to_homekit(min_temp, self._unit) if min_temp \ + min_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MIN_TEMP) + min_temp = ( + temperature_to_homekit(min_temp, self._unit) + if min_temp else DEFAULT_MIN_TEMP_WATER_HEATER + ) min_temp = round(min_temp * 2) / 2 return min_temp, max_temp def set_heat_cool(self, value): """Change operation mode to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) + _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) self._flag_heat_cool = True hass_value = HC_HOMEKIT_TO_HASS[value] if hass_value != HVAC_MODE_HEAT: @@ -301,16 +370,16 @@ class WaterHeater(HomeAccessory): @debounce def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set target temperature to %.1f°C', - self.entity_id, value) + _LOGGER.debug("%s: Set target temperature to %.1f°C", self.entity_id, value) self._flag_temperature = True temperature = temperature_to_states(value, self._unit) - params = { - ATTR_ENTITY_ID: self.entity_id, - ATTR_TEMPERATURE: temperature} + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TEMPERATURE: temperature} self.call_service( - DOMAIN_WATER_HEATER, SERVICE_SET_TEMPERATURE_WATER_HEATER, - params, '{}{}'.format(temperature, self._unit)) + DOMAIN_WATER_HEATER, + SERVICE_SET_TEMPERATURE_WATER_HEATER, + params, + "{}{}".format(temperature, self._unit), + ) def update_state(self, new_state): """Update water_heater state after state change.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index b3c90ae6cbe..3b5f3c81436 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -6,54 +6,95 @@ import voluptuous as vol from homeassistant.components import fan, media_player, sensor from homeassistant.const import ( - ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, TEMP_CELSIUS) + ATTR_CODE, + ATTR_SUPPORTED_FEATURES, + CONF_NAME, + CONF_TYPE, + TEMP_CELSIUS, +) from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import ( - CONF_FEATURE, CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, - CONF_LOW_BATTERY_THRESHOLD, DEFAULT_LOW_BATTERY_THRESHOLD, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, - HOMEKIT_NOTIFY_ID, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, - TYPE_SWITCH, TYPE_VALVE) + CONF_FEATURE, + CONF_FEATURE_LIST, + CONF_LINKED_BATTERY_SENSOR, + CONF_LOW_BATTERY_THRESHOLD, + DEFAULT_LOW_BATTERY_THRESHOLD, + FEATURE_ON_OFF, + FEATURE_PLAY_PAUSE, + FEATURE_PLAY_STOP, + FEATURE_TOGGLE_MUTE, + HOMEKIT_NOTIFY_ID, + TYPE_FAUCET, + TYPE_OUTLET, + TYPE_SHOWER, + TYPE_SPRINKLER, + TYPE_SWITCH, + TYPE_VALVE, +) _LOGGER = logging.getLogger(__name__) -BASIC_INFO_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_LINKED_BATTERY_SENSOR): cv.entity_domain(sensor.DOMAIN), - vol.Optional(CONF_LOW_BATTERY_THRESHOLD, - default=DEFAULT_LOW_BATTERY_THRESHOLD): cv.positive_int, -}) +BASIC_INFO_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_LINKED_BATTERY_SENSOR): cv.entity_domain(sensor.DOMAIN), + vol.Optional( + CONF_LOW_BATTERY_THRESHOLD, default=DEFAULT_LOW_BATTERY_THRESHOLD + ): cv.positive_int, + } +) -FEATURE_SCHEMA = BASIC_INFO_SCHEMA.extend({ - vol.Optional(CONF_FEATURE_LIST, default=None): cv.ensure_list, -}) +FEATURE_SCHEMA = BASIC_INFO_SCHEMA.extend( + {vol.Optional(CONF_FEATURE_LIST, default=None): cv.ensure_list} +) -CODE_SCHEMA = BASIC_INFO_SCHEMA.extend({ - vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string), -}) +CODE_SCHEMA = BASIC_INFO_SCHEMA.extend( + {vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string)} +) -MEDIA_PLAYER_SCHEMA = vol.Schema({ - vol.Required(CONF_FEATURE): vol.All( - cv.string, vol.In((FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, - FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE))), -}) +MEDIA_PLAYER_SCHEMA = vol.Schema( + { + vol.Required(CONF_FEATURE): vol.All( + cv.string, + vol.In( + ( + FEATURE_ON_OFF, + FEATURE_PLAY_PAUSE, + FEATURE_PLAY_STOP, + FEATURE_TOGGLE_MUTE, + ) + ), + ) + } +) -SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend({ - vol.Optional(CONF_TYPE, default=TYPE_SWITCH): vol.All( - cv.string, vol.In(( - TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, - TYPE_SWITCH, TYPE_VALVE))), -}) +SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend( + { + vol.Optional(CONF_TYPE, default=TYPE_SWITCH): vol.All( + cv.string, + vol.In( + ( + TYPE_FAUCET, + TYPE_OUTLET, + TYPE_SHOWER, + TYPE_SPRINKLER, + TYPE_SWITCH, + TYPE_VALVE, + ) + ), + ) + } +) def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" if not isinstance(values, dict): - raise vol.Invalid('expected a dictionary') + raise vol.Invalid("expected a dictionary") entities = {} for entity_id, config in values.items(): @@ -61,10 +102,11 @@ def validate_entity_config(values): domain, _ = split_entity_id(entity) if not isinstance(config, dict): - raise vol.Invalid('The configuration for {} must be ' - ' a dictionary.'.format(entity)) + raise vol.Invalid( + "The configuration for {} must be " " a dictionary.".format(entity) + ) - if domain in ('alarm_control_panel', 'lock'): + if domain in ("alarm_control_panel", "lock"): config = CODE_SCHEMA(config) elif domain == media_player.const.DOMAIN: @@ -74,12 +116,13 @@ def validate_entity_config(values): params = MEDIA_PLAYER_SCHEMA(feature) key = params.pop(CONF_FEATURE) if key in feature_list: - raise vol.Invalid('A feature can be added only once for {}' - .format(entity)) + raise vol.Invalid( + "A feature can be added only once for {}".format(entity) + ) feature_list[key] = params config[CONF_FEATURE_LIST] = feature_list - elif domain == 'switch': + elif domain == "switch": config = SWITCH_TYPE_SCHEMA(config) else: @@ -94,14 +137,13 @@ def validate_media_player_features(state, feature_list): features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported_modes = [] - if features & (media_player.const.SUPPORT_TURN_ON | - media_player.const.SUPPORT_TURN_OFF): + if features & ( + media_player.const.SUPPORT_TURN_ON | media_player.const.SUPPORT_TURN_OFF + ): supported_modes.append(FEATURE_ON_OFF) - if features & (media_player.const.SUPPORT_PLAY | - media_player.const.SUPPORT_PAUSE): + if features & (media_player.const.SUPPORT_PLAY | media_player.const.SUPPORT_PAUSE): supported_modes.append(FEATURE_PLAY_PAUSE) - if features & (media_player.const.SUPPORT_PLAY | - media_player.const.SUPPORT_STOP): + if features & (media_player.const.SUPPORT_PLAY | media_player.const.SUPPORT_STOP): supported_modes.append(FEATURE_PLAY_STOP) if features & media_player.const.SUPPORT_VOLUME_MUTE: supported_modes.append(FEATURE_TOGGLE_MUTE) @@ -112,13 +154,12 @@ def validate_media_player_features(state, feature_list): error_list.append(feature) if error_list: - _LOGGER.error('%s does not support features: %s', - state.entity_id, error_list) + _LOGGER.error("%s does not support features: %s", state.entity_id, error_list) return False return True -SpeedRange = namedtuple('SpeedRange', ('start', 'target')) +SpeedRange = namedtuple("SpeedRange", ("start", "target")) SpeedRange.__doc__ += """ Maps Home Assistant speed \ values to percentage based HomeKit speeds. start: Start of the range (inclusive). @@ -133,10 +174,14 @@ class HomeKitSpeedMapping: def __init__(self, speed_list): """Initialize a new SpeedMapping object.""" if speed_list[0] != fan.SPEED_OFF: - _LOGGER.warning("%s does not contain the speed setting " - "%s as its first element. " - "Assuming that %s is equivalent to 'off'.", - speed_list, fan.SPEED_OFF, speed_list[0]) + _LOGGER.warning( + "%s does not contain the speed setting " + "%s as its first element. " + "Assuming that %s is equivalent to 'off'.", + speed_list, + fan.SPEED_OFF, + speed_list[0], + ) self.speed_ranges = OrderedDict() list_size = len(speed_list) for index, speed in enumerate(speed_list): @@ -167,11 +212,14 @@ class HomeKitSpeedMapping: def show_setup_message(hass, pincode): """Display persistent notification with setup information.""" pin = pincode.decode() - _LOGGER.info('Pincode: %s', pin) - message = 'To set up Home Assistant in the Home App, enter the ' \ - 'following code:\n### {}'.format(pin) + _LOGGER.info("Pincode: %s", pin) + message = ( + "To set up Home Assistant in the Home App, enter the " + "following code:\n### {}".format(pin) + ) hass.components.persistent_notification.create( - message, 'HomeKit Setup', HOMEKIT_NOTIFY_ID) + message, "HomeKit Setup", HOMEKIT_NOTIFY_ID + ) def dismiss_setup_message(hass): diff --git a/homeassistant/components/homekit_controller/.translations/bg.json b/homeassistant/components/homekit_controller/.translations/bg.json index be7d5d323ac..f8ce05b4bbe 100644 --- a/homeassistant/components/homekit_controller/.translations/bg.json +++ b/homeassistant/components/homekit_controller/.translations/bg.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "accessory_not_found_error": "\u0421\u0434\u0432\u043e\u044f\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0434\u043e\u0431\u0430\u0432\u0435\u043d\u043e, \u0442\u044a\u0439 \u043a\u0430\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043e.", "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e \u0441 \u0442\u043e\u0437\u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440.", + "already_in_progress": "\u0412 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u0442\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", "already_paired": "\u0422\u043e\u0437\u0438 \u0430\u043a\u0441\u0435\u0441\u043e\u0430\u0440 \u0432\u0435\u0447\u0435 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d \u0441 \u0434\u0440\u0443\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \u041c\u043e\u043b\u044f, \u0432\u044a\u0437\u0441\u0442\u0430\u043d\u043e\u0432\u0435\u0442\u0435 \u0437\u0430\u0432\u043e\u0434\u0441\u043a\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0438 \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", "ignored_model": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430\u0442\u0430 \u043d\u0430 HomeKit \u0437\u0430 \u0442\u043e\u0437\u0438 \u043c\u043e\u0434\u0435\u043b \u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0430, \u0442\u044a\u0439 \u043a\u0430\u0442\u043e \u0435 \u043d\u0430\u043b\u0438\u0446\u0435 \u043f\u043e-\u043f\u044a\u043b\u043d\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430.", "invalid_config_entry": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0435 \u043f\u043e\u043a\u0430\u0437\u0432\u0430 \u043a\u0430\u0442\u043e \u0433\u043e\u0442\u043e\u0432\u043e \u0437\u0430 \u0441\u0434\u0432\u043e\u044f\u0432\u0430\u043d\u0435, \u043d\u043e \u0432\u0435\u0447\u0435 \u0438\u043c\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0437\u0430 \u043d\u0435\u0433\u043e \u0432 Home Assistant, \u043a\u043e\u044f\u0442\u043e \u043f\u044a\u0440\u0432\u043e \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430.", @@ -9,9 +11,14 @@ }, "error": { "authentication_error": "\u0413\u0440\u0435\u0448\u0435\u043d HomeKit \u043a\u043e\u0434. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0433\u043e \u0438 \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", + "busy_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043e\u0442\u043a\u0430\u0437\u0432\u0430 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438 \u0441\u0434\u0432\u043e\u044f\u0432\u0430\u043d\u0435, \u0442\u044a\u0439 \u043a\u0430\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u0435 \u0441\u0434\u0432\u043e\u044f \u0441 \u0434\u0440\u0443\u0433 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440.", + "max_peers_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043e\u0442\u043a\u0430\u0437\u0430 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438 \u0441\u0434\u0432\u043e\u044f\u0432\u0430\u043d\u0435, \u0442\u044a\u0439 \u043a\u0430\u0442\u043e \u043d\u044f\u043c\u0430 \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e \u0437\u0430 \u0441\u0434\u0432\u043e\u044f\u0432\u0430\u043d\u0435.", + "max_tries_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043e\u0442\u043a\u0430\u0437\u0432\u0430 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438 \u0441\u0434\u0432\u043e\u044f\u0432\u0430\u043d\u0435, \u0442\u044a\u0439 \u043a\u0430\u0442\u043e \u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e \u043f\u043e\u0432\u0435\u0447\u0435 \u043e\u0442 100 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0438 \u043e\u043f\u0438\u0442\u0430 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f.", + "pairing_failed": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043d\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0435\u043d\u043e \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u043e\u043f\u0438\u0442 \u0437\u0430 \u0441\u0434\u0432\u043e\u044f\u0432\u0430\u043d\u0435 \u0441 \u0442\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \u0422\u043e\u0432\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043d \u043f\u0440\u043e\u0431\u043b\u0435\u043c \u0438\u043b\u0438 \u0432\u0430\u0448\u0435\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043c\u043e\u0436\u0435 \u0434\u0430 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0432 \u043c\u043e\u043c\u0435\u043d\u0442\u0430.", "unable_to_pair": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", "unknown_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0441\u044a\u043e\u0431\u0449\u0438 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430. \u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u0431\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, + "flow_title": "HomeKit \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e: {name}", "step": { "pair": { "data": { diff --git a/homeassistant/components/homekit_controller/.translations/da.json b/homeassistant/components/homekit_controller/.translations/da.json index 3451053eb07..2bcda4fb1ad 100644 --- a/homeassistant/components/homekit_controller/.translations/da.json +++ b/homeassistant/components/homekit_controller/.translations/da.json @@ -1,11 +1,40 @@ { "config": { + "abort": { + "accessory_not_found_error": "Parring kan ikke tilf\u00f8jes da enheden ikke l\u00e6ngere findes.", + "already_configured": "Tilbeh\u00f8ret er allerede konfigureret med denne controller.", + "already_in_progress": "Enheds konfiguration er allerede i gang.", + "already_paired": "Dette tilbeh\u00f8r er allerede parret med en anden enhed. Nulstil tilbeh\u00f8ret og pr\u00f8v igen.", + "ignored_model": "HomeKit underst\u00f8ttelse til denne model er blokeret da en mere komplet native integration er til r\u00e5dighed.", + "invalid_config_entry": "Denne enhed vises som klar til parring, men der er allerede en modstridende konfigurationspost for den i Home Assistant, som f\u00f8rst skal fjernes.", + "no_devices": "Der blev ikke fundet nogen uparrede enheder" + }, + "error": { + "authentication_error": "Forkert HomeKit kode. Kontroller den og pr\u00f8v igen.", + "busy_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den allerede parrer med en anden controller.", + "max_peers_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den ikke har nok frit parrings lager.", + "max_tries_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den har modtaget mere end 100 mislykkede godkendelsesfors\u00f8g.", + "pairing_failed": "En uh\u00e5ndteret fejl opstod under fors\u00f8g p\u00e5 at parre med denne enhed. Dette kan v\u00e6re en midlertidig fejl eller din enhed muligvis ikke underst\u00f8ttes i \u00f8jeblikket.", + "unable_to_pair": "Kunne ikke parre, pr\u00f8v venligst igen.", + "unknown_error": "Enhed rapporterede en ukendt fejl. Parring mislykkedes." + }, + "flow_title": "HomeKit tilbeh\u00f8r: {name}", "step": { + "pair": { + "data": { + "pairing_code": "Parringskode" + }, + "description": "Indtast din HomeKit parringskode (i formatet XXX-XX-XXX) for at bruge dette tilbeh\u00f8r", + "title": "Par med HomeKit tilbeh\u00f8r" + }, "user": { "data": { "device": "Enhed" - } + }, + "description": "V\u00e6lg den enhed du vil parre med", + "title": "Par med HomeKit tilbeh\u00f8r" } - } + }, + "title": "HomeKit tilbeh\u00f8r" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/pt-BR.json b/homeassistant/components/homekit_controller/.translations/pt-BR.json index 58f12bf595c..479f6c6a97c 100644 --- a/homeassistant/components/homekit_controller/.translations/pt-BR.json +++ b/homeassistant/components/homekit_controller/.translations/pt-BR.json @@ -2,14 +2,39 @@ "config": { "abort": { "accessory_not_found_error": "N\u00e3o \u00e9 poss\u00edvel adicionar o emparelhamento, pois o dispositivo n\u00e3o pode mais ser encontrado.", - "already_in_progress": "O fluxo de configura\u00e7\u00e3o para o dispositivo j\u00e1 est\u00e1 em andamento." + "already_configured": "O acess\u00f3rio j\u00e1 est\u00e1 configurado com este controlador.", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o para o dispositivo j\u00e1 est\u00e1 em andamento.", + "already_paired": "Este acess\u00f3rio j\u00e1 est\u00e1 pareado com outro dispositivo. Por favor, redefina o acess\u00f3rio e tente novamente.", + "ignored_model": "O suporte do HomeKit para este modelo est\u00e1 bloqueado, j\u00e1 que uma integra\u00e7\u00e3o nativa mais completa est\u00e1 dispon\u00edvel.", + "invalid_config_entry": "Este dispositivo est\u00e1 mostrando como pronto para parear, mas existe um conflito na configura\u00e7\u00e3o de entrada para ele no Home Assistant que deve ser removida primeiro.", + "no_devices": "N\u00e3o foi poss\u00edvel encontrar dispositivos n\u00e3o pareados" }, "error": { + "authentication_error": "C\u00f3digo HomeKit incorreto. Por favor verifique e tente novamente.", "busy_error": "O dispositivo recusou-se a adicionar o emparelhamento, uma vez que j\u00e1 est\u00e1 emparelhando com outro controlador.", "max_peers_error": "O dispositivo recusou-se a adicionar o emparelhamento, pois n\u00e3o tem armazenamento de emparelhamento gratuito.", "max_tries_error": "O dispositivo recusou-se a adicionar o emparelhamento, uma vez que recebeu mais de 100 tentativas de autentica\u00e7\u00e3o malsucedidas.", - "pairing_failed": "Ocorreu um erro sem tratamento ao tentar emparelhar com este dispositivo. Isso pode ser uma falha tempor\u00e1ria ou o dispositivo pode n\u00e3o ser suportado no momento." + "pairing_failed": "Ocorreu um erro sem tratamento ao tentar emparelhar com este dispositivo. Isso pode ser uma falha tempor\u00e1ria ou o dispositivo pode n\u00e3o ser suportado no momento.", + "unable_to_pair": "N\u00e3o \u00e9 poss\u00edvel parear, tente novamente.", + "unknown_error": "O dispositivo relatou um erro desconhecido. O pareamento falhou." }, - "flow_title": "Acess\u00f3rio HomeKit: {name}" + "flow_title": "Acess\u00f3rio HomeKit: {name}", + "step": { + "pair": { + "data": { + "pairing_code": "C\u00f3digo de pareamento" + }, + "description": "Digite seu c\u00f3digo de pareamento do HomeKit (no formato XXX-XX-XXX) para usar este acess\u00f3rio", + "title": "Parear com o acess\u00f3rio HomeKit" + }, + "user": { + "data": { + "device": "Dispositivo" + }, + "description": "Selecione o dispositivo com o qual voc\u00ea deseja parear", + "title": "Parear com o acess\u00f3rio HomeKit" + } + }, + "title": "Acess\u00f3rio HomeKit" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hans.json b/homeassistant/components/homekit_controller/.translations/zh-Hans.json index aae5b68ceb2..8d064622f7e 100644 --- a/homeassistant/components/homekit_controller/.translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/.translations/zh-Hans.json @@ -17,7 +17,7 @@ "unable_to_pair": "\u65e0\u6cd5\u914d\u5bf9\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002", "unknown_error": "\u8bbe\u5907\u62a5\u544a\u4e86\u672a\u77e5\u9519\u8bef\u3002\u914d\u5bf9\u5931\u8d25\u3002" }, - "flow_title": "HomeKit \u914d\u4ef6", + "flow_title": "HomeKit \u914d\u4ef6: {name}", "step": { "pair": { "data": { diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hant.json b/homeassistant/components/homekit_controller/.translations/zh-Hant.json index aaa2c9eda8f..7340569e64f 100644 --- a/homeassistant/components/homekit_controller/.translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/.translations/zh-Hant.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "\u8a2d\u5b9a\u4ee3\u78bc" }, - "description": "\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u4ee3\u78bc", + "description": "\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", "title": "HomeKit \u914d\u4ef6\u914d\u5c0d" }, "user": { diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 9651e497ccc..79636cea9f3 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,6 +1,7 @@ """Support for Homekit device discovery.""" import logging +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -8,10 +9,8 @@ from homeassistant.helpers import device_registry as dr # We need an import from .config_flow, without it .config_flow is never loaded. from .config_flow import HomekitControllerFlowHandler # noqa: F401 from .connection import get_accessory_information, HKDevice -from .const import ( - CONTROLLER, ENTITY_MAP, KNOWN_DEVICES -) -from .const import DOMAIN # noqa: pylint: disable=unused-import +from .const import CONTROLLER, ENTITY_MAP, KNOWN_DEVICES +from .const import DOMAIN # noqa: pylint: disable=unused-import from .storage import EntityMapStorage _LOGGER = logging.getLogger(__name__) @@ -19,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) def escape_characteristic_name(char_name): """Escape any dash or dots in a characteristics name.""" - return char_name.replace('-', '_').replace('.', '_') + return char_name.replace("-", "_").replace(".", "_") class HomeKitEntity(Entity): @@ -27,14 +26,41 @@ class HomeKitEntity(Entity): def __init__(self, accessory, devinfo): """Initialise a generic HomeKit device.""" - self._available = True self._accessory = accessory - self._aid = devinfo['aid'] - self._iid = devinfo['iid'] + self._aid = devinfo["aid"] + self._iid = devinfo["iid"] self._features = 0 self._chars = {} self.setup() + self._signals = [] + + async def async_added_to_hass(self): + """Entity added to hass.""" + self._signals.append( + self.hass.helpers.dispatcher.async_dispatcher_connect( + self._accessory.signal_state_updated, self.async_state_changed + ) + ) + + self._accessory.add_pollable_characteristics(self.pollable_characteristics) + + async def async_will_remove_from_hass(self): + """Prepare to be removed from hass.""" + self._accessory.remove_pollable_characteristics(self._aid) + + for signal_remove in self._signals: + signal_remove() + self._signals.clear() + + @property + def should_poll(self) -> bool: + """Return False. + + Data update is triggered from HKDevice. + """ + return False + def setup(self): """Configure an entity baed on its HomeKit characterstics metadata.""" # pylint: disable=import-error @@ -43,24 +69,22 @@ class HomeKitEntity(Entity): accessories = self._accessory.accessories get_uuid = CharacteristicsTypes.get_uuid - characteristic_types = [ - get_uuid(c) for c in self.get_characteristic_types() - ] + characteristic_types = [get_uuid(c) for c in self.get_characteristic_types()] - self._chars_to_poll = [] + self.pollable_characteristics = [] self._chars = {} self._char_names = {} for accessory in accessories: - if accessory['aid'] != self._aid: + if accessory["aid"] != self._aid: continue self._accessory_info = get_accessory_information(accessory) - for service in accessory['services']: - if service['iid'] != self._iid: + for service in accessory["services"]: + if service["iid"] != self._iid: continue - for char in service['characteristics']: + for char in service["characteristics"]: try: - uuid = CharacteristicsTypes.get_uuid(char['type']) + uuid = CharacteristicsTypes.get_uuid(char["type"]) except KeyError: # If a KeyError is raised its a non-standard # characteristic. We must ignore it in this case. @@ -75,93 +99,81 @@ class HomeKitEntity(Entity): from homekit.model.characteristics import CharacteristicsTypes # Build up a list of (aid, iid) tuples to poll on update() - self._chars_to_poll.append((self._aid, char['iid'])) + self.pollable_characteristics.append((self._aid, char["iid"])) # Build a map of ctype -> iid - short_name = CharacteristicsTypes.get_short(char['type']) - self._chars[short_name] = char['iid'] - self._char_names[char['iid']] = short_name + short_name = CharacteristicsTypes.get_short(char["type"]) + self._chars[short_name] = char["iid"] + self._char_names[char["iid"]] = short_name # Callback to allow entity to configure itself based on this # characteristics metadata (valid values, value ranges, features, etc) setup_fn_name = escape_characteristic_name(short_name) - setup_fn = getattr(self, '_setup_{}'.format(setup_fn_name), None) + setup_fn = getattr(self, "_setup_{}".format(setup_fn_name), None) if not setup_fn: return # pylint: disable=not-callable setup_fn(char) - async def async_update(self): - """Obtain a HomeKit device's state.""" - # pylint: disable=import-error - from homekit.exceptions import ( - AccessoryDisconnectedError, AccessoryNotFoundError, - EncryptionError) - - try: - new_values_dict = await self._accessory.get_characteristics( - self._chars_to_poll - ) - except AccessoryNotFoundError: - # Not only did the connection fail, but also the accessory is not - # visible on the network. - self._available = False - return - except (AccessoryDisconnectedError, EncryptionError): - # Temporary connection failure. Device is still available but our - # connection was dropped. - return - - self._available = True - - for (_, iid), result in new_values_dict.items(): - if 'value' not in result: + @callback + def async_state_changed(self): + """Collect new data from bridge and update the entity state in hass.""" + accessory_state = self._accessory.current_state.get(self._aid, {}) + for iid, result in accessory_state.items(): + # No value so dont process this result + if "value" not in result: continue + + # Unknown iid - this is probably for a sibling service that is part + # of the same physical accessory. Ignore it. + if iid not in self._char_names: + continue + # Callback to update the entity with this characteristic value char_name = escape_characteristic_name(self._char_names[iid]) - update_fn = getattr(self, '_update_{}'.format(char_name), None) + update_fn = getattr(self, "_update_{}".format(char_name), None) if not update_fn: continue + # pylint: disable=not-callable - update_fn(result['value']) + update_fn(result["value"]) + + self.async_write_ha_state() @property def unique_id(self): """Return the ID of this device.""" - serial = self._accessory_info['serial-number'] + serial = self._accessory_info["serial-number"] return "homekit-{}-{}".format(serial, self._iid) @property def name(self): """Return the name of the device if any.""" - return self._accessory_info.get('name') + return self._accessory_info.get("name") @property def available(self) -> bool: """Return True if entity is available.""" - return self._available + return self._accessory.available @property def device_info(self): """Return the device info.""" - accessory_serial = self._accessory_info['serial-number'] + accessory_serial = self._accessory_info["serial-number"] device_info = { - 'identifiers': { - (DOMAIN, 'serial-number', accessory_serial), - }, - 'name': self._accessory_info['name'], - 'manufacturer': self._accessory_info.get('manufacturer', ''), - 'model': self._accessory_info.get('model', ''), - 'sw_version': self._accessory_info.get('firmware.revision', ''), + "identifiers": {(DOMAIN, "serial-number", accessory_serial)}, + "name": self._accessory_info["name"], + "manufacturer": self._accessory_info.get("manufacturer", ""), + "model": self._accessory_info.get("model", ""), + "sw_version": self._accessory_info.get("firmware.revision", ""), } # Some devices only have a single accessory - we don't add a # via_device otherwise it would be self referential. - bridge_serial = self._accessory.connection_info['serial-number'] + bridge_serial = self._accessory.connection_info["serial-number"] if accessory_serial != bridge_serial: - device_info['via_device'] = ( - DOMAIN, 'serial-number', bridge_serial) + device_info["via_device"] = (DOMAIN, "serial-number", bridge_serial) return device_info @@ -185,13 +197,13 @@ async def async_setup_entry(hass, entry): device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={ - (DOMAIN, 'serial-number', conn_info['serial-number']), - (DOMAIN, 'accessory-id', conn.unique_id), + (DOMAIN, "serial-number", conn_info["serial-number"]), + (DOMAIN, "accessory-id", conn.unique_id), }, name=conn.name, - manufacturer=conn_info.get('manufacturer'), - model=conn_info.get('model'), - sw_version=conn_info.get('firmware.revision'), + manufacturer=conn_info.get("manufacturer"), + model=conn_info.get("model"), + sw_version=conn_info.get("firmware.revision"), ) return True @@ -211,7 +223,18 @@ async def async_setup(hass, config): return True +async def async_unload_entry(hass, entry): + """Disconnect from HomeKit devices before unloading entry.""" + hkid = entry.data["AccessoryPairingID"] + + if hkid in hass.data[KNOWN_DEVICES]: + connection = hass.data[KNOWN_DEVICES][hkid] + await connection.async_unload() + + return True + + async def async_remove_entry(hass, entry): """Cleanup caches before removing config entry.""" - hkid = entry.data['AccessoryPairingID'] + hkid = entry.data["AccessoryPairingID"] hass.data[ENTITY_MAP].async_delete_map(hkid) diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 93279bd626e..38ed064f374 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -3,12 +3,17 @@ import logging from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.const import ( - ATTR_BATTERY_LEVEL, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) + ATTR_BATTERY_LEVEL, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) from . import KNOWN_DEVICES, HomeKitEntity -ICON = 'mdi:security' +ICON = "mdi:security" _LOGGER = logging.getLogger(__name__) @@ -28,21 +33,20 @@ TARGET_STATE_MAP = { } -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Legacy set up platform.""" pass async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit alarm control panel.""" - hkid = config_entry.data['AccessoryPairingID'] + hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] def async_add_service(aid, service): - if service['stype'] != 'security-system': + if service["stype"] != "security-system": return False - info = {'aid': aid, 'iid': service['iid']} + info = {"aid": aid, "iid": service["iid"]} async_add_entities([HomeKitAlarmControlPanel(conn, info)], True) return True @@ -62,6 +66,7 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): """Define the homekit characteristics the entity cares about.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes + return [ CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT, CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET, @@ -102,9 +107,13 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): async def set_alarm_state(self, state, code=None): """Send state command.""" - characteristics = [{'aid': self._aid, - 'iid': self._chars['security-system-state.target'], - 'value': TARGET_STATE_MAP[state]}] + characteristics = [ + { + "aid": self._aid, + "iid": self._chars["security-system-state.target"], + "value": TARGET_STATE_MAP[state], + } + ] await self._accessory.put_characteristics(characteristics) @property @@ -113,6 +122,4 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): if self._battery_level is None: return None - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } + return {ATTR_BATTERY_LEVEL: self._battery_level} diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index b9922ea43bb..1e1c8ef5d44 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Homekit motion sensors.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.binary_sensor import BinarySensorDevice from . import KNOWN_DEVICES, HomeKitEntity @@ -8,29 +10,8 @@ from . import KNOWN_DEVICES, HomeKitEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Homekit lighting.""" - hkid = config_entry.data['AccessoryPairingID'] - conn = hass.data[KNOWN_DEVICES][hkid] - - def async_add_service(aid, service): - if service['stype'] != 'motion': - return False - info = {'aid': aid, 'iid': service['iid']} - async_add_entities([HomeKitMotionSensor(conn, info)], True) - return True - - conn.add_listener(async_add_service) - - class HomeKitMotionSensor(HomeKitEntity, BinarySensorDevice): - """Representation of a Homekit sensor.""" + """Representation of a Homekit motion sensor.""" def __init__(self, *args): """Initialise the entity.""" @@ -39,12 +20,7 @@ class HomeKitMotionSensor(HomeKitEntity, BinarySensorDevice): def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - - return [ - CharacteristicsTypes.MOTION_DETECTED, - ] + return [CharacteristicsTypes.MOTION_DETECTED] def _update_motion_detected(self, value): self._on = value @@ -52,9 +28,54 @@ class HomeKitMotionSensor(HomeKitEntity, BinarySensorDevice): @property def device_class(self): """Define this binary_sensor as a motion sensor.""" - return 'motion' + return "motion" @property def is_on(self): """Has motion been detected.""" return self._on + + +class HomeKitContactSensor(HomeKitEntity, BinarySensorDevice): + """Representation of a Homekit contact sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.CONTACT_STATE] + + def _update_contact_state(self, value): + self._state = value + + @property + def is_on(self): + """Return true if the binary sensor is on/open.""" + return self._state == 1 + + +ENTITY_TYPES = {"motion": HomeKitMotionSensor, "contact": HomeKitContactSensor} + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit lighting.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + entity_class = ENTITY_TYPES.get(service["stype"]) + if not entity_class: + return False + info = {"aid": aid, "iid": service["iid"]} + async_add_entities([entity_class(conn, info)], True) + return True + + conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index d57c3a97971..d295f607d71 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -2,12 +2,21 @@ import logging from homeassistant.components.climate import ( - ClimateDevice, DEFAULT_MIN_HUMIDITY, DEFAULT_MAX_HUMIDITY, + ClimateDevice, + DEFAULT_MIN_HUMIDITY, + DEFAULT_MAX_HUMIDITY, ) from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - CURRENT_HVAC_OFF, CURRENT_HVAC_HEAT, CURRENT_HVAC_COOL, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY) + HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + CURRENT_HVAC_OFF, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_HUMIDITY, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from . import KNOWN_DEVICES, HomeKitEntity @@ -34,21 +43,20 @@ CURRENT_MODE_HOMEKIT_TO_HASS = { } -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Legacy set up platform.""" pass async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit climate.""" - hkid = config_entry.data['AccessoryPairingID'] + hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] def async_add_service(aid, service): - if service['stype'] != 'thermostat': + if service["stype"] != "thermostat": return False - info = {'aid': aid, 'iid': service['iid']} + info = {"aid": aid, "iid": service["iid"]} async_add_entities([HomeKitClimateDevice(conn, info)], True) return True @@ -78,6 +86,7 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): """Define the homekit characteristics the entity cares about.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes + return [ CharacteristicsTypes.HEATING_COOLING_CURRENT, CharacteristicsTypes.HEATING_COOLING_TARGET, @@ -88,45 +97,42 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): ] def _setup_heating_cooling_target(self, characteristic): - if 'valid-values' in characteristic: + if "valid-values" in characteristic: valid_values = [ - val for val in DEFAULT_VALID_MODES - if val in characteristic['valid-values'] + val + for val in DEFAULT_VALID_MODES + if val in characteristic["valid-values"] ] else: valid_values = DEFAULT_VALID_MODES - if 'minValue' in characteristic: + if "minValue" in characteristic: valid_values = [ - val for val in valid_values - if val >= characteristic['minValue'] + val for val in valid_values if val >= characteristic["minValue"] ] - if 'maxValue' in characteristic: + if "maxValue" in characteristic: valid_values = [ - val for val in valid_values - if val <= characteristic['maxValue'] + val for val in valid_values if val <= characteristic["maxValue"] ] - self._valid_modes = [ - MODE_HOMEKIT_TO_HASS[mode] for mode in valid_values - ] + self._valid_modes = [MODE_HOMEKIT_TO_HASS[mode] for mode in valid_values] def _setup_temperature_target(self, characteristic): self._features |= SUPPORT_TARGET_TEMPERATURE - if 'minValue' in characteristic: - self._min_target_temp = characteristic['minValue'] + if "minValue" in characteristic: + self._min_target_temp = characteristic["minValue"] - if 'maxValue' in characteristic: - self._max_target_temp = characteristic['maxValue'] + if "maxValue" in characteristic: + self._max_target_temp = characteristic["maxValue"] def _setup_relative_humidity_target(self, characteristic): self._features |= SUPPORT_TARGET_HUMIDITY - if 'minValue' in characteristic: - self._min_target_humidity = characteristic['minValue'] + if "minValue" in characteristic: + self._min_target_humidity = characteristic["minValue"] - if 'maxValue' in characteristic: - self._max_target_humidity = characteristic['maxValue'] + if "maxValue" in characteristic: + self._max_target_humidity = characteristic["maxValue"] def _update_heating_cooling_current(self, value): # This characteristic describes the current mode of a device, @@ -157,23 +163,31 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) - characteristics = [{'aid': self._aid, - 'iid': self._chars['temperature.target'], - 'value': temp}] + characteristics = [ + {"aid": self._aid, "iid": self._chars["temperature.target"], "value": temp} + ] await self._accessory.put_characteristics(characteristics) async def async_set_humidity(self, humidity): """Set new target humidity.""" - characteristics = [{'aid': self._aid, - 'iid': self._chars['relative-humidity.target'], - 'value': humidity}] + characteristics = [ + { + "aid": self._aid, + "iid": self._chars["relative-humidity.target"], + "value": humidity, + } + ] await self._accessory.put_characteristics(characteristics) async def async_set_hvac_mode(self, hvac_mode): """Set new target operation mode.""" - characteristics = [{'aid': self._aid, - 'iid': self._chars['heating-cooling.target'], - 'value': MODE_HASS_TO_HOMEKIT[hvac_mode]}] + characteristics = [ + { + "aid": self._aid, + "iid": self._chars["heating-cooling.target"], + "value": MODE_HASS_TO_HOMEKIT[hvac_mode], + } + ] await self._accessory.put_characteristics(characteristics) @property diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 9ddb144ec9a..e5337295a70 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -12,11 +12,9 @@ from .const import DOMAIN, KNOWN_DEVICES from .connection import get_bridge_information, get_accessory_name -HOMEKIT_IGNORE = [ - 'Home Assistant Bridge', -] -HOMEKIT_DIR = '.homekit' -PAIRING_FILE = 'pairing.json' +HOMEKIT_IGNORE = ["Home Assistant Bridge"] +HOMEKIT_DIR = ".homekit" +PAIRING_FILE = "pairing.json" _LOGGER = logging.getLogger(__name__) @@ -36,7 +34,7 @@ def load_old_pairings(hass): # Find any pairings created in HA <= 0.84 if os.path.exists(data_dir): for device in os.listdir(data_dir): - if not device.startswith('hk-'): + if not device.startswith("hk-"): continue alias = device[3:] if alias in old_pairings: @@ -51,7 +49,7 @@ def load_old_pairings(hass): def find_existing_host(hass, serial): """Return a set of the configured hosts.""" for entry in hass.config_entries.async_entries(DOMAIN): - if entry.data['AccessoryPairingID'] == serial: + if entry.data["AccessoryPairingID"] == serial: return entry @@ -77,34 +75,30 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): errors = {} if user_input is not None: - key = user_input['device'] - self.hkid = self.devices[key]['id'] - self.model = self.devices[key]['md'] + key = user_input["device"] + self.hkid = self.devices[key]["id"] + self.model = self.devices[key]["md"] return await self.async_step_pair() - all_hosts = await self.hass.async_add_executor_job( - self.controller.discover, 5 - ) + all_hosts = await self.hass.async_add_executor_job(self.controller.discover, 5) self.devices = {} for host in all_hosts: - status_flags = int(host['sf']) + status_flags = int(host["sf"]) paired = not status_flags & 0x01 if paired: continue - self.devices[host['name']] = host + self.devices[host["name"]] = host if not self.devices: - return self.async_abort( - reason='no_devices' - ) + return self.async_abort(reason="no_devices") return self.async_show_form( - step_id='user', + step_id="user", errors=errors, - data_schema=vol.Schema({ - vol.Required('device'): vol.In(self.devices.keys()), - }) + data_schema=vol.Schema( + {vol.Required("device"): vol.In(self.devices.keys())} + ), ) async def async_step_zeroconf(self, discovery_info): @@ -116,41 +110,38 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # homekit_python has code to do this, but not in a form we can # easily use, so do the bare minimum ourselves here instead. properties = { - key.lower(): value - for (key, value) in discovery_info['properties'].items() + key.lower(): value for (key, value) in discovery_info["properties"].items() } # The hkid is a unique random number that looks like a pairing code. # It changes if a device is factory reset. - hkid = properties['id'] - model = properties['md'] - name = discovery_info['name'].replace('._hap._tcp.local.', '') - status_flags = int(properties['sf']) + hkid = properties["id"] + model = properties["md"] + name = discovery_info["name"].replace("._hap._tcp.local.", "") + status_flags = int(properties["sf"]) paired = not status_flags & 0x01 _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) # pylint: disable=unsupported-assignment-operation - self.context['hkid'] = hkid - self.context['title_placeholders'] = { - 'name': name, - } + self.context["hkid"] = hkid + self.context["title_placeholders"] = {"name": name} # If multiple HomekitControllerFlowHandler end up getting created # for the same accessory dont let duplicates hang around active_flows = self._async_in_progress() - if any(hkid == flow['context']['hkid'] for flow in active_flows): - return self.async_abort(reason='already_in_progress') + if any(hkid == flow["context"]["hkid"] for flow in active_flows): + return self.async_abort(reason="already_in_progress") # The configuration number increases every time the characteristic map # needs updating. Some devices use a slightly off-spec name so handle # both cases. try: - config_num = int(properties['c#']) + config_num = int(properties["c#"]) except KeyError: _LOGGER.warning( - "HomeKit device %s: c# not exposed, in violation of spec", - hkid) + "HomeKit device %s: c# not exposed, in violation of spec", hkid + ) config_num = None if paired: @@ -161,32 +152,31 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): conn = self.hass.data[KNOWN_DEVICES][hkid] if conn.config_num != config_num: _LOGGER.debug( - "HomeKit info %s: c# incremented, refreshing entities", - hkid) + "HomeKit info %s: c# incremented, refreshing entities", hkid + ) self.hass.async_create_task( - conn.async_refresh_entity_map(config_num)) - return self.async_abort(reason='already_configured') + conn.async_refresh_entity_map(config_num) + ) + return self.async_abort(reason="already_configured") old_pairings = await self.hass.async_add_executor_job( - load_old_pairings, - self.hass + load_old_pairings, self.hass ) if hkid in old_pairings: return await self.async_import_legacy_pairing( - properties, - old_pairings[hkid] + properties, old_pairings[hkid] ) # Device is paired but not to us - ignore it _LOGGER.debug("HomeKit device %s ignored as already paired", hkid) - return self.async_abort(reason='already_paired') + return self.async_abort(reason="already_paired") # Devices in HOMEKIT_IGNORE have native local integrations - users # should be encouraged to use native integration and not confused # by alternative HK API. if model in HOMEKIT_IGNORE: - return self.async_abort(reason='ignored_model') + return self.async_abort(reason="ignored_model") # Device isn't paired with us or anyone else. # But we have a 'complete' config entry for it - that is probably @@ -207,18 +197,26 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): """Migrate a legacy pairing to config entries.""" from homekit.controller.ip_implementation import IpPairing - hkid = discovery_props['id'] + hkid = discovery_props["id"] existing = find_existing_host(self.hass, hkid) if existing: _LOGGER.info( - ("Legacy configuration for homekit accessory %s" - "not loaded as already migrated"), hkid) - return self.async_abort(reason='already_configured') + ( + "Legacy configuration for homekit accessory %s" + "not loaded as already migrated" + ), + hkid, + ) + return self.async_abort(reason="already_configured") _LOGGER.info( - ("Legacy configuration %s for homekit" - "accessory migrated to config entries"), hkid) + ( + "Legacy configuration %s for homekit" + "accessory migrated to config entries" + ), + hkid, + ) pairing = IpPairing(pairing_data) @@ -247,40 +245,35 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): errors = {} if pair_info: - code = pair_info['pairing_code'] + code = pair_info["pairing_code"] try: - await self.hass.async_add_executor_job( - self.finish_pairing, code - ) + await self.hass.async_add_executor_job(self.finish_pairing, code) pairing = self.controller.pairings.get(self.hkid) if pairing: - return await self._entry_from_accessory( - pairing) + return await self._entry_from_accessory(pairing) - errors['pairing_code'] = 'unable_to_pair' + errors["pairing_code"] = "unable_to_pair" except homekit.AuthenticationError: # PairSetup M4 - SRP proof failed # PairSetup M6 - Ed25519 signature verification failed # PairVerify M4 - Decryption failed # PairVerify M4 - Device not recognised # PairVerify M4 - Ed25519 signature verification failed - errors['pairing_code'] = 'authentication_error' + errors["pairing_code"] = "authentication_error" except homekit.UnknownError: - # An error occured on the device whilst performing this + # An error occurred on the device whilst performing this # operation. - errors['pairing_code'] = 'unknown_error' + errors["pairing_code"] = "unknown_error" except homekit.MaxPeersError: # The device can't pair with any more accessories. - errors['pairing_code'] = 'max_peers_error' + errors["pairing_code"] = "max_peers_error" except homekit.AccessoryNotFoundError: # Can no longer find the device on the network - return self.async_abort(reason='accessory_not_found_error') + return self.async_abort(reason="accessory_not_found_error") except Exception: # pylint: disable=broad-except - _LOGGER.exception( - "Pairing attempt failed with an unhandled exception" - ) - errors['pairing_code'] = 'pairing_failed' + _LOGGER.exception("Pairing attempt failed with an unhandled exception") + errors["pairing_code"] = "pairing_failed" start_pairing = self.controller.start_pairing try: @@ -290,32 +283,30 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): except homekit.BusyError: # Already performing a pair setup operation with a different # controller - errors['pairing_code'] = 'busy_error' + errors["pairing_code"] = "busy_error" except homekit.MaxTriesError: # The accessory has received more than 100 unsuccessful auth # attempts. - errors['pairing_code'] = 'max_tries_error' + errors["pairing_code"] = "max_tries_error" except homekit.UnavailableError: # The accessory is already paired - cannot try to pair again. - return self.async_abort(reason='already_paired') + return self.async_abort(reason="already_paired") except homekit.AccessoryNotFoundError: # Can no longer find the device on the network - return self.async_abort(reason='accessory_not_found_error') + return self.async_abort(reason="accessory_not_found_error") except Exception: # pylint: disable=broad-except - _LOGGER.exception( - "Pairing attempt failed with an unhandled exception" - ) - errors['pairing_code'] = 'pairing_failed' + _LOGGER.exception("Pairing attempt failed with an unhandled exception") + errors["pairing_code"] = "pairing_failed" return self._async_step_pair_show_form(errors) def _async_step_pair_show_form(self, errors=None): return self.async_show_form( - step_id='pair', + step_id="pair", errors=errors or {}, - data_schema=vol.Schema({ - vol.Required('pairing_code'): vol.All(str, vol.Strip), - }) + data_schema=vol.Schema( + {vol.Required("pairing_code"): vol.All(str, vol.Strip)} + ), ) async def _entry_from_accessory(self, pairing): @@ -330,7 +321,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # available. Otherwise request a fresh copy from the API. # This removes the 'accessories' key from pairing_data at # the same time. - accessories = pairing_data.pop('accessories', None) + accessories = pairing_data.pop("accessories", None) if not accessories: accessories = await self.hass.async_add_executor_job( pairing.list_accessories_and_characteristics @@ -339,7 +330,4 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): bridge_info = get_bridge_information(accessories) name = get_accessory_name(bridge_info) - return self.async_create_entry( - title=name, - data=pairing_data, - ) + return self.async_create_entry(title=name, data=pairing_data) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index d0fc99de0d7..c4c00cb384b 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -1,10 +1,14 @@ """Helpers for managing a pairing with a HomeKit accessory or bridge.""" import asyncio +import datetime import logging -from .const import HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP +from homeassistant.helpers.event import async_track_time_interval + +from .const import DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP +DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds _LOGGER = logging.getLogger(__name__) @@ -17,34 +21,34 @@ def get_accessory_information(accessory): from homekit.model.characteristics import CharacteristicsTypes result = {} - for service in accessory['services']: - stype = service['type'].upper() - if ServicesTypes.get_short(stype) != 'accessory-information': + for service in accessory["services"]: + stype = service["type"].upper() + if ServicesTypes.get_short(stype) != "accessory-information": continue - for characteristic in service['characteristics']: - ctype = CharacteristicsTypes.get_short(characteristic['type']) - if 'value' in characteristic: - result[ctype] = characteristic['value'] + for characteristic in service["characteristics"]: + ctype = CharacteristicsTypes.get_short(characteristic["type"]) + if "value" in characteristic: + result[ctype] = characteristic["value"] return result def get_bridge_information(accessories): """Return the accessory info for the bridge.""" for accessory in accessories: - if accessory['aid'] == 1: + if accessory["aid"] == 1: return get_accessory_information(accessory) return get_accessory_information(accessories[0]) def get_accessory_name(accessory_info): """Return the name field of an accessory.""" - for field in ('name', 'model', 'manufacturer'): + for field in ("name", "model", "manufacturer"): if field in accessory_info: return accessory_info[field] return None -class HKDevice(): +class HKDevice: """HomeKit device.""" def __init__(self, hass, config_entry, pairing_data): @@ -81,25 +85,82 @@ class HKDevice(): # allow one entity to use pairing at once. self.pairing_lock = asyncio.Lock() + self.available = True + + self.signal_state_updated = "_".join((DOMAIN, self.unique_id, "state_updated")) + + # Current values of all characteristics homekit_controller is tracking. + # Key is a (accessory_id, characteristic_id) tuple. + self.current_state = {} + + self.pollable_characteristics = [] + + # If this is set polling is active and can be disabled by calling + # this method. + self._polling_interval_remover = None + + def add_pollable_characteristics(self, characteristics): + """Add (aid, iid) pairs that we need to poll.""" + self.pollable_characteristics.extend(characteristics) + + def remove_pollable_characteristics(self, accessory_id): + """Remove all pollable characteristics by accessory id.""" + self.pollable_characteristics = [ + char for char in self.pollable_characteristics if char[0] != accessory_id + ] + + def async_set_unavailable(self): + """Mark state of all entities on this connection as unavailable.""" + self.available = False + self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated) + async def async_setup(self): """Prepare to use a paired HomeKit device in homeassistant.""" cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id) if not cache: - return await self.async_refresh_entity_map(self.config_num) + if await self.async_refresh_entity_map(self.config_num): + self._polling_interval_remover = async_track_time_interval( + self.hass, self.async_update, DEFAULT_SCAN_INTERVAL + ) + return True + return False - self.accessories = cache['accessories'] - self.config_num = cache['config_num'] + self.accessories = cache["accessories"] + self.config_num = cache["config_num"] # Ensure the Pairing object has access to the latest version of the # entity map. - self.pairing.pairing_data['accessories'] = self.accessories + self.pairing.pairing_data["accessories"] = self.accessories self.async_load_platforms() self.add_entities() + await self.async_update() + + self._polling_interval_remover = async_track_time_interval( + self.hass, self.async_update, DEFAULT_SCAN_INTERVAL + ) + return True + async def async_unload(self): + """Stop interacting with device and prepare for removal from hass.""" + if self._polling_interval_remover: + self._polling_interval_remover() + + unloads = [] + for platform in self.platforms: + unloads.append( + self.hass.config_entries.async_forward_entry_unload( + self.config_entry, platform + ) + ) + + results = await asyncio.gather(*unloads) + + return False not in results + async def async_refresh_entity_map(self, config_num): """Handle setup of a HomeKit accessory.""" # pylint: disable=import-error @@ -116,22 +177,22 @@ class HKDevice(): return self.hass.data[ENTITY_MAP].async_create_or_update_map( - self.unique_id, - config_num, - self.accessories, + self.unique_id, config_num, self.accessories ) self.config_num = config_num # For BLE, the Pairing instance relies on the entity map to map # aid/iid to GATT characteristics. So push it to there as well. - self.pairing.pairing_data['accessories'] = self.accessories + self.pairing.pairing_data["accessories"] = self.accessories self.async_load_platforms() # Register and add new entities that are available self.add_entities() + await self.async_update() + return True def add_listener(self, add_entities_cb): @@ -147,11 +208,11 @@ class HKDevice(): from homekit.model.services import ServicesTypes for accessory in self.accessories: - aid = accessory['aid'] - for service in accessory['services']: - iid = service['iid'] - stype = ServicesTypes.get_short(service['type'].upper()) - service['stype'] = stype + aid = accessory["aid"] + for service in accessory["services"]: + iid = service["iid"] + stype = ServicesTypes.get_short(service["type"].upper()) + service["stype"] = stype if (aid, iid) in self.entities: # Don't add the same entity again @@ -167,8 +228,8 @@ class HKDevice(): from homekit.model.services import ServicesTypes for accessory in self.accessories: - for service in accessory['services']: - stype = ServicesTypes.get_short(service['type'].upper()) + for service in accessory["services"]: + stype = ServicesTypes.get_short(service["type"].upper()) if stype not in HOMEKIT_ACCESSORY_DISPATCH: continue @@ -178,19 +239,55 @@ class HKDevice(): self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( - self.config_entry, - platform, + self.config_entry, platform ) ) self.platforms.add(platform) + async def async_update(self, now=None): + """Poll state of all entities attached to this bridge/accessory.""" + # pylint: disable=import-error + from homekit.exceptions import ( + AccessoryDisconnectedError, + AccessoryNotFoundError, + EncryptionError, + ) + + if not self.pollable_characteristics: + _LOGGER.debug("HomeKit connection not polling any characteristics.") + return + + _LOGGER.debug("Starting HomeKit controller update") + + try: + new_values_dict = await self.get_characteristics( + self.pollable_characteristics + ) + except AccessoryNotFoundError: + # Not only did the connection fail, but also the accessory is not + # visible on the network. + self.async_set_unavailable() + return + except (AccessoryDisconnectedError, EncryptionError): + # Temporary connection failure. Device is still available but our + # connection was dropped. + return + + self.available = True + + for (aid, cid), value in new_values_dict.items(): + accessory = self.current_state.setdefault(aid, {}) + accessory[cid] = value + + self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated) + + _LOGGER.debug("Finished HomeKit controller update") + async def get_characteristics(self, *args, **kwargs): """Read latest state from homekit accessory.""" async with self.pairing_lock: chars = await self.hass.async_add_executor_job( - self.pairing.get_characteristics, - *args, - **kwargs, + self.pairing.get_characteristics, *args, **kwargs ) return chars @@ -198,16 +295,11 @@ class HKDevice(): """Control a HomeKit device state from Home Assistant.""" chars = [] for row in characteristics: - chars.append(( - row['aid'], - row['iid'], - row['value'], - )) + chars.append((row["aid"], row["iid"], row["value"])) async with self.pairing_lock: await self.hass.async_add_executor_job( - self.pairing.put_characteristics, - chars + self.pairing.put_characteristics, chars ) @property @@ -217,7 +309,7 @@ class HKDevice(): This id is random and will change if a device undergoes a hard reset. """ - return self.pairing_data['AccessoryPairingID'] + return self.pairing_data["AccessoryPairingID"] @property def connection_info(self): diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index f112737ca24..6aa5dc93662 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -1,26 +1,28 @@ """Constants for the homekit_controller component.""" -DOMAIN = 'homekit_controller' +DOMAIN = "homekit_controller" KNOWN_DEVICES = "{}-devices".format(DOMAIN) CONTROLLER = "{}-controller".format(DOMAIN) -ENTITY_MAP = '{}-entity-map'.format(DOMAIN) +ENTITY_MAP = "{}-entity-map".format(DOMAIN) -HOMEKIT_DIR = '.homekit' -PAIRING_FILE = 'pairing.json' +HOMEKIT_DIR = ".homekit" +PAIRING_FILE = "pairing.json" # Mapping from Homekit type to component. HOMEKIT_ACCESSORY_DISPATCH = { - 'lightbulb': 'light', - 'outlet': 'switch', - 'switch': 'switch', - 'thermostat': 'climate', - 'security-system': 'alarm_control_panel', - 'garage-door-opener': 'cover', - 'window': 'cover', - 'window-covering': 'cover', - 'lock-mechanism': 'lock', - 'motion': 'binary_sensor', - 'humidity': 'sensor', - 'light': 'sensor', - 'temperature': 'sensor' + "lightbulb": "light", + "outlet": "switch", + "switch": "switch", + "thermostat": "climate", + "security-system": "alarm_control_panel", + "garage-door-opener": "cover", + "window": "cover", + "window-covering": "cover", + "lock-mechanism": "lock", + "contact": "binary_sensor", + "motion": "binary_sensor", + "carbon-dioxide": "sensor", + "humidity": "sensor", + "light": "sensor", + "temperature": "sensor", } diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 7f3761d33a4..c15e0c092ac 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -2,15 +2,22 @@ import logging from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, - SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, SUPPORT_STOP, - SUPPORT_SET_TILT_POSITION, CoverDevice) -from homeassistant.const import ( - STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING) + ATTR_POSITION, + ATTR_TILT_POSITION, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_POSITION, + SUPPORT_STOP, + SUPPORT_SET_TILT_POSITION, + CoverDevice, +) +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from . import KNOWN_DEVICES, HomeKitEntity -STATE_STOPPED = 'stopped' +STATE_STOPPED = "stopped" _LOGGER = logging.getLogger(__name__) @@ -19,40 +26,31 @@ CURRENT_GARAGE_STATE_MAP = { 1: STATE_CLOSED, 2: STATE_OPENING, 3: STATE_CLOSING, - 4: STATE_STOPPED + 4: STATE_STOPPED, } -TARGET_GARAGE_STATE_MAP = { - STATE_OPEN: 0, - STATE_CLOSED: 1, - STATE_STOPPED: 2 -} +TARGET_GARAGE_STATE_MAP = {STATE_OPEN: 0, STATE_CLOSED: 1, STATE_STOPPED: 2} -CURRENT_WINDOW_STATE_MAP = { - 0: STATE_OPENING, - 1: STATE_CLOSING, - 2: STATE_STOPPED -} +CURRENT_WINDOW_STATE_MAP = {0: STATE_OPENING, 1: STATE_CLOSING, 2: STATE_STOPPED} -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Legacy set up platform.""" pass async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit covers.""" - hkid = config_entry.data['AccessoryPairingID'] + hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] def async_add_service(aid, service): - info = {'aid': aid, 'iid': service['iid']} - if service['stype'] == 'garage-door-opener': + info = {"aid": aid, "iid": service["iid"]} + if service["stype"] == "garage-door-opener": async_add_entities([HomeKitGarageDoorCover(conn, info)], True) return True - if service['stype'] in ('window-covering', 'window'): + if service["stype"] in ("window-covering", "window"): async_add_entities([HomeKitWindowCover(conn, info)], True) return True @@ -74,12 +72,13 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): @property def device_class(self): """Define this cover as a garage door.""" - return 'garage' + return "garage" def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes + return [ CharacteristicsTypes.DOOR_STATE_CURRENT, CharacteristicsTypes.DOOR_STATE_TARGET, @@ -122,9 +121,13 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): async def set_door_state(self, state): """Send state command.""" - characteristics = [{'aid': self._aid, - 'iid': self._chars['door-state.target'], - 'value': TARGET_GARAGE_STATE_MAP[state]}] + characteristics = [ + { + "aid": self._aid, + "iid": self._chars["door-state.target"], + "value": TARGET_GARAGE_STATE_MAP[state], + } + ] await self._accessory.put_characteristics(characteristics) @property @@ -133,9 +136,7 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): if self._obstruction_detected is None: return None - return { - 'obstruction-detected': self._obstruction_detected, - } + return {"obstruction-detected": self._obstruction_detected} class HomeKitWindowCover(HomeKitEntity, CoverDevice): @@ -149,13 +150,13 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): self._tilt_position = None self._obstruction_detected = None self.lock_state = None - self._features = ( - SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION) + self._features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes + return [ CharacteristicsTypes.POSITION_STATE, CharacteristicsTypes.POSITION_CURRENT, @@ -173,13 +174,13 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): def _setup_vertical_tilt_current(self, char): self._features |= ( - SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | - SUPPORT_SET_TILT_POSITION) + SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_SET_TILT_POSITION + ) def _setup_horizontal_tilt_current(self, char): self._features |= ( - SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | - SUPPORT_SET_TILT_POSITION) + SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_SET_TILT_POSITION + ) def _update_position_state(self, value): self._state = CURRENT_WINDOW_STATE_MAP[value] @@ -223,9 +224,9 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): async def async_stop_cover(self, **kwargs): """Send hold command.""" - characteristics = [{'aid': self._aid, - 'iid': self._chars['position.hold'], - 'value': 1}] + characteristics = [ + {"aid": self._aid, "iid": self._chars["position.hold"], "value": 1} + ] await self._accessory.put_characteristics(characteristics) async def async_open_cover(self, **kwargs): @@ -239,9 +240,9 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): async def async_set_cover_position(self, **kwargs): """Send position command.""" position = kwargs[ATTR_POSITION] - characteristics = [{'aid': self._aid, - 'iid': self._chars['position.target'], - 'value': position}] + characteristics = [ + {"aid": self._aid, "iid": self._chars["position.target"], "value": position} + ] await self._accessory.put_characteristics(characteristics) @property @@ -252,16 +253,23 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" tilt_position = kwargs[ATTR_TILT_POSITION] - if 'vertical-tilt.target' in self._chars: - characteristics = [{'aid': self._aid, - 'iid': self._chars['vertical-tilt.target'], - 'value': tilt_position}] + if "vertical-tilt.target" in self._chars: + characteristics = [ + { + "aid": self._aid, + "iid": self._chars["vertical-tilt.target"], + "value": tilt_position, + } + ] await self._accessory.put_characteristics(characteristics) - elif 'horizontal-tilt.target' in self._chars: - characteristics = [{'aid': self._aid, - 'iid': - self._chars['horizontal-tilt.target'], - 'value': tilt_position}] + elif "horizontal-tilt.target" in self._chars: + characteristics = [ + { + "aid": self._aid, + "iid": self._chars["horizontal-tilt.target"], + "value": tilt_position, + } + ] await self._accessory.put_characteristics(characteristics) @property @@ -269,7 +277,6 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): """Return the optional state attributes.""" state_attributes = {} if self._obstruction_detected is not None: - state_attributes['obstruction-detected'] = \ - self._obstruction_detected + state_attributes["obstruction-detected"] = self._obstruction_detected return state_attributes diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 248412c91a3..534a8c7cd18 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -2,29 +2,34 @@ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + Light, +) from . import KNOWN_DEVICES, HomeKitEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Legacy set up platform.""" pass async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit lightbulb.""" - hkid = config_entry.data['AccessoryPairingID'] + hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] def async_add_service(aid, service): - if service['stype'] != 'lightbulb': + if service["stype"] != "lightbulb": return False - info = {'aid': aid, 'iid': service['iid']} + info = {"aid": aid, "iid": service["iid"]} async_add_entities([HomeKitLight(conn, info)], True) return True @@ -47,6 +52,7 @@ class HomeKitLight(HomeKitEntity, Light): """Define the homekit characteristics the entity cares about.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes + return [ CharacteristicsTypes.ON, CharacteristicsTypes.BRIGHTNESS, @@ -115,29 +121,39 @@ class HomeKitLight(HomeKitEntity, Light): characteristics = [] if hs_color is not None: - characteristics.append({'aid': self._aid, - 'iid': self._chars['hue'], - 'value': hs_color[0]}) - characteristics.append({'aid': self._aid, - 'iid': self._chars['saturation'], - 'value': hs_color[1]}) + characteristics.append( + {"aid": self._aid, "iid": self._chars["hue"], "value": hs_color[0]} + ) + characteristics.append( + { + "aid": self._aid, + "iid": self._chars["saturation"], + "value": hs_color[1], + } + ) if brightness is not None: - characteristics.append({'aid': self._aid, - 'iid': self._chars['brightness'], - 'value': int(brightness * 100 / 255)}) + characteristics.append( + { + "aid": self._aid, + "iid": self._chars["brightness"], + "value": int(brightness * 100 / 255), + } + ) if temperature is not None: - characteristics.append({'aid': self._aid, - 'iid': self._chars['color-temperature'], - 'value': int(temperature)}) - characteristics.append({'aid': self._aid, - 'iid': self._chars['on'], - 'value': True}) + characteristics.append( + { + "aid": self._aid, + "iid": self._chars["color-temperature"], + "value": int(temperature), + } + ) + characteristics.append( + {"aid": self._aid, "iid": self._chars["on"], "value": True} + ) await self._accessory.put_characteristics(characteristics) async def async_turn_off(self, **kwargs): """Turn the specified light off.""" - characteristics = [{'aid': self._aid, - 'iid': self._chars['on'], - 'value': False}] + characteristics = [{"aid": self._aid, "iid": self._chars["on"], "value": False}] await self._accessory.put_characteristics(characteristics) diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 1449f265245..4ca118acee6 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -2,43 +2,33 @@ import logging from homeassistant.components.lock import LockDevice -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED) +from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED from . import KNOWN_DEVICES, HomeKitEntity _LOGGER = logging.getLogger(__name__) -STATE_JAMMED = 'jammed' +STATE_JAMMED = "jammed" -CURRENT_STATE_MAP = { - 0: STATE_UNLOCKED, - 1: STATE_LOCKED, - 2: STATE_JAMMED, - 3: None, -} +CURRENT_STATE_MAP = {0: STATE_UNLOCKED, 1: STATE_LOCKED, 2: STATE_JAMMED, 3: None} -TARGET_STATE_MAP = { - STATE_UNLOCKED: 0, - STATE_LOCKED: 1, -} +TARGET_STATE_MAP = {STATE_UNLOCKED: 0, STATE_LOCKED: 1} -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Legacy set up platform.""" pass async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit lock.""" - hkid = config_entry.data['AccessoryPairingID'] + hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] def async_add_service(aid, service): - if service['stype'] != 'lock-mechanism': + if service["stype"] != "lock-mechanism": return False - info = {'aid': aid, 'iid': service['iid']} + info = {"aid": aid, "iid": service["iid"]} async_add_entities([HomeKitLock(conn, info)], True) return True @@ -58,6 +48,7 @@ class HomeKitLock(HomeKitEntity, LockDevice): """Define the homekit characteristics the entity cares about.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes + return [ CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE, CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE, @@ -85,9 +76,13 @@ class HomeKitLock(HomeKitEntity, LockDevice): async def _set_lock_state(self, state): """Send state command.""" - characteristics = [{'aid': self._aid, - 'iid': self._chars['lock-mechanism.target-state'], - 'value': TARGET_STATE_MAP[state]}] + characteristics = [ + { + "aid": self._aid, + "iid": self._chars["lock-mechanism.target-state"], + "value": TARGET_STATE_MAP[state], + } + ] await self._accessory.put_characteristics(characteristics) @property @@ -96,6 +91,4 @@ class HomeKitLock(HomeKitEntity, LockDevice): if self._battery_level is None: return None - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } + return {ATTR_BATTERY_LEVEL: self._battery_level} diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 62dbf3740a3..70f6f6a3ce4 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/homekit_controller", "requirements": [ - "homekit[IP]==0.14.0" + "homekit[IP]==0.15.0" ], "dependencies": [], "zeroconf": ["_hap._tcp.local."], diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 9ffa6c6b597..596b697bede 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -1,45 +1,18 @@ """Support for Homekit sensors.""" +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.const import TEMP_CELSIUS from . import KNOWN_DEVICES, HomeKitEntity -HUMIDITY_ICON = 'mdi:water-percent' +HUMIDITY_ICON = "mdi:water-percent" TEMP_C_ICON = "mdi:thermometer" BRIGHTNESS_ICON = "mdi:brightness-6" +CO2_ICON = "mdi:periodic-table-co2" UNIT_PERCENT = "%" UNIT_LUX = "lux" - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Homekit covers.""" - hkid = config_entry.data['AccessoryPairingID'] - conn = hass.data[KNOWN_DEVICES][hkid] - - def async_add_service(aid, service): - devtype = service['stype'] - info = {'aid': aid, 'iid': service['iid']} - if devtype == 'humidity': - async_add_entities([HomeKitHumiditySensor(conn, info)], True) - return True - - if devtype == 'temperature': - async_add_entities([HomeKitTemperatureSensor(conn, info)], True) - return True - - if devtype == 'light': - async_add_entities([HomeKitLightSensor(conn, info)], True) - return True - - return False - - conn.add_listener(async_add_service) +UNIT_CO2 = "ppm" class HomeKitHumiditySensor(HomeKitEntity): @@ -52,12 +25,7 @@ class HomeKitHumiditySensor(HomeKitEntity): def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - - return [ - CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT - ] + return [CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT] @property def name(self): @@ -93,12 +61,7 @@ class HomeKitTemperatureSensor(HomeKitEntity): def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - - return [ - CharacteristicsTypes.TEMPERATURE_CURRENT - ] + return [CharacteristicsTypes.TEMPERATURE_CURRENT] @property def name(self): @@ -134,12 +97,7 @@ class HomeKitLightSensor(HomeKitEntity): def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - - return [ - CharacteristicsTypes.LIGHT_LEVEL_CURRENT - ] + return [CharacteristicsTypes.LIGHT_LEVEL_CURRENT] @property def name(self): @@ -163,3 +121,68 @@ class HomeKitLightSensor(HomeKitEntity): def state(self): """Return the current light level in lux.""" return self._state + + +class HomeKitCarbonDioxideSensor(HomeKitEntity): + """Representation of a Homekit Carbon Dioxide sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.CARBON_DIOXIDE_LEVEL] + + @property + def name(self): + """Return the name of the device.""" + return "{} {}".format(super().name, "CO2") + + @property + def icon(self): + """Return the sensor icon.""" + return CO2_ICON + + @property + def unit_of_measurement(self): + """Return units for the sensor.""" + return UNIT_CO2 + + def _update_carbon_dioxide_level(self, value): + self._state = value + + @property + def state(self): + """Return the current CO2 level in ppm.""" + return self._state + + +ENTITY_TYPES = { + "humidity": HomeKitHumiditySensor, + "temperature": HomeKitTemperatureSensor, + "light": HomeKitLightSensor, + "carbon-dioxide": HomeKitCarbonDioxideSensor, +} + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit sensors.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + entity_class = ENTITY_TYPES.get(service["stype"]) + if not entity_class: + return False + info = {"aid": aid, "iid": service["iid"]} + async_add_entities([entity_class(conn, info)], True) + return True + + conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 4a7c0a8057b..ec5a2e7cc43 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -5,7 +5,7 @@ from homeassistant.core import callback from .const import DOMAIN -ENTITY_MAP_STORAGE_KEY = '{}-entity-map'.format(DOMAIN) +ENTITY_MAP_STORAGE_KEY = "{}-entity-map".format(DOMAIN) ENTITY_MAP_STORAGE_VERSION = 1 ENTITY_MAP_SAVE_DELAY = 10 @@ -29,11 +29,7 @@ class EntityMapStorage: def __init__(self, hass): """Create a new entity map store.""" self.hass = hass - self.store = Store( - hass, - ENTITY_MAP_STORAGE_VERSION, - ENTITY_MAP_STORAGE_KEY - ) + self.store = Store(hass, ENTITY_MAP_STORAGE_VERSION, ENTITY_MAP_STORAGE_KEY) self.storage_data = {} async def async_initialize(self): @@ -43,7 +39,7 @@ class EntityMapStorage: # There is no cached data about HomeKit devices yet return - self.storage_data = raw_storage.get('pairings', {}) + self.storage_data = raw_storage.get("pairings", {}) def get_map(self, homekit_id): """Get a pairing cache item.""" @@ -51,10 +47,7 @@ class EntityMapStorage: def async_create_or_update_map(self, homekit_id, config_num, accessories): """Create a new pairing cache.""" - data = { - 'config_num': config_num, - 'accessories': accessories, - } + data = {"config_num": config_num, "accessories": accessories} self.storage_data[homekit_id] = data self._async_schedule_save() return data @@ -75,6 +68,4 @@ class EntityMapStorage: @callback def _data_to_save(self): """Return data of entity map to store in a file.""" - return { - 'pairings': self.storage_data, - } + return {"pairings": self.storage_data} diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 670ddd4db5b..7fdc6a7082f 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -10,21 +10,20 @@ OUTLET_IN_USE = "outlet_in_use" _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Legacy set up platform.""" pass async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit lock.""" - hkid = config_entry.data['AccessoryPairingID'] + hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] def async_add_service(aid, service): - if service['stype'] not in ('switch', 'outlet'): + if service["stype"] not in ("switch", "outlet"): return False - info = {'aid': aid, 'iid': service['iid']} + info = {"aid": aid, "iid": service["iid"]} async_add_entities([HomeKitSwitch(conn, info)], True) return True @@ -44,10 +43,8 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice): """Define the homekit characteristics the entity cares about.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes - return [ - CharacteristicsTypes.ON, - CharacteristicsTypes.OUTLET_IN_USE, - ] + + return [CharacteristicsTypes.ON, CharacteristicsTypes.OUTLET_IN_USE] def _update_on(self, value): self._on = value @@ -63,22 +60,16 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the specified switch on.""" self._on = True - characteristics = [{'aid': self._aid, - 'iid': self._chars['on'], - 'value': True}] + characteristics = [{"aid": self._aid, "iid": self._chars["on"], "value": True}] await self._accessory.put_characteristics(characteristics) async def async_turn_off(self, **kwargs): """Turn the specified switch off.""" - characteristics = [{'aid': self._aid, - 'iid': self._chars['on'], - 'value': False}] + characteristics = [{"aid": self._aid, "iid": self._chars["on"], "value": False}] await self._accessory.put_characteristics(characteristics) @property def device_state_attributes(self): """Return the optional state attributes.""" if self._outlet_in_use is not None: - return { - OUTLET_IN_USE: self._outlet_in_use, - } + return {OUTLET_IN_USE: self._outlet_in_use} diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 013f1eab679..0ab47247edc 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -6,259 +6,360 @@ import logging import voluptuous as vol from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_NAME, CONF_HOST, CONF_HOSTS, CONF_PASSWORD, - CONF_PLATFORM, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) + ATTR_ENTITY_ID, + ATTR_NAME, + CONF_HOST, + CONF_HOSTS, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, + STATE_UNKNOWN, +) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -DOMAIN = 'homematic' +DOMAIN = "homematic" SCAN_INTERVAL_HUB = timedelta(seconds=300) SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) -DISCOVER_SWITCHES = 'homematic.switch' -DISCOVER_LIGHTS = 'homematic.light' -DISCOVER_SENSORS = 'homematic.sensor' -DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' -DISCOVER_COVER = 'homematic.cover' -DISCOVER_CLIMATE = 'homematic.climate' -DISCOVER_LOCKS = 'homematic.locks' -DISCOVER_BATTERY = 'homematic.battery' +DISCOVER_SWITCHES = "homematic.switch" +DISCOVER_LIGHTS = "homematic.light" +DISCOVER_SENSORS = "homematic.sensor" +DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" +DISCOVER_COVER = "homematic.cover" +DISCOVER_CLIMATE = "homematic.climate" +DISCOVER_LOCKS = "homematic.locks" +DISCOVER_BATTERY = "homematic.battery" -ATTR_DISCOVER_DEVICES = 'devices' -ATTR_PARAM = 'param' -ATTR_CHANNEL = 'channel' -ATTR_ADDRESS = 'address' -ATTR_VALUE = 'value' -ATTR_VALUE_TYPE = 'value_type' -ATTR_INTERFACE = 'interface' -ATTR_ERRORCODE = 'error' -ATTR_MESSAGE = 'message' -ATTR_MODE = 'mode' -ATTR_TIME = 'time' -ATTR_UNIQUE_ID = 'unique_id' -ATTR_PARAMSET_KEY = 'paramset_key' -ATTR_PARAMSET = 'paramset' -ATTR_DISCOVERY_TYPE = 'discovery_type' -ATTR_LOW_BAT = 'LOW_BAT' -ATTR_LOWBAT = 'LOWBAT' +ATTR_DISCOVER_DEVICES = "devices" +ATTR_PARAM = "param" +ATTR_CHANNEL = "channel" +ATTR_ADDRESS = "address" +ATTR_VALUE = "value" +ATTR_VALUE_TYPE = "value_type" +ATTR_INTERFACE = "interface" +ATTR_ERRORCODE = "error" +ATTR_MESSAGE = "message" +ATTR_MODE = "mode" +ATTR_TIME = "time" +ATTR_UNIQUE_ID = "unique_id" +ATTR_PARAMSET_KEY = "paramset_key" +ATTR_PARAMSET = "paramset" +ATTR_DISCOVERY_TYPE = "discovery_type" +ATTR_LOW_BAT = "LOW_BAT" +ATTR_LOWBAT = "LOWBAT" -EVENT_KEYPRESS = 'homematic.keypress' -EVENT_IMPULSE = 'homematic.impulse' -EVENT_ERROR = 'homematic.error' +EVENT_KEYPRESS = "homematic.keypress" +EVENT_IMPULSE = "homematic.impulse" +EVENT_ERROR = "homematic.error" -SERVICE_VIRTUALKEY = 'virtualkey' -SERVICE_RECONNECT = 'reconnect' -SERVICE_SET_VARIABLE_VALUE = 'set_variable_value' -SERVICE_SET_DEVICE_VALUE = 'set_device_value' -SERVICE_SET_INSTALL_MODE = 'set_install_mode' -SERVICE_PUT_PARAMSET = 'put_paramset' +SERVICE_VIRTUALKEY = "virtualkey" +SERVICE_RECONNECT = "reconnect" +SERVICE_SET_VARIABLE_VALUE = "set_variable_value" +SERVICE_SET_DEVICE_VALUE = "set_device_value" +SERVICE_SET_INSTALL_MODE = "set_install_mode" +SERVICE_PUT_PARAMSET = "put_paramset" HM_DEVICE_TYPES = { DISCOVER_SWITCHES: [ - 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren', - 'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic', - 'IPKeySwitchPowermeter', 'IPGarage', 'IPKeySwitch', 'IPMultiIO'], - DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer', 'IPDimmer', - 'ColorEffectLight'], + "Switch", + "SwitchPowermeter", + "IOSwitch", + "IPSwitch", + "RFSiren", + "IPSwitchPowermeter", + "HMWIOSwitch", + "Rain", + "EcoLogic", + "IPKeySwitchPowermeter", + "IPGarage", + "IPKeySwitch", + "IPMultiIO", + ], + DISCOVER_LIGHTS: [ + "Dimmer", + "KeyDimmer", + "IPKeyDimmer", + "IPDimmer", + "ColorEffectLight", + ], DISCOVER_SENSORS: [ - 'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP', - 'ThermostatWall', 'AreaThermostat', 'RotaryHandleSensor', - 'WaterSensor', 'PowermeterGas', 'LuxSensor', 'WeatherSensor', - 'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor', - 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', - 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', - 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', - 'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor', - 'IPKeySwitchPowermeter', 'IPThermostatWall230V', 'IPWeatherSensorPlus', - 'IPWeatherSensorBasic', 'IPBrightnessSensor', 'IPGarage', - 'UniversalSensor', 'MotionIPV2', 'IPMultiIO', 'IPThermostatWall2'], + "SwitchPowermeter", + "Motion", + "MotionV2", + "RemoteMotion", + "MotionIP", + "ThermostatWall", + "AreaThermostat", + "RotaryHandleSensor", + "WaterSensor", + "PowermeterGas", + "LuxSensor", + "WeatherSensor", + "WeatherStation", + "ThermostatWall2", + "TemperatureDiffSensor", + "TemperatureSensor", + "CO2Sensor", + "IPSwitchPowermeter", + "HMWIOSwitch", + "FillingLevel", + "ValveDrive", + "EcoLogic", + "IPThermostatWall", + "IPSmoke", + "RFSiren", + "PresenceIP", + "IPAreaThermostat", + "IPWeatherSensor", + "RotaryHandleSensorIP", + "IPPassageSensor", + "IPKeySwitchPowermeter", + "IPThermostatWall230V", + "IPWeatherSensorPlus", + "IPWeatherSensorBasic", + "IPBrightnessSensor", + "IPGarage", + "UniversalSensor", + "MotionIPV2", + "IPMultiIO", + "IPThermostatWall2", + ], DISCOVER_CLIMATE: [ - 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', - 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', - 'ThermostatGroup', 'IPThermostatWall230V', 'IPThermostatWall2'], + "Thermostat", + "ThermostatWall", + "MAXThermostat", + "ThermostatWall2", + "MAXWallThermostat", + "IPThermostat", + "IPThermostatWall", + "ThermostatGroup", + "IPThermostatWall230V", + "IPThermostatWall2", + ], DISCOVER_BINARY_SENSORS: [ - 'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2', - 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', - 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', - 'WiredSensor', 'PresenceIP', 'IPWeatherSensor', 'IPPassageSensor', - 'SmartwareMotion', 'IPWeatherSensorPlus', 'MotionIPV2', 'WaterIP', - 'IPMultiIO', 'TiltIP', 'IPShutterContactSabotage'], - DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], - DISCOVER_LOCKS: ['KeyMatic'] + "ShutterContact", + "Smoke", + "SmokeV2", + "Motion", + "MotionV2", + "MotionIP", + "RemoteMotion", + "WeatherSensor", + "TiltSensor", + "IPShutterContact", + "HMWIOSwitch", + "MaxShutterContact", + "Rain", + "WiredSensor", + "PresenceIP", + "IPWeatherSensor", + "IPPassageSensor", + "SmartwareMotion", + "IPWeatherSensorPlus", + "MotionIPV2", + "WaterIP", + "IPMultiIO", + "TiltIP", + "IPShutterContactSabotage", + ], + DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], + DISCOVER_LOCKS: ["KeyMatic"], } -HM_IGNORE_DISCOVERY_NODE = [ - 'ACTUAL_TEMPERATURE', - 'ACTUAL_HUMIDITY' -] +HM_IGNORE_DISCOVERY_NODE = ["ACTUAL_TEMPERATURE", "ACTUAL_HUMIDITY"] HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { - 'ACTUAL_TEMPERATURE': [ - 'IPAreaThermostat', 'IPWeatherSensor', - 'IPWeatherSensorPlus', 'IPWeatherSensorBasic', - 'IPThermostatWall', 'IPThermostatWall2'], + "ACTUAL_TEMPERATURE": [ + "IPAreaThermostat", + "IPWeatherSensor", + "IPWeatherSensorPlus", + "IPWeatherSensorBasic", + "IPThermostatWall", + "IPThermostatWall2", + ] } HM_ATTRIBUTE_SUPPORT = { - 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], - 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], - 'ERROR': ['error', {0: 'No'}], - 'ERROR_SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], - 'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], - 'RSSI_PEER': ['rssi_peer', {}], - 'RSSI_DEVICE': ['rssi_device', {}], - 'VALVE_STATE': ['valve', {}], - 'LEVEL': ['level', {}], - 'BATTERY_STATE': ['battery', {}], - 'CONTROL_MODE': ['mode', { - 0: 'Auto', - 1: 'Manual', - 2: 'Away', - 3: 'Boost', - 4: 'Comfort', - 5: 'Lowering' - }], - 'POWER': ['power', {}], - 'CURRENT': ['current', {}], - 'VOLTAGE': ['voltage', {}], - 'OPERATING_VOLTAGE': ['voltage', {}], - 'WORKING': ['working', {0: 'No', 1: 'Yes'}], - 'STATE_UNCERTAIN': ['state_uncertain', {}] + "LOWBAT": ["battery", {0: "High", 1: "Low"}], + "LOW_BAT": ["battery", {0: "High", 1: "Low"}], + "ERROR": ["error", {0: "No"}], + "ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], + "SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], + "RSSI_PEER": ["rssi_peer", {}], + "RSSI_DEVICE": ["rssi_device", {}], + "VALVE_STATE": ["valve", {}], + "LEVEL": ["level", {}], + "BATTERY_STATE": ["battery", {}], + "CONTROL_MODE": [ + "mode", + {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"}, + ], + "POWER": ["power", {}], + "CURRENT": ["current", {}], + "VOLTAGE": ["voltage", {}], + "OPERATING_VOLTAGE": ["voltage", {}], + "WORKING": ["working", {0: "No", 1: "Yes"}], + "STATE_UNCERTAIN": ["state_uncertain", {}], } HM_PRESS_EVENTS = [ - 'PRESS_SHORT', - 'PRESS_LONG', - 'PRESS_CONT', - 'PRESS_LONG_RELEASE', - 'PRESS', + "PRESS_SHORT", + "PRESS_LONG", + "PRESS_CONT", + "PRESS_LONG_RELEASE", + "PRESS", ] -HM_IMPULSE_EVENTS = [ - 'SEQUENCE_OK', -] +HM_IMPULSE_EVENTS = ["SEQUENCE_OK"] -CONF_RESOLVENAMES_OPTIONS = [ - 'metadata', - 'json', - 'xml', - False -] +CONF_RESOLVENAMES_OPTIONS = ["metadata", "json", "xml", False] -DATA_HOMEMATIC = 'homematic' -DATA_STORE = 'homematic_store' -DATA_CONF = 'homematic_conf' +DATA_HOMEMATIC = "homematic" +DATA_STORE = "homematic_store" +DATA_CONF = "homematic_conf" -CONF_INTERFACES = 'interfaces' -CONF_LOCAL_IP = 'local_ip' -CONF_LOCAL_PORT = 'local_port' -CONF_PORT = 'port' -CONF_PATH = 'path' -CONF_CALLBACK_IP = 'callback_ip' -CONF_CALLBACK_PORT = 'callback_port' -CONF_RESOLVENAMES = 'resolvenames' -CONF_JSONPORT = 'jsonport' -CONF_VARIABLES = 'variables' -CONF_DEVICES = 'devices' -CONF_PRIMARY = 'primary' +CONF_INTERFACES = "interfaces" +CONF_LOCAL_IP = "local_ip" +CONF_LOCAL_PORT = "local_port" +CONF_PORT = "port" +CONF_PATH = "path" +CONF_CALLBACK_IP = "callback_ip" +CONF_CALLBACK_PORT = "callback_port" +CONF_RESOLVENAMES = "resolvenames" +CONF_JSONPORT = "jsonport" +CONF_VARIABLES = "variables" +CONF_DEVICES = "devices" +CONF_PRIMARY = "primary" -DEFAULT_LOCAL_IP = '0.0.0.0' +DEFAULT_LOCAL_IP = "0.0.0.0" DEFAULT_LOCAL_PORT = 0 DEFAULT_RESOLVENAMES = False DEFAULT_JSONPORT = 80 DEFAULT_PORT = 2001 -DEFAULT_PATH = '' -DEFAULT_USERNAME = 'Admin' -DEFAULT_PASSWORD = '' +DEFAULT_PATH = "" +DEFAULT_USERNAME = "Admin" +DEFAULT_PASSWORD = "" DEFAULT_SSL = False DEFAULT_VERIFY_SSL = False DEFAULT_CHANNEL = 1 -DEVICE_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'homematic', - vol.Required(ATTR_NAME): cv.string, - vol.Required(ATTR_ADDRESS): cv.string, - vol.Required(ATTR_INTERFACE): cv.string, - vol.Optional(ATTR_CHANNEL, default=DEFAULT_CHANNEL): vol.Coerce(int), - vol.Optional(ATTR_PARAM): cv.string, - vol.Optional(ATTR_UNIQUE_ID): cv.string, -}) +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "homematic", + vol.Required(ATTR_NAME): cv.string, + vol.Required(ATTR_ADDRESS): cv.string, + vol.Required(ATTR_INTERFACE): cv.string, + vol.Optional(ATTR_CHANNEL, default=DEFAULT_CHANNEL): vol.Coerce(int), + vol.Optional(ATTR_PARAM): cv.string, + vol.Optional(ATTR_UNIQUE_ID): cv.string, + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_INTERFACES, default={}): {cv.match_all: { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, - vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES): - vol.In(CONF_RESOLVENAMES_OPTIONS), - vol.Optional(CONF_JSONPORT, default=DEFAULT_JSONPORT): cv.port, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_CALLBACK_IP): cv.string, - vol.Optional(CONF_CALLBACK_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional( - CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - }}, - vol.Optional(CONF_HOSTS, default={}): {cv.match_all: { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - }}, - vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string, - vol.Optional(CONF_LOCAL_PORT): cv.port, - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_INTERFACES, default={}): { + cv.match_all: { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, + vol.Optional( + CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES + ): vol.In(CONF_RESOLVENAMES_OPTIONS), + vol.Optional(CONF_JSONPORT, default=DEFAULT_JSONPORT): cv.port, + vol.Optional( + CONF_USERNAME, default=DEFAULT_USERNAME + ): cv.string, + vol.Optional( + CONF_PASSWORD, default=DEFAULT_PASSWORD + ): cv.string, + vol.Optional(CONF_CALLBACK_IP): cv.string, + vol.Optional(CONF_CALLBACK_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional( + CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL + ): cv.boolean, + } + }, + vol.Optional(CONF_HOSTS, default={}): { + cv.match_all: { + vol.Required(CONF_HOST): cv.string, + vol.Optional( + CONF_USERNAME, default=DEFAULT_USERNAME + ): cv.string, + vol.Optional( + CONF_PASSWORD, default=DEFAULT_PASSWORD + ): cv.string, + } + }, + vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string, + vol.Optional(CONF_LOCAL_PORT): cv.port, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) -SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({ - vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_CHANNEL): vol.Coerce(int), - vol.Required(ATTR_PARAM): cv.string, - vol.Optional(ATTR_INTERFACE): cv.string, -}) +SCHEMA_SERVICE_VIRTUALKEY = vol.Schema( + { + vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_CHANNEL): vol.Coerce(int), + vol.Required(ATTR_PARAM): cv.string, + vol.Optional(ATTR_INTERFACE): cv.string, + } +) -SCHEMA_SERVICE_SET_VARIABLE_VALUE = vol.Schema({ - vol.Required(ATTR_NAME): cv.string, - vol.Required(ATTR_VALUE): cv.match_all, - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) +SCHEMA_SERVICE_SET_VARIABLE_VALUE = vol.Schema( + { + vol.Required(ATTR_NAME): cv.string, + vol.Required(ATTR_VALUE): cv.match_all, + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + } +) -SCHEMA_SERVICE_SET_DEVICE_VALUE = vol.Schema({ - vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_CHANNEL): vol.Coerce(int), - vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_VALUE): cv.match_all, - vol.Optional(ATTR_VALUE_TYPE): vol.In([ - 'boolean', 'dateTime.iso8601', - 'double', 'int', 'string' - ]), - vol.Optional(ATTR_INTERFACE): cv.string, -}) +SCHEMA_SERVICE_SET_DEVICE_VALUE = vol.Schema( + { + vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_CHANNEL): vol.Coerce(int), + vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_VALUE): cv.match_all, + vol.Optional(ATTR_VALUE_TYPE): vol.In( + ["boolean", "dateTime.iso8601", "double", "int", "string"] + ), + vol.Optional(ATTR_INTERFACE): cv.string, + } +) SCHEMA_SERVICE_RECONNECT = vol.Schema({}) -SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({ - vol.Required(ATTR_INTERFACE): cv.string, - vol.Optional(ATTR_TIME, default=60): cv.positive_int, - vol.Optional(ATTR_MODE, default=1): - vol.All(vol.Coerce(int), vol.In([1, 2])), - vol.Optional(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), -}) +SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema( + { + vol.Required(ATTR_INTERFACE): cv.string, + vol.Optional(ATTR_TIME, default=60): cv.positive_int, + vol.Optional(ATTR_MODE, default=1): vol.All(vol.Coerce(int), vol.In([1, 2])), + vol.Optional(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), + } +) -SCHEMA_SERVICE_PUT_PARAMSET = vol.Schema({ - vol.Required(ATTR_INTERFACE): cv.string, - vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_PARAMSET_KEY): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_PARAMSET): dict, -}) +SCHEMA_SERVICE_PUT_PARAMSET = vol.Schema( + { + vol.Required(ATTR_INTERFACE): cv.string, + vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_PARAMSET_KEY): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_PARAMSET): dict, + } +) def setup(hass, config): @@ -272,27 +373,27 @@ def setup(hass, config): # Create hosts-dictionary for pyhomematic for rname, rconfig in conf[CONF_INTERFACES].items(): remotes[rname] = { - 'ip': rconfig.get(CONF_HOST), - 'port': rconfig.get(CONF_PORT), - 'path': rconfig.get(CONF_PATH), - 'resolvenames': rconfig.get(CONF_RESOLVENAMES), - 'jsonport': rconfig.get(CONF_JSONPORT), - 'username': rconfig.get(CONF_USERNAME), - 'password': rconfig.get(CONF_PASSWORD), - 'callbackip': rconfig.get(CONF_CALLBACK_IP), - 'callbackport': rconfig.get(CONF_CALLBACK_PORT), - 'ssl': rconfig.get(CONF_SSL), - 'verify_ssl': rconfig.get(CONF_VERIFY_SSL), - 'connect': True, + "ip": rconfig.get(CONF_HOST), + "port": rconfig.get(CONF_PORT), + "path": rconfig.get(CONF_PATH), + "resolvenames": rconfig.get(CONF_RESOLVENAMES), + "jsonport": rconfig.get(CONF_JSONPORT), + "username": rconfig.get(CONF_USERNAME), + "password": rconfig.get(CONF_PASSWORD), + "callbackip": rconfig.get(CONF_CALLBACK_IP), + "callbackport": rconfig.get(CONF_CALLBACK_PORT), + "ssl": rconfig.get(CONF_SSL), + "verify_ssl": rconfig.get(CONF_VERIFY_SSL), + "connect": True, } for sname, sconfig in conf[CONF_HOSTS].items(): remotes[sname] = { - 'ip': sconfig.get(CONF_HOST), - 'port': DEFAULT_PORT, - 'username': sconfig.get(CONF_USERNAME), - 'password': sconfig.get(CONF_PASSWORD), - 'connect': False, + "ip": sconfig.get(CONF_HOST), + "port": DEFAULT_PORT, + "username": sconfig.get(CONF_USERNAME), + "password": sconfig.get(CONF_PASSWORD), + "connect": False, } # Create server thread @@ -302,15 +403,14 @@ def setup(hass, config): localport=config[DOMAIN].get(CONF_LOCAL_PORT, DEFAULT_LOCAL_PORT), remotes=remotes, systemcallback=bound_system_callback, - interface_id='homeassistant' + interface_id="homeassistant", ) # Start server thread, connect to hosts, initialize to receive events homematic.start() # Stops server when HASS is shutting down - hass.bus.listen_once( - EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop) # Init homematic hubs entity_hubs = [] @@ -336,16 +436,18 @@ def setup(hass, config): # Channel doesn't exist for device if channel not in hmdevice.ACTIONNODE[param]: - _LOGGER.error("%i is not a channel in hm device %s", - channel, address) + _LOGGER.error("%i is not a channel in hm device %s", channel, address) return # Call parameter hmdevice.actionNodeData(param, True, channel) hass.services.register( - DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey, - schema=SCHEMA_SERVICE_VIRTUALKEY) + DOMAIN, + SERVICE_VIRTUALKEY, + _hm_service_virtualkey, + schema=SCHEMA_SERVICE_VIRTUALKEY, + ) def _service_handle_value(service): """Service to call setValue method for HomeMatic system variable.""" @@ -354,8 +456,9 @@ def setup(hass, config): value = service.data[ATTR_VALUE] if entity_ids: - entities = [entity for entity in entity_hubs if - entity.entity_id in entity_ids] + entities = [ + entity for entity in entity_hubs if entity.entity_id in entity_ids + ] else: entities = entity_hubs @@ -367,16 +470,22 @@ def setup(hass, config): hub.hm_set_variable(name, value) hass.services.register( - DOMAIN, SERVICE_SET_VARIABLE_VALUE, _service_handle_value, - schema=SCHEMA_SERVICE_SET_VARIABLE_VALUE) + DOMAIN, + SERVICE_SET_VARIABLE_VALUE, + _service_handle_value, + schema=SCHEMA_SERVICE_SET_VARIABLE_VALUE, + ) def _service_handle_reconnect(service): """Service to reconnect all HomeMatic hubs.""" homematic.reconnect() hass.services.register( - DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect, - schema=SCHEMA_SERVICE_RECONNECT) + DOMAIN, + SERVICE_RECONNECT, + _service_handle_reconnect, + schema=SCHEMA_SERVICE_RECONNECT, + ) def _service_handle_device(service): """Service to call setValue method for HomeMatic devices.""" @@ -389,14 +498,14 @@ def setup(hass, config): # Convert value into correct XML-RPC Type. # https://docs.python.org/3/library/xmlrpc.client.html#xmlrpc.client.ServerProxy if value_type: - if value_type == 'int': + if value_type == "int": value = int(value) - elif value_type == 'double': + elif value_type == "double": value = float(value) - elif value_type == 'boolean': + elif value_type == "boolean": value = bool(value) - elif value_type == 'dateTime.iso8601': - value = datetime.strptime(value, '%Y%m%dT%H:%M:%S') + elif value_type == "dateTime.iso8601": + value = datetime.strptime(value, "%Y%m%dT%H:%M:%S") else: # Default is 'string' value = str(value) @@ -410,8 +519,11 @@ def setup(hass, config): hmdevice.setValue(param, value, channel) hass.services.register( - DOMAIN, SERVICE_SET_DEVICE_VALUE, _service_handle_device, - schema=SCHEMA_SERVICE_SET_DEVICE_VALUE) + DOMAIN, + SERVICE_SET_DEVICE_VALUE, + _service_handle_device, + schema=SCHEMA_SERVICE_SET_DEVICE_VALUE, + ) def _service_handle_install_mode(service): """Service to set interface into install mode.""" @@ -423,8 +535,11 @@ def setup(hass, config): homematic.setInstallMode(interface, t=time, mode=mode, address=address) hass.services.register( - DOMAIN, SERVICE_SET_INSTALL_MODE, _service_handle_install_mode, - schema=SCHEMA_SERVICE_SET_INSTALL_MODE) + DOMAIN, + SERVICE_SET_INSTALL_MODE, + _service_handle_install_mode, + schema=SCHEMA_SERVICE_SET_INSTALL_MODE, + ) def _service_put_paramset(service): """Service to call the putParamset method on a HomeMatic connection.""" @@ -438,13 +553,19 @@ def setup(hass, config): _LOGGER.debug( "Calling putParamset: %s, %s, %s, %s", - interface, address, paramset_key, paramset + interface, + address, + paramset_key, + paramset, ) homematic.putParamset(interface, address, paramset_key, paramset) hass.services.register( - DOMAIN, SERVICE_PUT_PARAMSET, _service_put_paramset, - schema=SCHEMA_SERVICE_PUT_PARAMSET) + DOMAIN, + SERVICE_PUT_PARAMSET, + _service_put_paramset, + schema=SCHEMA_SERVICE_PUT_PARAMSET, + ) return True @@ -452,17 +573,17 @@ def setup(hass, config): def _system_callback_handler(hass, config, src, *args): """System callback handler.""" # New devices available at hub - if src == 'newDevices': + if src == "newDevices": (interface_id, dev_descriptions) = args - interface = interface_id.split('-')[-1] + interface = interface_id.split("-")[-1] # Device support active? - if not hass.data[DATA_CONF][interface]['connect']: + if not hass.data[DATA_CONF][interface]["connect"]: return addresses = [] for dev in dev_descriptions: - address = dev['ADDRESS'].split(':')[0] + address = dev["ADDRESS"].split(":")[0] if address not in hass.data[DATA_STORE]: hass.data[DATA_STORE].add(address) addresses.append(address) @@ -474,40 +595,42 @@ def _system_callback_handler(hass, config, src, *args): hmdevice = hass.data[DATA_HOMEMATIC].devices[interface].get(dev) if hmdevice.EVENTNODE: - hmdevice.setEventCallback( - callback=bound_event_callback, bequeath=True) + hmdevice.setEventCallback(callback=bound_event_callback, bequeath=True) # Create HASS entities if addresses: for component_name, discovery_type in ( - ('switch', DISCOVER_SWITCHES), - ('light', DISCOVER_LIGHTS), - ('cover', DISCOVER_COVER), - ('binary_sensor', DISCOVER_BINARY_SENSORS), - ('sensor', DISCOVER_SENSORS), - ('climate', DISCOVER_CLIMATE), - ('lock', DISCOVER_LOCKS), - ('binary_sensor', DISCOVER_BATTERY)): + ("switch", DISCOVER_SWITCHES), + ("light", DISCOVER_LIGHTS), + ("cover", DISCOVER_COVER), + ("binary_sensor", DISCOVER_BINARY_SENSORS), + ("sensor", DISCOVER_SENSORS), + ("climate", DISCOVER_CLIMATE), + ("lock", DISCOVER_LOCKS), + ("binary_sensor", DISCOVER_BATTERY), + ): # Get all devices of a specific type - found_devices = _get_devices( - hass, discovery_type, addresses, interface) + found_devices = _get_devices(hass, discovery_type, addresses, interface) # When devices of this type are found # they are setup in HASS and a discovery event is fired if found_devices: - discovery.load_platform(hass, component_name, DOMAIN, { - ATTR_DISCOVER_DEVICES: found_devices, - ATTR_DISCOVERY_TYPE: discovery_type, - }, config) + discovery.load_platform( + hass, + component_name, + DOMAIN, + { + ATTR_DISCOVER_DEVICES: found_devices, + ATTR_DISCOVERY_TYPE: discovery_type, + }, + config, + ) # Homegear error message - elif src == 'error': + elif src == "error": _LOGGER.error("Error: %s", args) (interface_id, errorcode, message) = args - hass.bus.fire(EVENT_ERROR, { - ATTR_ERRORCODE: errorcode, - ATTR_MESSAGE: message - }) + hass.bus.fire(EVENT_ERROR, {ATTR_ERRORCODE: errorcode, ATTR_MESSAGE: message}) def _get_devices(hass, discovery_type, keys, interface): @@ -520,8 +643,10 @@ def _get_devices(hass, discovery_type, keys, interface): metadata = {} # Class not supported by discovery type - if discovery_type != DISCOVER_BATTERY and \ - class_name not in HM_DEVICE_TYPES[discovery_type]: + if ( + discovery_type != DISCOVER_BATTERY + and class_name not in HM_DEVICE_TYPES[discovery_type] + ): continue # Load metadata needed to generate a parameter list @@ -531,11 +656,9 @@ def _get_devices(hass, discovery_type, keys, interface): metadata.update(device.BINARYNODE) elif discovery_type == DISCOVER_BATTERY: if ATTR_LOWBAT in device.ATTRIBUTENODE: - metadata.update( - {ATTR_LOWBAT: device.ATTRIBUTENODE[ATTR_LOWBAT]}) + metadata.update({ATTR_LOWBAT: device.ATTRIBUTENODE[ATTR_LOWBAT]}) elif ATTR_LOW_BAT in device.ATTRIBUTENODE: - metadata.update( - {ATTR_LOW_BAT: device.ATTRIBUTENODE[ATTR_LOW_BAT]}) + metadata.update({ATTR_LOW_BAT: device.ATTRIBUTENODE[ATTR_LOW_BAT]}) else: continue else: @@ -543,21 +666,22 @@ def _get_devices(hass, discovery_type, keys, interface): # Generate options for 1...n elements with 1...n parameters for param, channels in metadata.items(): - if param in HM_IGNORE_DISCOVERY_NODE and class_name not in \ - HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS.get(param, []): + if ( + param in HM_IGNORE_DISCOVERY_NODE + and class_name not in HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS.get(param, []) + ): continue # Add devices - _LOGGER.debug("%s: Handling %s: %s: %s", - discovery_type, key, param, channels) + _LOGGER.debug( + "%s: Handling %s: %s: %s", discovery_type, key, param, channels + ) for channel in channels: name = _create_ha_id( - name=device.NAME, channel=channel, param=param, - count=len(channels) + name=device.NAME, channel=channel, param=param, count=len(channels) ) unique_id = _create_ha_id( - name=key, channel=channel, param=param, - count=len(channels) + name=key, channel=channel, param=param, count=len(channels) ) device_dict = { CONF_PLATFORM: "homematic", @@ -565,7 +689,7 @@ def _get_devices(hass, discovery_type, keys, interface): ATTR_INTERFACE: interface, ATTR_NAME: name, ATTR_CHANNEL: channel, - ATTR_UNIQUE_ID: unique_id + ATTR_UNIQUE_ID: unique_id, } if param is not None: device_dict[ATTR_PARAM] = param @@ -575,8 +699,7 @@ def _get_devices(hass, discovery_type, keys, interface): DEVICE_SCHEMA(device_dict) device_arr.append(device_dict) except vol.MultipleInvalid as err: - _LOGGER.error("Invalid device config: %s", - str(err)) + _LOGGER.error("Invalid device config: %s", str(err)) return device_arr @@ -613,24 +736,19 @@ def _hm_event_handler(hass, interface, device, caller, attribute, value): if attribute not in hmdevice.EVENTNODE: return - _LOGGER.debug("Event %s for %s channel %i", attribute, - hmdevice.NAME, channel) + _LOGGER.debug("Event %s for %s channel %i", attribute, hmdevice.NAME, channel) # Keypress event if attribute in HM_PRESS_EVENTS: - hass.bus.fire(EVENT_KEYPRESS, { - ATTR_NAME: hmdevice.NAME, - ATTR_PARAM: attribute, - ATTR_CHANNEL: channel - }) + hass.bus.fire( + EVENT_KEYPRESS, + {ATTR_NAME: hmdevice.NAME, ATTR_PARAM: attribute, ATTR_CHANNEL: channel}, + ) return # Impulse event if attribute in HM_IMPULSE_EVENTS: - hass.bus.fire(EVENT_IMPULSE, { - ATTR_NAME: hmdevice.NAME, - ATTR_CHANNEL: channel - }) + hass.bus.fire(EVENT_IMPULSE, {ATTR_NAME: hmdevice.NAME, ATTR_CHANNEL: channel}) return _LOGGER.warning("Event is unknown and not forwarded") @@ -640,8 +758,8 @@ def _device_from_servicecall(hass, service): """Extract HomeMatic device from service call.""" address = service.data.get(ATTR_ADDRESS) interface = service.data.get(ATTR_INTERFACE) - if address == 'BIDCOS-RF': - address = 'BidCoS-RF' + if address == "BIDCOS-RF": + address = "BidCoS-RF" if interface: return hass.data[DATA_HOMEMATIC].devices[interface].get(address) @@ -664,12 +782,12 @@ class HMHub(Entity): self._state = None # Load data - self.hass.helpers.event.track_time_interval( - self._update_hub, SCAN_INTERVAL_HUB) + self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB) self.hass.add_job(self._update_hub, None) self.hass.helpers.event.track_time_interval( - self._update_variables, SCAN_INTERVAL_VARIABLES) + self._update_variables, SCAN_INTERVAL_VARIABLES + ) self.hass.add_job(self._update_variables, None) @property @@ -799,8 +917,8 @@ class HMDevice(Entity): attr[data[0]] = value # Static attributes - attr['id'] = self._hmdevice.ADDRESS - attr['interface'] = self._interface + attr["id"] = self._hmdevice.ADDRESS + attr["interface"] = self._interface return attr @@ -811,8 +929,7 @@ class HMDevice(Entity): # Initialize self._homematic = self.hass.data[DATA_HOMEMATIC] - self._hmdevice = \ - self._homematic.devices[self._interface][self._address] + self._hmdevice = self._homematic.devices[self._interface][self._address] self._connected = True try: @@ -825,13 +942,11 @@ class HMDevice(Entity): self._available = not self._hmdevice.UNREACH except Exception as err: # pylint: disable=broad-except self._connected = False - _LOGGER.error("Exception while linking %s: %s", - self._address, str(err)) + _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) def _hm_event_callback(self, device, caller, attribute, value): """Handle all pyhomematic device events.""" - _LOGGER.debug("%s received event '%s' value: %s", self._name, - attribute, value) + _LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value) has_changed = False # Is data needed for this instance? @@ -855,10 +970,14 @@ class HMDevice(Entity): channels_to_sub = set() # Push data to channels_to_sub from hmdevice metadata - for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE, - self._hmdevice.ATTRIBUTENODE, - self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE, - self._hmdevice.ACTIONNODE): + for metadata in ( + self._hmdevice.SENSORNODE, + self._hmdevice.BINARYNODE, + self._hmdevice.ATTRIBUTENODE, + self._hmdevice.WRITENODE, + self._hmdevice.EVENTNODE, + self._hmdevice.ACTIONNODE, + ): for node, channels in metadata.items(): # Data is needed for this instance if node in self._data: @@ -872,16 +991,14 @@ class HMDevice(Entity): try: channels_to_sub.add(int(channel)) except (ValueError, TypeError): - _LOGGER.error("Invalid channel in metadata from %s", - self._name) + _LOGGER.error("Invalid channel in metadata from %s", self._name) # Set callbacks for channel in channels_to_sub: - _LOGGER.debug( - "Subscribe channel %d from %s", channel, self._name) + _LOGGER.debug("Subscribe channel %d from %s", channel, self._name) self._hmdevice.setEventCallback( - callback=self._hm_event_callback, bequeath=False, - channel=channel) + callback=self._hm_event_callback, bequeath=False, channel=channel + ) def _load_data_from_hm(self): """Load first value from pyhomematic.""" @@ -890,11 +1007,11 @@ class HMDevice(Entity): # Read data from pyhomematic for metadata, funct in ( - (self._hmdevice.ATTRIBUTENODE, - self._hmdevice.getAttributeData), - (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), - (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), - (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData)): + (self._hmdevice.ATTRIBUTENODE, self._hmdevice.getAttributeData), + (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), + (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), + (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData), + ): for node in metadata: if metadata[node] and node in self._data: self._data[node] = funct(name=node, channel=self._channel) diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 8a1db6a8a7b..064b6f3e009 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -2,8 +2,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.homematic import ( - ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY) +from homeassistant.components.homematic import ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY from homeassistant.const import DEVICE_CLASS_BATTERY from . import ATTR_DISCOVER_DEVICES, HMDevice @@ -11,19 +10,19 @@ from . import ATTR_DISCOVER_DEVICES, HMDevice _LOGGER = logging.getLogger(__name__) SENSOR_TYPES_CLASS = { - 'IPShutterContact': 'opening', - 'IPShutterContactSabotage': 'opening', - 'MaxShutterContact': 'opening', - 'Motion': 'motion', - 'MotionV2': 'motion', - 'PresenceIP': 'motion', - 'Remote': None, - 'RemoteMotion': None, - 'ShutterContact': 'opening', - 'Smoke': 'smoke', - 'SmokeV2': 'smoke', - 'TiltSensor': None, - 'WeatherSensor': None, + "IPShutterContact": "opening", + "IPShutterContactSabotage": "opening", + "MaxShutterContact": "opening", + "Motion": "motion", + "MotionV2": "motion", + "PresenceIP": "motion", + "Remote": None, + "RemoteMotion": None, + "ShutterContact": "opening", + "Smoke": "smoke", + "SmokeV2": "smoke", + "TiltSensor": None, + "WeatherSensor": None, } @@ -56,8 +55,8 @@ class HMBinarySensor(HMDevice, BinarySensorDevice): def device_class(self): """Return the class of this sensor from DEVICE_CLASSES.""" # If state is MOTION (Only RemoteMotion working) - if self._state == 'MOTION': - return 'motion' + if self._state == "MOTION": + return "motion" return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None) def _init_data_struct(self): diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 09a1452a48d..1a2f642f91c 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -3,24 +3,24 @@ import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_BOOST, - PRESET_COMFORT, PRESET_ECO, SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE) + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from . import ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice _LOGGER = logging.getLogger(__name__) -HM_TEMP_MAP = [ - 'ACTUAL_TEMPERATURE', - 'TEMPERATURE', -] +HM_TEMP_MAP = ["ACTUAL_TEMPERATURE", "TEMPERATURE"] -HM_HUMI_MAP = [ - 'ACTUAL_HUMIDITY', - 'HUMIDITY', -] +HM_HUMI_MAP = ["ACTUAL_HUMIDITY", "HUMIDITY"] HM_PRESET_MAP = { "BOOST_MODE": PRESET_BOOST, @@ -28,8 +28,8 @@ HM_PRESET_MAP = { "LOWERING_MODE": PRESET_ECO, } -HM_CONTROL_MODE = 'CONTROL_MODE' -HMIP_CONTROL_MODE = 'SET_POINT_MODE' +HM_CONTROL_MODE = "CONTROL_MODE" +HMIP_CONTROL_MODE = "SET_POINT_MODE" SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -66,10 +66,10 @@ class HMThermostat(HMDevice, ClimateDevice): Need to be one of HVAC_MODE_*. """ - if self.current_temperature <= self._hmdevice.OFF_VALUE + 0.5: + if self.target_temperature <= self._hmdevice.OFF_VALUE + 0.5: return HVAC_MODE_OFF if "MANU_MODE" in self._hmdevice.ACTIONNODE: - if self._hm_controll_mode == self._hmdevice.MANU_MODE: + if self._hm_control_mode == self._hmdevice.MANU_MODE: return HVAC_MODE_HEAT return HVAC_MODE_AUTO @@ -91,11 +91,11 @@ class HMThermostat(HMDevice, ClimateDevice): @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" - if self._data.get('BOOST_MODE', False): - return 'boost' + if self._data.get("BOOST_MODE", False): + return "boost" # Get the name of the mode - mode = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][self._hm_controll_mode] + mode = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][self._hm_control_mode] mode = mode.lower() # Filter HVAC states @@ -173,20 +173,22 @@ class HMThermostat(HMDevice, ClimateDevice): return 0.5 @property - def _hm_controll_mode(self): + def _hm_control_mode(self): """Return Control mode.""" if HMIP_CONTROL_MODE in self._data: return self._data[HMIP_CONTROL_MODE] # Homematic - return self._data['CONTROL_MODE'] + return self._data["CONTROL_MODE"] def _init_data_struct(self): """Generate a data dict (self._data) from the Homematic metadata.""" self._state = next(iter(self._hmdevice.WRITENODE.keys())) self._data[self._state] = None - if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE or \ - HMIP_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE: + if ( + HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE + or HMIP_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE + ): self._data[HM_CONTROL_MODE] = None for node in self._hmdevice.SENSORNODE.keys(): diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index 28e66f39a50..893b3ce8921 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -2,7 +2,10 @@ import logging from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_TILT_POSITION, CoverDevice) + ATTR_POSITION, + ATTR_TILT_POSITION, + CoverDevice, +) from homeassistant.const import STATE_UNKNOWN from . import ATTR_DISCOVER_DEVICES, HMDevice @@ -67,8 +70,7 @@ class HMCover(HMDevice, CoverDevice): self._state = "LEVEL" self._data.update({self._state: STATE_UNKNOWN}) if "LEVEL_2" in self._hmdevice.WRITENODE: - self._data.update( - {'LEVEL_2': STATE_UNKNOWN}) + self._data.update({"LEVEL_2": STATE_UNKNOWN}) @property def current_cover_tilt_position(self): @@ -76,10 +78,10 @@ class HMCover(HMDevice, CoverDevice): None is unknown, 0 is closed, 100 is fully open. """ - if 'LEVEL_2' not in self._data: + if "LEVEL_2" not in self._data: return None - return int(self._data.get('LEVEL_2', 0) * 100) + return int(self._data.get("LEVEL_2", 0) * 100) def set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index f9bc785d3f4..971a8a9cac0 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -2,8 +2,15 @@ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_EFFECT, Light) + ATTR_BRIGHTNESS, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_EFFECT, + Light, +) from . import ATTR_DISCOVER_DEVICES, HMDevice @@ -32,7 +39,7 @@ class HMLight(HMDevice, Light): def brightness(self): """Return the brightness of this light between 0..255.""" # Is dimmer? - if self._state == 'LEVEL': + if self._state == "LEVEL": return int(self._hm_get_state() * 255) return None @@ -47,7 +54,7 @@ class HMLight(HMDevice, Light): @property def supported_features(self): """Flag supported features.""" - if 'COLOR' in self._hmdevice.WRITENODE: + if "COLOR" in self._hmdevice.WRITENODE: return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_EFFECT return SUPPORT_BRIGHTNESS @@ -57,7 +64,7 @@ class HMLight(HMDevice, Light): if not self.supported_features & SUPPORT_COLOR: return None hue, sat = self._hmdevice.get_hs_color() - return hue*360.0, sat*100.0 + return hue * 360.0, sat * 100.0 @property def effect_list(self): @@ -76,7 +83,7 @@ class HMLight(HMDevice, Light): def turn_on(self, **kwargs): """Turn the light on and/or change color or color effect settings.""" if ATTR_TRANSITION in kwargs: - self._hmdevice.setValue('RAMP_TIME', kwargs[ATTR_TRANSITION]) + self._hmdevice.setValue("RAMP_TIME", kwargs[ATTR_TRANSITION]) if ATTR_BRIGHTNESS in kwargs and self._state == "LEVEL": percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255 @@ -86,8 +93,9 @@ class HMLight(HMDevice, Light): if ATTR_HS_COLOR in kwargs: self._hmdevice.set_hs_color( - hue=kwargs[ATTR_HS_COLOR][0]/360.0, - saturation=kwargs[ATTR_HS_COLOR][1]/100.0) + hue=kwargs[ATTR_HS_COLOR][0] / 360.0, + saturation=kwargs[ATTR_HS_COLOR][1] / 100.0, + ) if ATTR_EFFECT in kwargs: self._hmdevice.set_effect(kwargs[ATTR_EFFECT]) diff --git a/homeassistant/components/homematic/notify.py b/homeassistant/components/homematic/notify.py index 74ea7095b41..9fd94b9832c 100644 --- a/homeassistant/components/homematic/notify.py +++ b/homeassistant/components/homematic/notify.py @@ -4,22 +4,33 @@ import logging import voluptuous as vol from homeassistant.components.notify import ( - ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) + ATTR_DATA, + PLATFORM_SCHEMA, + BaseNotificationService, +) import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper from . import ( - ATTR_ADDRESS, ATTR_CHANNEL, ATTR_INTERFACE, ATTR_PARAM, ATTR_VALUE, DOMAIN, - SERVICE_SET_DEVICE_VALUE) + ATTR_ADDRESS, + ATTR_CHANNEL, + ATTR_INTERFACE, + ATTR_PARAM, + ATTR_VALUE, + DOMAIN, + SERVICE_SET_DEVICE_VALUE, +) _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_CHANNEL): vol.Coerce(int), - vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_VALUE): cv.match_all, - vol.Optional(ATTR_INTERFACE): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_CHANNEL): vol.Coerce(int), + vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_VALUE): cv.match_all, + vol.Optional(ATTR_INTERFACE): cv.string, + } +) def get_service(hass, config, discovery_info=None): @@ -28,7 +39,7 @@ def get_service(hass, config, discovery_info=None): ATTR_ADDRESS: config[ATTR_ADDRESS], ATTR_CHANNEL: config[ATTR_CHANNEL], ATTR_PARAM: config[ATTR_PARAM], - ATTR_VALUE: config[ATTR_VALUE] + ATTR_VALUE: config[ATTR_VALUE], } if ATTR_INTERFACE in config: data[ATTR_INTERFACE] = config[ATTR_INTERFACE] diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index fca8c746a49..a3fc9f7e0fa 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -8,56 +8,59 @@ from . import ATTR_DISCOVER_DEVICES, HMDevice _LOGGER = logging.getLogger(__name__) HM_STATE_HA_CAST = { - 'RotaryHandleSensor': {0: 'closed', 1: 'tilted', 2: 'open'}, - 'RotaryHandleSensorIP': {0: 'closed', 1: 'tilted', 2: 'open'}, - 'WaterSensor': {0: 'dry', 1: 'wet', 2: 'water'}, - 'CO2Sensor': {0: 'normal', 1: 'added', 2: 'strong'}, - 'IPSmoke': {0: 'off', 1: 'primary', 2: 'intrusion', 3: 'secondary'}, - 'RFSiren': { - 0: 'disarmed', 1: 'extsens_armed', 2: 'allsens_armed', - 3: 'alarm_blocked'}, + "RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"}, + "RotaryHandleSensorIP": {0: "closed", 1: "tilted", 2: "open"}, + "WaterSensor": {0: "dry", 1: "wet", 2: "water"}, + "CO2Sensor": {0: "normal", 1: "added", 2: "strong"}, + "IPSmoke": {0: "off", 1: "primary", 2: "intrusion", 3: "secondary"}, + "RFSiren": { + 0: "disarmed", + 1: "extsens_armed", + 2: "allsens_armed", + 3: "alarm_blocked", + }, } HM_UNIT_HA_CAST = { - 'HUMIDITY': '%', - 'TEMPERATURE': '°C', - 'ACTUAL_TEMPERATURE': '°C', - 'BRIGHTNESS': '#', - 'POWER': POWER_WATT, - 'CURRENT': 'mA', - 'VOLTAGE': 'V', - 'ENERGY_COUNTER': ENERGY_WATT_HOUR, - 'GAS_POWER': 'm3', - 'GAS_ENERGY_COUNTER': 'm3', - 'LUX': 'lx', - 'ILLUMINATION': 'lx', - 'CURRENT_ILLUMINATION': 'lx', - 'AVERAGE_ILLUMINATION': 'lx', - 'LOWEST_ILLUMINATION': 'lx', - 'HIGHEST_ILLUMINATION': 'lx', - 'RAIN_COUNTER': 'mm', - 'WIND_SPEED': 'km/h', - 'WIND_DIRECTION': '°', - 'WIND_DIRECTION_RANGE': '°', - 'SUNSHINEDURATION': '#', - 'AIR_PRESSURE': 'hPa', - 'FREQUENCY': 'Hz', - 'VALUE': '#', + "HUMIDITY": "%", + "TEMPERATURE": "°C", + "ACTUAL_TEMPERATURE": "°C", + "BRIGHTNESS": "#", + "POWER": POWER_WATT, + "CURRENT": "mA", + "VOLTAGE": "V", + "ENERGY_COUNTER": ENERGY_WATT_HOUR, + "GAS_POWER": "m3", + "GAS_ENERGY_COUNTER": "m3", + "LUX": "lx", + "ILLUMINATION": "lx", + "CURRENT_ILLUMINATION": "lx", + "AVERAGE_ILLUMINATION": "lx", + "LOWEST_ILLUMINATION": "lx", + "HIGHEST_ILLUMINATION": "lx", + "RAIN_COUNTER": "mm", + "WIND_SPEED": "km/h", + "WIND_DIRECTION": "°", + "WIND_DIRECTION_RANGE": "°", + "SUNSHINEDURATION": "#", + "AIR_PRESSURE": "hPa", + "FREQUENCY": "Hz", + "VALUE": "#", } HM_ICON_HA_CAST = { - 'WIND_SPEED': 'mdi:weather-windy', - 'HUMIDITY': 'mdi:water-percent', - 'TEMPERATURE': 'mdi:thermometer', - 'ACTUAL_TEMPERATURE': 'mdi:thermometer', - 'LUX': 'mdi:weather-sunny', - 'CURRENT_ILLUMINATION': 'mdi:weather-sunny', - 'AVERAGE_ILLUMINATION': 'mdi:weather-sunny', - 'LOWEST_ILLUMINATION': 'mdi:weather-sunny', - 'HIGHEST_ILLUMINATION': 'mdi:weather-sunny', - 'BRIGHTNESS': 'mdi:invert-colors', - 'POWER': 'mdi:flash-red-eye', - 'CURRENT': 'mdi:flash-red-eye', + "WIND_SPEED": "mdi:weather-windy", + "HUMIDITY": "mdi:water-percent", + "TEMPERATURE": "mdi:thermometer", + "ACTUAL_TEMPERATURE": "mdi:thermometer", + "LUX": "mdi:weather-sunny", + "CURRENT_ILLUMINATION": "mdi:weather-sunny", + "AVERAGE_ILLUMINATION": "mdi:weather-sunny", + "LOWEST_ILLUMINATION": "mdi:weather-sunny", + "HIGHEST_ILLUMINATION": "mdi:weather-sunny", + "BRIGHTNESS": "mdi:invert-colors", + "POWER": "mdi:flash-red-eye", + "CURRENT": "mdi:flash-red-eye", } diff --git a/homeassistant/components/homematicip_cloud/.translations/bg.json b/homeassistant/components/homematicip_cloud/.translations/bg.json new file mode 100644 index 00000000000..d2b9a1b1761 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/bg.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0411\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "connection_aborted": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 HMIP \u0441\u044a\u0440\u0432\u044a\u0440", + "unknown": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430." + }, + "error": { + "invalid_pin": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u041f\u0418\u041d \u043a\u043e\u0434, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", + "press_the_button": "\u041c\u043e\u043b\u044f, \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0441\u0438\u043d\u0438\u044f \u0431\u0443\u0442\u043e\u043d.", + "register_failed": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", + "timeout_button": "\u0421\u0438\u043d\u0438\u044f \u0431\u0443\u0442\u043e\u043d \u043d\u0435 \u0431\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + }, + "step": { + "init": { + "data": { + "hapid": "ID \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f (SGTIN)", + "name": "\u0418\u043c\u0435 (\u043d\u0435\u0437\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0441\u0435 \u043a\u0430\u0442\u043e \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u043d\u0430 \u0438\u043c\u0435\u043d\u0430\u0442\u0430 \u043d\u0430 \u0432\u0441\u0438\u0447\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430)", + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434 (\u043d\u0435\u0437\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e)" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 HomematicIP \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + }, + "link": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0441\u0438\u043d\u0438\u044f \u0431\u0443\u0442\u043e\u043d \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0438 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"\u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435\", \u0437\u0430 \u0434\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u0442\u0435 HomematicIP \u0441 Home Assistant. \n\n![\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + } + }, + "title": "HomematicIP \u041e\u0431\u043b\u0430\u043a" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json index d2d33455191..f95ccf57272 100644 --- a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json +++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Accesspoint \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "Access point \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "connection_aborted": "\u7121\u6cd5\u9023\u7dda\u81f3 HMIP \u4f3a\u670d\u5668", "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, @@ -14,15 +14,15 @@ "step": { "init": { "data": { - "hapid": "Accesspoint ID (SGTIN)", + "hapid": "Access point ID (SGTIN)", "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u88dd\u7f6e\u7684\u5b57\u9996\u7528\uff09", "pin": "PIN \u78bc\uff08\u9078\u9805\uff09" }, - "title": "\u9078\u64c7 HomematicIP Accesspoint" + "title": "\u9078\u64c7 HomematicIP Access point" }, "link": { "description": "\u6309\u4e0b AP \u4e0a\u7684\u85cd\u8272\u6309\u9215\u8207\u50b3\u9001\u6309\u9215\uff0c\u4ee5\u65bc Home Assistant \u4e0a\u9032\u884c HomematicIP \u8a3b\u518a\u3002\n\n![\u6a4b\u63a5\u5668\u4e0a\u7684\u6309\u9215\u4f4d\u7f6e](/static/images/config_flows/config_homematicip_cloud.png)", - "title": "\u9023\u7d50 Accesspoint" + "title": "\u9023\u7d50 Access point" } }, "title": "HomematicIP Cloud" diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index f73ce5a9d21..f2d84095b19 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -13,61 +13,78 @@ from homeassistant.helpers.typing import ConfigType from .config_flow import configured_haps from .const import ( - CONF_ACCESSPOINT, CONF_AUTHTOKEN, DOMAIN, HMIPC_AUTHTOKEN, HMIPC_HAPID, - HMIPC_NAME) + CONF_ACCESSPOINT, + CONF_AUTHTOKEN, + DOMAIN, + HMIPC_AUTHTOKEN, + HMIPC_HAPID, + HMIPC_NAME, +) from .device import HomematicipGenericDevice # noqa: F401 from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 _LOGGER = logging.getLogger(__name__) -ATTR_DURATION = 'duration' -ATTR_ENDTIME = 'endtime' -ATTR_TEMPERATURE = 'temperature' -ATTR_ACCESSPOINT_ID = 'accesspoint_id' +ATTR_DURATION = "duration" +ATTR_ENDTIME = "endtime" +ATTR_TEMPERATURE = "temperature" +ATTR_ACCESSPOINT_ID = "accesspoint_id" -SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION = 'activate_eco_mode_with_duration' -SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD = 'activate_eco_mode_with_period' -SERVICE_ACTIVATE_VACATION = 'activate_vacation' -SERVICE_DEACTIVATE_ECO_MODE = 'deactivate_eco_mode' -SERVICE_DEACTIVATE_VACATION = 'deactivate_vacation' +SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION = "activate_eco_mode_with_duration" +SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD = "activate_eco_mode_with_period" +SERVICE_ACTIVATE_VACATION = "activate_vacation" +SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode" +SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" -CONFIG_SCHEMA = vol.Schema({ - vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({ - vol.Optional(CONF_NAME, default=''): vol.Any(cv.string), - vol.Required(CONF_ACCESSPOINT): cv.string, - vol.Required(CONF_AUTHTOKEN): cv.string, - })]), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + vol.Optional(DOMAIN, default=[]): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(CONF_NAME, default=""): vol.Any(cv.string), + vol.Required(CONF_ACCESSPOINT): cv.string, + vol.Required(CONF_AUTHTOKEN): cv.string, + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) -SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION = vol.Schema({ - vol.Required(ATTR_DURATION): cv.positive_int, - vol.Optional(ATTR_ACCESSPOINT_ID): - vol.All(str, vol.Length(min=24, max=24)), -}) +SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION = vol.Schema( + { + vol.Required(ATTR_DURATION): cv.positive_int, + vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), + } +) -SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD = vol.Schema({ - vol.Required(ATTR_ENDTIME): cv.datetime, - vol.Optional(ATTR_ACCESSPOINT_ID): - vol.All(str, vol.Length(min=24, max=24)), -}) +SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD = vol.Schema( + { + vol.Required(ATTR_ENDTIME): cv.datetime, + vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), + } +) -SCHEMA_ACTIVATE_VACATION = vol.Schema({ - vol.Required(ATTR_ENDTIME): cv.datetime, - vol.Required(ATTR_TEMPERATURE, default=18.0): - vol.All(vol.Coerce(float), vol.Range(min=0, max=55)), - vol.Optional(ATTR_ACCESSPOINT_ID): - vol.All(str, vol.Length(min=24, max=24)), -}) +SCHEMA_ACTIVATE_VACATION = vol.Schema( + { + vol.Required(ATTR_ENDTIME): cv.datetime, + vol.Required(ATTR_TEMPERATURE, default=18.0): vol.All( + vol.Coerce(float), vol.Range(min=0, max=55) + ), + vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), + } +) -SCHEMA_DEACTIVATE_ECO_MODE = vol.Schema({ - vol.Optional(ATTR_ACCESSPOINT_ID): - vol.All(str, vol.Length(min=24, max=24)), -}) +SCHEMA_DEACTIVATE_ECO_MODE = vol.Schema( + {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} +) -SCHEMA_DEACTIVATE_VACATION = vol.Schema({ - vol.Optional(ATTR_ACCESSPOINT_ID): - vol.All(str, vol.Length(min=24, max=24)), -}) +SCHEMA_DEACTIVATE_VACATION = vol.Schema( + {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} +) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -78,14 +95,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: for conf in accesspoints: if conf[CONF_ACCESSPOINT] not in configured_haps(hass): - hass.async_add_job(hass.config_entries.flow.async_init( - DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, - data={ - HMIPC_HAPID: conf[CONF_ACCESSPOINT], - HMIPC_AUTHTOKEN: conf[CONF_AUTHTOKEN], - HMIPC_NAME: conf[CONF_NAME], - } - )) + hass.async_add_job( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + HMIPC_HAPID: conf[CONF_ACCESSPOINT], + HMIPC_AUTHTOKEN: conf[CONF_AUTHTOKEN], + HMIPC_NAME: conf[CONF_NAME], + }, + ) + ) async def _async_activate_eco_mode_with_duration(service): """Service to activate eco mode with duration.""" @@ -102,9 +122,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await home.activate_absence_with_duration(duration) hass.services.async_register( - DOMAIN, SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION, + DOMAIN, + SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION, _async_activate_eco_mode_with_duration, - schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION) + schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION, + ) async def _async_activate_eco_mode_with_period(service): """Service to activate eco mode with period.""" @@ -121,9 +143,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await home.activate_absence_with_period(endtime) hass.services.async_register( - DOMAIN, SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD, + DOMAIN, + SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD, _async_activate_eco_mode_with_period, - schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD) + schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD, + ) async def _async_activate_vacation(service): """Service to activate vacation.""" @@ -141,8 +165,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await home.activate_vacation(endtime, temperature) hass.services.async_register( - DOMAIN, SERVICE_ACTIVATE_VACATION, _async_activate_vacation, - schema=SCHEMA_ACTIVATE_VACATION) + DOMAIN, + SERVICE_ACTIVATE_VACATION, + _async_activate_vacation, + schema=SCHEMA_ACTIVATE_VACATION, + ) async def _async_deactivate_eco_mode(service): """Service to deactivate eco mode.""" @@ -158,8 +185,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await home.deactivate_absence() hass.services.async_register( - DOMAIN, SERVICE_DEACTIVATE_ECO_MODE, _async_deactivate_eco_mode, - schema=SCHEMA_DEACTIVATE_ECO_MODE) + DOMAIN, + SERVICE_DEACTIVATE_ECO_MODE, + _async_deactivate_eco_mode, + schema=SCHEMA_DEACTIVATE_ECO_MODE, + ) async def _async_deactivate_vacation(service): """Service to deactivate vacation.""" @@ -175,8 +205,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await home.deactivate_vacation() hass.services.async_register( - DOMAIN, SERVICE_DEACTIVATE_VACATION, _async_deactivate_vacation, - schema=SCHEMA_DEACTIVATE_VACATION) + DOMAIN, + SERVICE_DEACTIVATE_VACATION, + _async_deactivate_vacation, + schema=SCHEMA_DEACTIVATE_VACATION, + ) def _get_home(hapid: str): """Return a HmIP home.""" @@ -191,7 +224,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an access point from a config entry.""" hap = HomematicipHAP(hass, entry) - hapid = entry.data[HMIPC_HAPID].replace('-', '').upper() + hapid = entry.data[HMIPC_HAPID].replace("-", "").upper() hass.data[DOMAIN][hapid] = hap if not await hap.async_setup(): @@ -201,12 +234,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_registry = await dr.async_get_registry(hass) home = hap.home # Add the HAP name from configuration if set. - hapname = home.label \ - if not home.name else "{} {}".format(home.label, home.name) + hapname = home.label if not home.name else "{} {}".format(home.label, home.name) device_registry.async_get_or_create( config_entry_id=home.id, identifiers={(DOMAIN, home.id)}, - manufacturer='eQ-3', + manufacturer="eQ-3", name=hapname, model=home.modelType, sw_version=home.currentAPVersion, diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index ccd19f26d68..38097afc1b6 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -8,25 +8,28 @@ from homematicip.base.enums import WindowState from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED) + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID _LOGGER = logging.getLogger(__name__) -CONST_ALARM_CONTROL_PANEL_NAME = 'HmIP Alarm Control Panel' +CONST_ALARM_CONTROL_PANEL_NAME = "HmIP Alarm Control Panel" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud alarm control devices.""" pass -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -51,7 +54,7 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): self.alarm_state = STATE_ALARM_DISARMED for security_zone in security_zones: - if security_zone.label == 'INTERNAL': + if security_zone.label == "INTERNAL": self._internal_alarm_zone = security_zone else: self._external_alarm_zone = security_zone @@ -62,8 +65,7 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): activation_state = self._home.get_security_zones_activation() # check arm_away if activation_state == (True, True): - if self._internal_alarm_zone_state or \ - self._external_alarm_zone_state: + if self._internal_alarm_zone_state or self._external_alarm_zone_state: return STATE_ALARM_TRIGGERED return STATE_ALARM_ARMED_AWAY # check arm_home @@ -102,8 +104,7 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): def _async_device_changed(self, *args, **kwargs): """Handle device state changes.""" - _LOGGER.debug("Event %s (%s)", self.name, - CONST_ALARM_CONTROL_PANEL_NAME) + _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME) self.async_schedule_update_ha_state() @property @@ -122,8 +123,10 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): @property def available(self) -> bool: """Device available.""" - return not self._internal_alarm_zone.unreach or \ - not self._external_alarm_zone.unreach + return ( + not self._internal_alarm_zone.unreach + or not self._external_alarm_zone.unreach + ) @property def unique_id(self) -> str: @@ -133,11 +136,13 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): def _get_zone_alarm_state(security_zone) -> bool: if security_zone.active: - if (security_zone.sabotage or - security_zone.motionDetected or - security_zone.presenceDetected or - security_zone.windowState == WindowState.OPEN or - security_zone.windowState == WindowState.TILTED): + if ( + security_zone.sabotage + or security_zone.motionDetected + or security_zone.presenceDetected + or security_zone.windowState == WindowState.OPEN + or security_zone.windowState == WindowState.TILTED + ): return True return False diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 696b00883ae..7bb7718f0b3 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -2,20 +2,38 @@ import logging from homematicip.aio.device import ( - AsyncDevice, AsyncFullFlushContactInterface, AsyncMotionDetectorIndoor, - AsyncMotionDetectorOutdoor, AsyncMotionDetectorPushButton, - AsyncPresenceDetectorIndoor, AsyncRotaryHandleSensor, AsyncShutterContact, - AsyncSmokeDetector, AsyncWaterSensor, AsyncWeatherSensor, - AsyncWeatherSensorPlus, AsyncWeatherSensorPro) + AsyncContactInterface, + AsyncDevice, + AsyncFullFlushContactInterface, + AsyncMotionDetectorIndoor, + AsyncMotionDetectorOutdoor, + AsyncMotionDetectorPushButton, + AsyncPresenceDetectorIndoor, + AsyncRotaryHandleSensor, + AsyncShutterContact, + AsyncShutterContactMagnetic, + AsyncSmokeDetector, + AsyncWaterSensor, + AsyncWeatherSensor, + AsyncWeatherSensorPlus, + AsyncWeatherSensorPro, +) from homematicip.aio.group import AsyncSecurityGroup, AsyncSecurityZoneGroup from homematicip.aio.home import AsyncHome from homematicip.base.enums import SmokeDetectorAlarmType, WindowState from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, DEVICE_CLASS_DOOR, DEVICE_CLASS_LIGHT, - DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, - DEVICE_CLASS_PRESENCE, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, - BinarySensorDevice) + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + BinarySensorDevice, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -24,36 +42,44 @@ from .device import ATTR_GROUP_MEMBER_UNREACHABLE _LOGGER = logging.getLogger(__name__) -ATTR_LOW_BATTERY = 'low_battery' -ATTR_MOTIONDETECTED = 'motion detected' -ATTR_PRESENCEDETECTED = 'presence detected' -ATTR_POWERMAINSFAILURE = 'power mains failure' -ATTR_WINDOWSTATE = 'window state' -ATTR_MOISTUREDETECTED = 'moisture detected' -ATTR_WATERLEVELDETECTED = 'water level detected' -ATTR_SMOKEDETECTORALARM = 'smoke detector alarm' -ATTR_TODAY_SUNSHINE_DURATION = 'today_sunshine_duration_in_minutes' +ATTR_LOW_BATTERY = "low_battery" +ATTR_MOTIONDETECTED = "motion detected" +ATTR_PRESENCEDETECTED = "presence detected" +ATTR_POWERMAINSFAILURE = "power mains failure" +ATTR_WINDOWSTATE = "window state" +ATTR_MOISTUREDETECTED = "moisture detected" +ATTR_WATERLEVELDETECTED = "water level detected" +ATTR_SMOKEDETECTORALARM = "smoke detector alarm" +ATTR_TODAY_SUNSHINE_DURATION = "today_sunshine_duration_in_minutes" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud binary sensor devices.""" pass -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: - if isinstance(device, AsyncFullFlushContactInterface): + if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): devices.append(HomematicipContactInterface(home, device)) - if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)): + if isinstance( + device, + (AsyncShutterContact, AsyncShutterContactMagnetic, AsyncRotaryHandleSensor), + ): devices.append(HomematicipShutterContact(home, device)) - if isinstance(device, (AsyncMotionDetectorIndoor, - AsyncMotionDetectorOutdoor, - AsyncMotionDetectorPushButton)): + if isinstance( + device, + ( + AsyncMotionDetectorIndoor, + AsyncMotionDetectorOutdoor, + AsyncMotionDetectorPushButton, + ), + ): devices.append(HomematicipMotionDetector(home, device)) if isinstance(device, AsyncPresenceDetectorIndoor): devices.append(HomematicipPresenceDetector(home, device)) @@ -61,11 +87,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, devices.append(HomematicipSmokeDetector(home, device)) if isinstance(device, AsyncWaterSensor): devices.append(HomematicipWaterDetector(home, device)) - if isinstance(device, (AsyncWeatherSensorPlus, - AsyncWeatherSensorPro)): + if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): devices.append(HomematicipRainSensor(home, device)) - if isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, - AsyncWeatherSensorPro)): + if isinstance( + device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) + ): devices.append(HomematicipStormSensor(home, device)) devices.append(HomematicipSunshineSensor(home, device)) if isinstance(device, AsyncDevice) and device.lowBat is not None: @@ -81,8 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities(devices) -class HomematicipContactInterface(HomematicipGenericDevice, - BinarySensorDevice): +class HomematicipContactInterface(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud contact interface.""" @property @@ -93,7 +118,7 @@ class HomematicipContactInterface(HomematicipGenericDevice, @property def is_on(self) -> bool: """Return true if the contact interface is on/open.""" - if hasattr(self._device, 'sabotage') and self._device.sabotage: + if hasattr(self._device, "sabotage") and self._device.sabotage: return True if self._device.windowState is None: return None @@ -111,7 +136,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self) -> bool: """Return true if the shutter contact is on/open.""" - if hasattr(self._device, 'sabotage') and self._device.sabotage: + if hasattr(self._device, "sabotage") and self._device.sabotage: return True if self._device.windowState is None: return None @@ -129,13 +154,12 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self) -> bool: """Return true if motion is detected.""" - if hasattr(self._device, 'sabotage') and self._device.sabotage: + if hasattr(self._device, "sabotage") and self._device.sabotage: return True return self._device.motionDetected -class HomematicipPresenceDetector(HomematicipGenericDevice, - BinarySensorDevice): +class HomematicipPresenceDetector(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud presence detector.""" @property @@ -146,7 +170,7 @@ class HomematicipPresenceDetector(HomematicipGenericDevice, @property def is_on(self) -> bool: """Return true if presence is detected.""" - if hasattr(self._device, 'sabotage') and self._device.sabotage: + if hasattr(self._device, "sabotage") and self._device.sabotage: return True return self._device.presenceDetected @@ -162,8 +186,7 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self) -> bool: """Return true if smoke is detected.""" - return (self._device.smokeDetectorAlarmType - != SmokeDetectorAlarmType.IDLE_OFF) + return self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): @@ -190,7 +213,7 @@ class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice): @property def icon(self) -> str: """Return the icon.""" - return 'mdi:weather-windy' if self.is_on else 'mdi:pinwheel-outline' + return "mdi:weather-windy" if self.is_on else "mdi:pinwheel-outline" @property def is_on(self) -> bool: @@ -221,7 +244,7 @@ class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): def __init__(self, home: AsyncHome, device) -> None: """Initialize sunshine sensor.""" - super().__init__(home, device, 'Sunshine') + super().__init__(home, device, "Sunshine") @property def device_class(self) -> str: @@ -237,10 +260,11 @@ class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the illuminance sensor.""" attr = super().device_state_attributes - if hasattr(self._device, 'todaySunshineDuration') and \ - self._device.todaySunshineDuration: - attr[ATTR_TODAY_SUNSHINE_DURATION] = \ - self._device.todaySunshineDuration + if ( + hasattr(self._device, "todaySunshineDuration") + and self._device.todaySunshineDuration + ): + attr[ATTR_TODAY_SUNSHINE_DURATION] = self._device.todaySunshineDuration return attr @@ -249,7 +273,7 @@ class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): def __init__(self, home: AsyncHome, device) -> None: """Initialize battery sensor.""" - super().__init__(home, device, 'Battery') + super().__init__(home, device, "Battery") @property def device_class(self) -> str: @@ -262,14 +286,12 @@ class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): return self._device.lowBat -class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, - BinarySensorDevice): +class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud security zone group.""" - def __init__(self, home: AsyncHome, device, - post: str = 'SecurityZone') -> None: + def __init__(self, home: AsyncHome, device, post: str = "SecurityZone") -> None: """Initialize security zone group.""" - device.modelType = 'HmIP-{}'.format(post) + device.modelType = "HmIP-{}".format(post) super().__init__(home, device, post) @property @@ -294,8 +316,10 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, if self._device.presenceDetected: attr[ATTR_PRESENCEDETECTED] = True - if self._device.windowState is not None and \ - self._device.windowState != WindowState.CLOSED: + if ( + self._device.windowState is not None + and self._device.windowState != WindowState.CLOSED + ): attr[ATTR_WINDOWSTATE] = str(self._device.windowState) if self._device.unreach: attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True @@ -304,25 +328,30 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, @property def is_on(self) -> bool: """Return true if security issue detected.""" - if self._device.motionDetected or \ - self._device.presenceDetected or \ - self._device.unreach or \ - self._device.sabotage: + if ( + self._device.motionDetected + or self._device.presenceDetected + or self._device.unreach + or self._device.sabotage + ): return True - if self._device.windowState is not None and \ - self._device.windowState != WindowState.CLOSED: + if ( + self._device.windowState is not None + and self._device.windowState != WindowState.CLOSED + ): return True return False -class HomematicipSecuritySensorGroup(HomematicipSecurityZoneSensorGroup, - BinarySensorDevice): +class HomematicipSecuritySensorGroup( + HomematicipSecurityZoneSensorGroup, BinarySensorDevice +): """Representation of a HomematicIP security group.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize security group.""" - super().__init__(home, device, 'Sensors') + super().__init__(home, device, "Sensors") @property def device_state_attributes(self): @@ -337,11 +366,11 @@ class HomematicipSecuritySensorGroup(HomematicipSecurityZoneSensorGroup, attr[ATTR_WATERLEVELDETECTED] = True if self._device.lowBat: attr[ATTR_LOW_BATTERY] = True - if self._device.smokeDetectorAlarmType is not None and \ - self._device.smokeDetectorAlarmType != \ - SmokeDetectorAlarmType.IDLE_OFF: - attr[ATTR_SMOKEDETECTORALARM] = \ - str(self._device.smokeDetectorAlarmType) + if ( + self._device.smokeDetectorAlarmType is not None + and self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF + ): + attr[ATTR_SMOKEDETECTORALARM] = str(self._device.smokeDetectorAlarmType) return attr @@ -349,14 +378,17 @@ class HomematicipSecuritySensorGroup(HomematicipSecurityZoneSensorGroup, def is_on(self) -> bool: """Return true if safety issue detected.""" parent_is_on = super().is_on - if parent_is_on or \ - self._device.powerMainsFailure or \ - self._device.moistureDetected or \ - self._device.waterlevelDetected or \ - self._device.lowBat: + if ( + parent_is_on + or self._device.powerMainsFailure + or self._device.moistureDetected + or self._device.waterlevelDetected + or self._device.lowBat + ): return True - if self._device.smokeDetectorAlarmType is not None and \ - self._device.smokeDetectorAlarmType != \ - SmokeDetectorAlarmType.IDLE_OFF: + if ( + self._device.smokeDetectorAlarmType is not None + and self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF + ): return True return False diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 56cab03396e..53e7403ce56 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -2,15 +2,20 @@ import logging from typing import Awaitable -from homematicip.aio.device import ( - AsyncHeatingThermostat, AsyncHeatingThermostatCompact) +from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup from homematicip.aio.home import AsyncHome from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, HVAC_MODE_HEAT, PRESET_BOOST, PRESET_ECO, - SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE) + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + PRESET_BOOST, + PRESET_ECO, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + PRESET_NONE, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant @@ -19,19 +24,19 @@ from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice _LOGGER = logging.getLogger(__name__) -HMIP_AUTOMATIC_CM = 'AUTOMATIC' -HMIP_MANUAL_CM = 'MANUAL' -HMIP_ECO_CM = 'ECO' +HMIP_AUTOMATIC_CM = "AUTOMATIC" +HMIP_MANUAL_CM = "MANUAL" +HMIP_ECO_CM = "ECO" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud climate devices.""" pass -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: """Set up the HomematicIP climate from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -48,7 +53,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): def __init__(self, home: AsyncHome, device) -> None: """Initialize heating group.""" - device.modelType = 'Group-Heating' + device.modelType = "Group-Heating" self._simple_heating = None if device.actualTemperature is None: self._simple_heating = _get_first_heating_thermostat(device) @@ -121,7 +126,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): Requires SUPPORT_PRESET_MODE. """ - return [PRESET_BOOST] + return [PRESET_NONE, PRESET_BOOST] @property def min_temp(self) -> float: @@ -158,7 +163,6 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): """Return the first HeatingThermostat from a HeatingGroup.""" for device in heating_group.devices: - if isinstance(device, (AsyncHeatingThermostat, - AsyncHeatingThermostatCompact)): + if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): return device return None diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index 696425df5b5..a94ea7b53f1 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -7,16 +7,23 @@ from homeassistant import config_entries from homeassistant.core import HomeAssistant, callback from .const import ( - _LOGGER, DOMAIN as HMIPC_DOMAIN, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, - HMIPC_PIN) + _LOGGER, + DOMAIN as HMIPC_DOMAIN, + HMIPC_AUTHTOKEN, + HMIPC_HAPID, + HMIPC_NAME, + HMIPC_PIN, +) from .hap import HomematicipAuth @callback def configured_haps(hass: HomeAssistant) -> Set[str]: """Return a set of the configured access points.""" - return set(entry.data[HMIPC_HAPID] for entry - in hass.config_entries.async_entries(HMIPC_DOMAIN)) + return set( + entry.data[HMIPC_HAPID] + for entry in hass.config_entries.async_entries(HMIPC_DOMAIN) + ) @config_entries.HANDLERS.register(HMIPC_DOMAIN) @@ -39,10 +46,9 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): errors = {} if user_input is not None: - user_input[HMIPC_HAPID] = \ - user_input[HMIPC_HAPID].replace('-', '').upper() + user_input[HMIPC_HAPID] = user_input[HMIPC_HAPID].replace("-", "").upper() if user_input[HMIPC_HAPID] in configured_haps(self.hass): - return self.async_abort(reason='already_configured') + return self.async_abort(reason="already_configured") self.auth = HomematicipAuth(self.hass, user_input) connected = await self.auth.async_setup() @@ -51,13 +57,15 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): return await self.async_step_link() return self.async_show_form( - step_id='init', - data_schema=vol.Schema({ - vol.Required(HMIPC_HAPID): str, - vol.Optional(HMIPC_NAME): str, - vol.Optional(HMIPC_PIN): str, - }), - errors=errors + step_id="init", + data_schema=vol.Schema( + { + vol.Required(HMIPC_HAPID): str, + vol.Optional(HMIPC_NAME): str, + vol.Optional(HMIPC_PIN): str, + } + ), + errors=errors, ) async def async_step_link(self, user_input=None): @@ -74,12 +82,13 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): data={ HMIPC_HAPID: self.auth.config.get(HMIPC_HAPID), HMIPC_AUTHTOKEN: authtoken, - HMIPC_NAME: self.auth.config.get(HMIPC_NAME) - }) - return self.async_abort(reason='connection_aborted') - errors['base'] = 'press_the_button' + HMIPC_NAME: self.auth.config.get(HMIPC_NAME), + }, + ) + return self.async_abort(reason="connection_aborted") + errors["base"] = "press_the_button" - return self.async_show_form(step_id='link', errors=errors) + return self.async_show_form(step_id="link", errors=errors) async def async_step_import(self, import_info): """Import a new access point as a config entry.""" @@ -87,17 +96,13 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): authtoken = import_info[HMIPC_AUTHTOKEN] name = import_info[HMIPC_NAME] - hapid = hapid.replace('-', '').upper() + hapid = hapid.replace("-", "").upper() if hapid in configured_haps(self.hass): - return self.async_abort(reason='already_configured') + return self.async_abort(reason="already_configured") _LOGGER.info("Imported authentication for %s", hapid) return self.async_create_entry( title=hapid, - data={ - HMIPC_AUTHTOKEN: authtoken, - HMIPC_HAPID: hapid, - HMIPC_NAME: name, - } + data={HMIPC_AUTHTOKEN: authtoken, HMIPC_HAPID: hapid, HMIPC_NAME: name}, ) diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index c9a5df601e4..5c48de975f9 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -1,25 +1,25 @@ """Constants for the HomematicIP Cloud component.""" import logging -_LOGGER = logging.getLogger('.') +_LOGGER = logging.getLogger(".") -DOMAIN = 'homematicip_cloud' +DOMAIN = "homematicip_cloud" COMPONENTS = [ - 'alarm_control_panel', - 'binary_sensor', - 'climate', - 'cover', - 'light', - 'sensor', - 'switch', - 'weather', + "alarm_control_panel", + "binary_sensor", + "climate", + "cover", + "light", + "sensor", + "switch", + "weather", ] -CONF_ACCESSPOINT = 'accesspoint' -CONF_AUTHTOKEN = 'authtoken' +CONF_ACCESSPOINT = "accesspoint" +CONF_AUTHTOKEN = "authtoken" -HMIPC_NAME = 'name' -HMIPC_HAPID = 'hapid' -HMIPC_AUTHTOKEN = 'authtoken' -HMIPC_PIN = 'pin' +HMIPC_NAME = "name" +HMIPC_HAPID = "hapid" +HMIPC_AUTHTOKEN = "authtoken" +HMIPC_PIN = "pin" diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index ca102b626c7..9252c4322d9 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -5,7 +5,10 @@ from typing import Optional from homematicip.aio.device import AsyncFullFlushBlind, AsyncFullFlushShutter from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_TILT_POSITION, CoverDevice) + ATTR_POSITION, + ATTR_TILT_POSITION, + CoverDevice, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -19,14 +22,14 @@ HMIP_SLATS_OPEN = 0 HMIP_SLATS_CLOSED = 1 -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud cover devices.""" pass -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: """Set up the HomematicIP cover from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 3cd84791c67..021c264f63f 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -10,20 +10,19 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_MODEL_TYPE = 'model_type' +ATTR_MODEL_TYPE = "model_type" # RSSI HAP -> Device -ATTR_RSSI_DEVICE = 'rssi_device' +ATTR_RSSI_DEVICE = "rssi_device" # RSSI Device -> HAP -ATTR_RSSI_PEER = 'rssi_peer' -ATTR_SABOTAGE = 'sabotage' -ATTR_GROUP_MEMBER_UNREACHABLE = 'group_member_unreachable' +ATTR_RSSI_PEER = "rssi_peer" +ATTR_SABOTAGE = "sabotage" +ATTR_GROUP_MEMBER_UNREACHABLE = "group_member_unreachable" class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" - def __init__(self, home: AsyncHome, device, - post: Optional[str] = None) -> None: + def __init__(self, home: AsyncHome, device, post: Optional[str] = None) -> None: """Initialize the generic device.""" self._home = home self._device = device @@ -36,16 +35,15 @@ class HomematicipGenericDevice(Entity): # Only physical devices should be HA devices. if isinstance(self._device, AsyncDevice): return { - 'identifiers': { + "identifiers": { # Serial numbers of Homematic IP device (homematicip_cloud.DOMAIN, self._device.id) }, - 'name': self._device.label, - 'manufacturer': self._device.oem, - 'model': self._device.modelType, - 'sw_version': self._device.firmwareVersion, - 'via_device': ( - homematicip_cloud.DOMAIN, self._device.homeId), + "name": self._device.label, + "manufacturer": self._device.oem, + "model": self._device.modelType, + "sw_version": self._device.firmwareVersion, + "via_device": (homematicip_cloud.DOMAIN, self._device.homeId), } return None @@ -62,9 +60,9 @@ class HomematicipGenericDevice(Entity): def name(self) -> str: """Return the name of the generic device.""" name = self._device.label - if self._home.name is not None and self._home.name != '': + if self._home.name is not None and self._home.name != "": name = "{} {}".format(self._home.name, name) - if self.post is not None and self.post != '': + if self.post is not None and self.post != "": name = "{} {}".format(name, self.post) return name @@ -86,22 +84,20 @@ class HomematicipGenericDevice(Entity): @property def icon(self) -> Optional[str]: """Return the icon.""" - if hasattr(self._device, 'lowBat') and self._device.lowBat: - return 'mdi:battery-outline' - if hasattr(self._device, 'sabotage') and self._device.sabotage: - return 'mdi:alert' + if hasattr(self._device, "lowBat") and self._device.lowBat: + return "mdi:battery-outline" + if hasattr(self._device, "sabotage") and self._device.sabotage: + return "mdi:alert" return None @property def device_state_attributes(self): """Return the state attributes of the generic device.""" attr = {ATTR_MODEL_TYPE: self._device.modelType} - if hasattr(self._device, 'sabotage') and self._device.sabotage: + if hasattr(self._device, "sabotage") and self._device.sabotage: attr[ATTR_SABOTAGE] = self._device.sabotage - if hasattr(self._device, 'rssiDeviceValue') and \ - self._device.rssiDeviceValue: + if hasattr(self._device, "rssiDeviceValue") and self._device.rssiDeviceValue: attr[ATTR_RSSI_DEVICE] = self._device.rssiDeviceValue - if hasattr(self._device, 'rssiPeerValue') and \ - self._device.rssiPeerValue: + if hasattr(self._device, "rssiPeerValue") and self._device.rssiPeerValue: attr[ATTR_RSSI_PEER] = self._device.rssiPeerValue return attr diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 8bbbb8f41b6..7418aa94d89 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -11,8 +11,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ( - COMPONENTS, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN) +from .const import COMPONENTS, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN from .errors import HmipcConnectionError _LOGGER = logging.getLogger(__name__) @@ -31,9 +30,7 @@ class HomematicipAuth: """Connect to HomematicIP for registration.""" try: self.auth = await self.get_auth( - self.hass, - self.config.get(HMIPC_HAPID), - self.config.get(HMIPC_PIN) + self.hass, self.config.get(HMIPC_HAPID), self.config.get(HMIPC_PIN) ) return True except HmipcConnectionError: @@ -62,7 +59,7 @@ class HomematicipAuth: await auth.init(hapid) if pin: auth.pin = pin - await auth.connectionRequest('HomeAssistant') + await auth.connectionRequest("HomeAssistant") except HmipConnectionError: return False return auth @@ -89,18 +86,21 @@ class HomematicipHAP: self.hass, self.config_entry.data.get(HMIPC_HAPID), self.config_entry.data.get(HMIPC_AUTHTOKEN), - self.config_entry.data.get(HMIPC_NAME) + self.config_entry.data.get(HMIPC_NAME), ) except HmipcConnectionError: raise ConfigEntryNotReady - _LOGGER.info("Connected to HomematicIP with HAP %s", - self.config_entry.data.get(HMIPC_HAPID)) + _LOGGER.info( + "Connected to HomematicIP with HAP %s", + self.config_entry.data.get(HMIPC_HAPID), + ) for component in COMPONENTS: self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( - self.config_entry, component) + self.config_entry, component + ) ) return True @@ -116,8 +116,7 @@ class HomematicipHAP: are set to unavailable. """ if not self.home.connected: - _LOGGER.error( - "HMIP access point has lost connection with the cloud") + _LOGGER.error("HMIP access point has lost connection with the cloud") self._accesspoint_connected = False self.set_all_to_unavailable() elif not self._accesspoint_connected: @@ -141,8 +140,7 @@ class HomematicipHAP: except HmipConnectionError: # Somehow connection could not recover. Will disconnect and # so reconnect loop is taking over. - _LOGGER.error( - "Updating state after HMIP access point reconnect failed") + _LOGGER.error("Updating state after HMIP access point reconnect failed") self.hass.async_create_task(self.home.disable_events()) def set_all_to_unavailable(self): @@ -168,10 +166,12 @@ class HomematicipHAP: tries = 0 await hmip_events except HmipConnectionError: - _LOGGER.error("Error connecting to HomematicIP with HAP %s. " - "Retrying in %d seconds", - self.config_entry.data.get(HMIPC_HAPID), - retry_delay) + _LOGGER.error( + "Error connecting to HomematicIP with HAP %s. " + "Retrying in %d seconds", + self.config_entry.data.get(HMIPC_HAPID), + retry_delay, + ) if self._ws_close_requested: break @@ -179,8 +179,9 @@ class HomematicipHAP: tries += 1 try: - self._retry_task = self.hass.async_create_task(asyncio.sleep( - retry_delay)) + self._retry_task = self.hass.async_create_task( + asyncio.sleep(retry_delay) + ) await self._retry_task except asyncio.CancelledError: break @@ -194,17 +195,19 @@ class HomematicipHAP: _LOGGER.info("Closed connection to HomematicIP cloud server") for component in COMPONENTS: await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, component) + self.config_entry, component + ) return True - async def get_hap(self, hass: HomeAssistant, hapid: str, authtoken: str, - name: str) -> AsyncHome: + async def get_hap( + self, hass: HomeAssistant, hapid: str, authtoken: str, name: str + ) -> AsyncHome: """Create a HomematicIP access point object.""" home = AsyncHome(hass.loop, async_get_clientsession(hass)) home.name = name - home.label = 'Access Point' - home.modelType = 'HmIP-HAP' + home.label = "Access Point" + home.modelType = "HmIP-HAP" home.set_auth_token(authtoken) try: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 7cfbae95a33..c034b19bb3a 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -2,16 +2,25 @@ import logging from homematicip.aio.device import ( - AsyncBrandDimmer, AsyncBrandSwitchMeasuring, - AsyncBrandSwitchNotificationLight, AsyncDimmer, AsyncFullFlushDimmer, - AsyncPluggableDimmer) + AsyncBrandDimmer, + AsyncBrandSwitchMeasuring, + AsyncBrandSwitchNotificationLight, + AsyncDimmer, + AsyncFullFlushDimmer, + AsyncPluggableDimmer, +) from homematicip.aio.home import AsyncHome from homematicip.base.enums import RGBColorState from homematicip.base.functionalChannels import NotificationLightChannel from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, Light) + ATTR_BRIGHTNESS, + ATTR_COLOR_NAME, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + Light, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -19,18 +28,18 @@ from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice _LOGGER = logging.getLogger(__name__) -ATTR_ENERGY_COUNTER = 'energy_counter_kwh' -ATTR_POWER_CONSUMPTION = 'power_consumption' +ATTR_ENERGY_COUNTER = "energy_counter_kwh" +ATTR_POWER_CONSUMPTION = "power_consumption" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up HomematicIP Cloud lights.""" pass -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -39,13 +48,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, devices.append(HomematicipLightMeasuring(home, device)) elif isinstance(device, AsyncBrandSwitchNotificationLight): devices.append(HomematicipLight(home, device)) - devices.append(HomematicipNotificationLight( - home, device, device.topLightChannelIndex)) - devices.append(HomematicipNotificationLight( - home, device, device.bottomLightChannelIndex)) - elif isinstance(device, - (AsyncDimmer, AsyncPluggableDimmer, - AsyncBrandDimmer, AsyncFullFlushDimmer)): + devices.append( + HomematicipNotificationLight(home, device, device.topLightChannelIndex) + ) + devices.append( + HomematicipNotificationLight( + home, device, device.bottomLightChannelIndex + ) + ) + elif isinstance( + device, + (AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer), + ): devices.append(HomematicipDimmer(home, device)) if devices: @@ -81,8 +95,9 @@ class HomematicipLightMeasuring(HomematicipLight): """Return the state attributes of the generic device.""" attr = super().device_state_attributes if self._device.currentPowerConsumption > 0.05: - attr[ATTR_POWER_CONSUMPTION] = \ - round(self._device.currentPowerConsumption, 2) + attr[ATTR_POWER_CONSUMPTION] = round( + self._device.currentPowerConsumption, 2 + ) attr[ATTR_ENERGY_COUNTER] = round(self._device.energyCounter, 2) return attr @@ -97,14 +112,13 @@ class HomematicipDimmer(HomematicipGenericDevice, Light): @property def is_on(self) -> bool: """Return true if device is on.""" - return self._device.dimLevel is not None and \ - self._device.dimLevel > 0.0 + return self._device.dimLevel is not None and self._device.dimLevel > 0.0 @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" if self._device.dimLevel: - return int(self._device.dimLevel*255) + return int(self._device.dimLevel * 255) return 0 @property @@ -115,7 +129,7 @@ class HomematicipDimmer(HomematicipGenericDevice, Light): async def async_turn_on(self, **kwargs): """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: - await self._device.set_dim_level(kwargs[ATTR_BRIGHTNESS]/255.0) + await self._device.set_dim_level(kwargs[ATTR_BRIGHTNESS] / 255.0) else: await self._device.set_dim_level(1) @@ -131,9 +145,9 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): """Initialize the dimmer light device.""" self.channel = channel if self.channel == 2: - super().__init__(home, device, 'Top') + super().__init__(home, device, "Top") else: - super().__init__(home, device, 'Bottom') + super().__init__(home, device, "Bottom") self._color_switcher = { RGBColorState.WHITE: [0.0, 0.0], @@ -142,7 +156,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): RGBColorState.GREEN: [120.0, 100.0], RGBColorState.TURQUOISE: [180.0, 100.0], RGBColorState.BLUE: [240.0, 100.0], - RGBColorState.PURPLE: [300.0, 100.0] + RGBColorState.PURPLE: [300.0, 100.0], } @property @@ -152,8 +166,10 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): @property def is_on(self) -> bool: """Return true if device is on.""" - return self._func_channel.dimLevel is not None and \ - self._func_channel.dimLevel > 0.0 + return ( + self._func_channel.dimLevel is not None + and self._func_channel.dimLevel > 0.0 + ) @property def brightness(self) -> int: @@ -179,7 +195,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): @property def name(self) -> str: """Return the name of the generic device.""" - return "{} {}".format(super().name, 'Notification') + return "{} {}".format(super().name, "Notification") @property def supported_features(self) -> int: @@ -189,9 +205,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}_{}_{}".format(self.__class__.__name__, - self.post, - self._device.id) + return "{}_{}_{}".format(self.__class__.__name__, self.post, self._device.id) async def async_turn_on(self, **kwargs): """Turn the light on.""" @@ -212,17 +226,12 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): brightness = max(10, brightness) dim_level = brightness / 255.0 - await self._device.set_rgb_dim_level( - self.channel, - simple_rgb_color, - dim_level) + await self._device.set_rgb_dim_level(self.channel, simple_rgb_color, dim_level) async def async_turn_off(self, **kwargs): """Turn the light off.""" simple_rgb_color = self._func_channel.simpleRGBColorState - await self._device.set_rgb_dim_level( - self.channel, - simple_rgb_color, 0.0) + await self._device.set_rgb_dim_level(self.channel, simple_rgb_color, 0.0) def _convert_color(color) -> RGBColorState: diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b679130ce05..ee0d2cb1271 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/homematicip_cloud", "requirements": [ - "homematicip==0.10.9" + "homematicip==0.10.10" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index b3e23bde2be..add03c6b644 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -2,74 +2,102 @@ import logging from homematicip.aio.device import ( - AsyncBrandSwitchMeasuring, AsyncFullFlushSwitchMeasuring, - AsyncHeatingThermostat, AsyncHeatingThermostatCompact, AsyncLightSensor, - AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, - AsyncMotionDetectorPushButton, AsyncPlugableSwitchMeasuring, - AsyncPresenceDetectorIndoor, AsyncTemperatureHumiditySensorDisplay, + AsyncBrandSwitchMeasuring, + AsyncFullFlushSwitchMeasuring, + AsyncHeatingThermostat, + AsyncHeatingThermostatCompact, + AsyncLightSensor, + AsyncMotionDetectorIndoor, + AsyncMotionDetectorOutdoor, + AsyncMotionDetectorPushButton, + AsyncPlugableSwitchMeasuring, + AsyncPresenceDetectorIndoor, + AsyncTemperatureHumiditySensorDisplay, AsyncTemperatureHumiditySensorOutdoor, - AsyncTemperatureHumiditySensorWithoutDisplay, AsyncWeatherSensor, - AsyncWeatherSensorPlus, AsyncWeatherSensorPro) + AsyncTemperatureHumiditySensorWithoutDisplay, + AsyncWeatherSensor, + AsyncWeatherSensorPlus, + AsyncWeatherSensorPro, +) from homematicip.aio.home import AsyncHome from homematicip.base.enums import ValveState from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, POWER_WATT, TEMP_CELSIUS) + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + POWER_WATT, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice _LOGGER = logging.getLogger(__name__) -ATTR_TEMPERATURE_OFFSET = 'temperature_offset' -ATTR_WIND_DIRECTION = 'wind_direction' -ATTR_WIND_DIRECTION_VARIATION = 'wind_direction_variation_in_degree' +ATTR_TEMPERATURE_OFFSET = "temperature_offset" +ATTR_WIND_DIRECTION = "wind_direction" +ATTR_WIND_DIRECTION_VARIATION = "wind_direction_variation_in_degree" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud sensors devices.""" pass -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [HomematicipAccesspointStatus(home)] for device in home.devices: - if isinstance(device, (AsyncHeatingThermostat, - AsyncHeatingThermostatCompact)): + if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): devices.append(HomematicipHeatingThermostat(home, device)) devices.append(HomematicipTemperatureSensor(home, device)) - if isinstance(device, (AsyncTemperatureHumiditySensorDisplay, - AsyncTemperatureHumiditySensorWithoutDisplay, - AsyncTemperatureHumiditySensorOutdoor, - AsyncWeatherSensor, - AsyncWeatherSensorPlus, - AsyncWeatherSensorPro)): + if isinstance( + device, + ( + AsyncTemperatureHumiditySensorDisplay, + AsyncTemperatureHumiditySensorWithoutDisplay, + AsyncTemperatureHumiditySensorOutdoor, + AsyncWeatherSensor, + AsyncWeatherSensorPlus, + AsyncWeatherSensorPro, + ), + ): devices.append(HomematicipTemperatureSensor(home, device)) devices.append(HomematicipHumiditySensor(home, device)) - if isinstance(device, (AsyncLightSensor, AsyncMotionDetectorIndoor, - AsyncMotionDetectorOutdoor, - AsyncMotionDetectorPushButton, - AsyncPresenceDetectorIndoor, - AsyncWeatherSensor, - AsyncWeatherSensorPlus, - AsyncWeatherSensorPro)): + if isinstance( + device, + ( + AsyncLightSensor, + AsyncMotionDetectorIndoor, + AsyncMotionDetectorOutdoor, + AsyncMotionDetectorPushButton, + AsyncPresenceDetectorIndoor, + AsyncWeatherSensor, + AsyncWeatherSensorPlus, + AsyncWeatherSensorPro, + ), + ): devices.append(HomematicipIlluminanceSensor(home, device)) - if isinstance(device, (AsyncPlugableSwitchMeasuring, - AsyncBrandSwitchMeasuring, - AsyncFullFlushSwitchMeasuring)): + if isinstance( + device, + ( + AsyncPlugableSwitchMeasuring, + AsyncBrandSwitchMeasuring, + AsyncFullFlushSwitchMeasuring, + ), + ): devices.append(HomematicipPowerSensor(home, device)) - if isinstance(device, (AsyncWeatherSensor, - AsyncWeatherSensorPlus, - AsyncWeatherSensorPro)): + if isinstance( + device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) + ): devices.append(HomematicipWindspeedSensor(home, device)) - if isinstance(device, (AsyncWeatherSensorPlus, - AsyncWeatherSensorPro)): + if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): devices.append(HomematicipTodayRainSensor(home, device)) if devices: @@ -88,7 +116,7 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): """Return device specific attributes.""" # Adds a sensor to the existing HAP device return { - 'identifiers': { + "identifiers": { # Serial numbers of Homematic IP device (HMIPC_DOMAIN, self._device.id) } @@ -97,7 +125,7 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): @property def icon(self) -> str: """Return the icon of the access point device.""" - return 'mdi:access-point-network' + return "mdi:access-point-network" @property def state(self) -> float: @@ -112,15 +140,15 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return '%' + return "%" class HomematicipHeatingThermostat(HomematicipGenericDevice): - """Represenation of a HomematicIP heating thermostat device.""" + """Representation of a HomematicIP heating thermostat device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize heating thermostat device.""" - super().__init__(home, device, 'Heating') + super().__init__(home, device, "Heating") @property def icon(self) -> str: @@ -128,28 +156,28 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): if super().icon: return super().icon if self._device.valveState != ValveState.ADAPTION_DONE: - return 'mdi:alert' - return 'mdi:radiator' + return "mdi:alert" + return "mdi:radiator" @property def state(self) -> int: """Return the state of the radiator valve.""" if self._device.valveState != ValveState.ADAPTION_DONE: return self._device.valveState - return round(self._device.valvePosition*100) + return round(self._device.valvePosition * 100) @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return '%' + return "%" class HomematicipHumiditySensor(HomematicipGenericDevice): - """Represenation of a HomematicIP Cloud humidity device.""" + """Representation of a HomematicIP Cloud humidity device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the thermometer device.""" - super().__init__(home, device, 'Humidity') + super().__init__(home, device, "Humidity") @property def device_class(self) -> str: @@ -164,7 +192,7 @@ class HomematicipHumiditySensor(HomematicipGenericDevice): @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return '%' + return "%" class HomematicipTemperatureSensor(HomematicipGenericDevice): @@ -172,7 +200,7 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): def __init__(self, home: AsyncHome, device) -> None: """Initialize the thermometer device.""" - super().__init__(home, device, 'Temperature') + super().__init__(home, device, "Temperature") @property def device_class(self) -> str: @@ -182,7 +210,7 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): @property def state(self) -> float: """Return the state.""" - if hasattr(self._device, 'valveActualTemperature'): + if hasattr(self._device, "valveActualTemperature"): return self._device.valveActualTemperature return self._device.actualTemperature @@ -196,18 +224,20 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): def device_state_attributes(self): """Return the state attributes of the windspeed sensor.""" attr = super().device_state_attributes - if hasattr(self._device, 'temperatureOffset') and \ - self._device.temperatureOffset: + if ( + hasattr(self._device, "temperatureOffset") + and self._device.temperatureOffset + ): attr[ATTR_TEMPERATURE_OFFSET] = self._device.temperatureOffset return attr class HomematicipIlluminanceSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP Illuminance device.""" + """Representation of a HomematicIP Illuminance device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" - super().__init__(home, device, 'Illuminance') + super().__init__(home, device, "Illuminance") @property def device_class(self) -> str: @@ -217,7 +247,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): @property def state(self) -> float: """Return the state.""" - if hasattr(self._device, 'averageIllumination'): + if hasattr(self._device, "averageIllumination"): return self._device.averageIllumination return self._device.illumination @@ -225,15 +255,15 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return 'lx' + return "lx" class HomematicipPowerSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP power measuring device.""" + """Representation of a HomematicIP power measuring device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" - super().__init__(home, device, 'Power') + super().__init__(home, device, "Power") @property def device_class(self) -> str: @@ -242,7 +272,7 @@ class HomematicipPowerSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Represenation of the HomematicIP power comsumption value.""" + """Representation of the HomematicIP power comsumption value.""" return self._device.currentPowerConsumption @property @@ -252,85 +282,84 @@ class HomematicipPowerSensor(HomematicipGenericDevice): class HomematicipWindspeedSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP wind speed sensor.""" + """Representation of a HomematicIP wind speed sensor.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" - super().__init__(home, device, 'Windspeed') + super().__init__(home, device, "Windspeed") @property def state(self) -> float: - """Represenation of the HomematicIP wind speed value.""" + """Representation of the HomematicIP wind speed value.""" return self._device.windSpeed @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return 'km/h' + return "km/h" @property def device_state_attributes(self): """Return the state attributes of the wind speed sensor.""" attr = super().device_state_attributes - if hasattr(self._device, 'windDirection') and \ - self._device.windDirection: - attr[ATTR_WIND_DIRECTION] = \ - _get_wind_direction(self._device.windDirection) - if hasattr(self._device, 'windDirectionVariation') and \ - self._device.windDirectionVariation: - attr[ATTR_WIND_DIRECTION_VARIATION] = \ - self._device.windDirectionVariation + if hasattr(self._device, "windDirection") and self._device.windDirection: + attr[ATTR_WIND_DIRECTION] = _get_wind_direction(self._device.windDirection) + if ( + hasattr(self._device, "windDirectionVariation") + and self._device.windDirectionVariation + ): + attr[ATTR_WIND_DIRECTION_VARIATION] = self._device.windDirectionVariation return attr class HomematicipTodayRainSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP rain counter of a day sensor.""" + """Representation of a HomematicIP rain counter of a day sensor.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" - super().__init__(home, device, 'Today Rain') + super().__init__(home, device, "Today Rain") @property def state(self) -> float: - """Represenation of the HomematicIP todays rain value.""" + """Representation of the HomematicIP todays rain value.""" return round(self._device.todayRainCounter, 2) @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return 'mm' + return "mm" def _get_wind_direction(wind_direction_degree: float) -> str: """Convert wind direction degree to named direction.""" if 11.25 <= wind_direction_degree < 33.75: - return 'NNE' + return "NNE" if 33.75 <= wind_direction_degree < 56.25: - return 'NE' + return "NE" if 56.25 <= wind_direction_degree < 78.75: - return 'ENE' + return "ENE" if 78.75 <= wind_direction_degree < 101.25: - return 'E' + return "E" if 101.25 <= wind_direction_degree < 123.75: - return 'ESE' + return "ESE" if 123.75 <= wind_direction_degree < 146.25: - return 'SE' + return "SE" if 146.25 <= wind_direction_degree < 168.75: - return 'SSE' + return "SSE" if 168.75 <= wind_direction_degree < 191.25: - return 'S' + return "S" if 191.25 <= wind_direction_degree < 213.75: - return 'SSW' + return "SSW" if 213.75 <= wind_direction_degree < 236.25: - return 'SW' + return "SW" if 236.25 <= wind_direction_degree < 258.75: - return 'WSW' + return "WSW" if 258.75 <= wind_direction_degree < 281.25: - return 'W' + return "W" if 281.25 <= wind_direction_degree < 303.75: - return 'WNW' + return "WNW" if 303.75 <= wind_direction_degree < 326.25: - return 'NW' + return "NW" if 326.25 <= wind_direction_degree < 348.75: - return 'NNW' - return 'N' + return "NNW" + return "N" diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 777fc0c35fd..a9535736d0f 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -2,10 +2,15 @@ import logging from homematicip.aio.device import ( - AsyncBrandSwitchMeasuring, AsyncFullFlushSwitchMeasuring, AsyncMultiIOBox, - AsyncOpenCollector8Module, AsyncPlugableSwitch, - AsyncPlugableSwitchMeasuring, AsyncPrintedCircuitBoardSwitch2, - AsyncPrintedCircuitBoardSwitchBattery) + AsyncBrandSwitchMeasuring, + AsyncFullFlushSwitchMeasuring, + AsyncMultiIOBox, + AsyncOpenCollector8Module, + AsyncPlugableSwitch, + AsyncPlugableSwitchMeasuring, + AsyncPrintedCircuitBoardSwitch2, + AsyncPrintedCircuitBoardSwitchBattery, +) from homematicip.aio.group import AsyncSwitchingGroup from homematicip.aio.home import AsyncHome @@ -19,14 +24,14 @@ from .device import ATTR_GROUP_MEMBER_UNREACHABLE _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud switch devices.""" pass -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: """Set up the HomematicIP switch from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -36,11 +41,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, # This device is implemented in the light platform and will # not be added in the switch platform pass - elif isinstance(device, (AsyncPlugableSwitchMeasuring, - AsyncFullFlushSwitchMeasuring)): + elif isinstance( + device, (AsyncPlugableSwitchMeasuring, AsyncFullFlushSwitchMeasuring) + ): devices.append(HomematicipSwitchMeasuring(home, device)) - elif isinstance(device, (AsyncPlugableSwitch, - AsyncPrintedCircuitBoardSwitchBattery)): + elif isinstance( + device, (AsyncPlugableSwitch, AsyncPrintedCircuitBoardSwitchBattery) + ): devices.append(HomematicipSwitch(home, device)) elif isinstance(device, AsyncOpenCollector8Module): for channel in range(1, 9): @@ -54,8 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, for group in home.groups: if isinstance(group, AsyncSwitchingGroup): - devices.append( - HomematicipGroupSwitch(home, group)) + devices.append(HomematicipGroupSwitch(home, group)) if devices: async_add_entities(devices) @@ -85,9 +91,9 @@ class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): """representation of a HomematicIP switching group.""" - def __init__(self, home: AsyncHome, device, post: str = 'Group') -> None: + def __init__(self, home: AsyncHome, device, post: str = "Group") -> None: """Initialize switching group.""" - device.modelType = 'HmIP-{}'.format(post) + device.modelType = "HmIP-{}".format(post) super().__init__(home, device, post) @property @@ -143,13 +149,12 @@ class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): def __init__(self, home: AsyncHome, device, channel: int): """Initialize the multi switch device.""" self.channel = channel - super().__init__(home, device, 'Channel{}'.format(channel)) + super().__init__(home, device, "Channel{}".format(channel)) @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}_{}_{}".format(self.__class__.__name__, - self.post, self._device.id) + return "{}_{}_{}".format(self.__class__.__name__, self.post, self._device.id) @property def is_on(self) -> bool: diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index b97948b2d9f..463e1bfb741 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -1,9 +1,11 @@ - """Support for HomematicIP Cloud weather devices.""" import logging from homematicip.aio.device import ( - AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) + AsyncWeatherSensor, + AsyncWeatherSensorPlus, + AsyncWeatherSensorPro, +) from homematicip.aio.home import AsyncHome from homeassistant.components.weather import WeatherEntity @@ -16,14 +18,14 @@ from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud weather sensor.""" pass -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: """Set up the HomematicIP weather sensor from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -78,12 +80,12 @@ class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): def condition(self) -> str: """Return the current condition.""" if hasattr(self._device, "raining") and self._device.raining: - return 'rainy' + return "rainy" if self._device.storm: - return 'windy' + return "windy" if self._device.sunshine: - return 'sunny' - return '' + return "sunny" + return "" class HomematicipWeatherSensorPro(HomematicipWeatherSensor): diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index b722a5a4a2d..dcc2ce5dde6 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -4,52 +4,63 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_HOST, CONF_ID, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP) + CONF_HOST, + CONF_ID, + CONF_NAME, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -DOMAIN = 'homeworks' +DOMAIN = "homeworks" -HOMEWORKS_CONTROLLER = 'homeworks' -ENTITY_SIGNAL = 'homeworks_entity_{}' -EVENT_BUTTON_PRESS = 'homeworks_button_press' -EVENT_BUTTON_RELEASE = 'homeworks_button_release' +HOMEWORKS_CONTROLLER = "homeworks" +ENTITY_SIGNAL = "homeworks_entity_{}" +EVENT_BUTTON_PRESS = "homeworks_button_press" +EVENT_BUTTON_RELEASE = "homeworks_button_release" -CONF_DIMMERS = 'dimmers' -CONF_KEYPADS = 'keypads' -CONF_ADDR = 'addr' -CONF_RATE = 'rate' +CONF_DIMMERS = "dimmers" +CONF_KEYPADS = "keypads" +CONF_ADDR = "addr" +CONF_RATE = "rate" -FADE_RATE = 1. +FADE_RATE = 1.0 CV_FADE_RATE = vol.All(vol.Coerce(float), vol.Range(min=0, max=20)) -DIMMER_SCHEMA = vol.Schema({ - vol.Required(CONF_ADDR): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_RATE, default=FADE_RATE): CV_FADE_RATE, -}) +DIMMER_SCHEMA = vol.Schema( + { + vol.Required(CONF_ADDR): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_RATE, default=FADE_RATE): CV_FADE_RATE, + } +) -KEYPAD_SCHEMA = vol.Schema({ - vol.Required(CONF_ADDR): cv.string, - vol.Required(CONF_NAME): cv.string, -}) +KEYPAD_SCHEMA = vol.Schema( + {vol.Required(CONF_ADDR): cv.string, vol.Required(CONF_NAME): cv.string} +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_DIMMERS): vol.All(cv.ensure_list, [DIMMER_SCHEMA]), - vol.Optional(CONF_KEYPADS, default=[]): - vol.All(cv.ensure_list, [KEYPAD_SCHEMA]), - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_DIMMERS): vol.All(cv.ensure_list, [DIMMER_SCHEMA]), + vol.Optional(CONF_KEYPADS, default=[]): vol.All( + cv.ensure_list, [KEYPAD_SCHEMA] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) def setup(hass, base_config): @@ -58,7 +69,7 @@ def setup(hass, base_config): def hw_callback(msg_type, values): """Dispatch state changes.""" - _LOGGER.debug('callback: %s, %s', msg_type, values) + _LOGGER.debug("callback: %s, %s", msg_type, values) addr = values[0] signal = ENTITY_SIGNAL.format(addr) dispatcher_send(hass, signal, msg_type, values) @@ -73,7 +84,7 @@ def setup(hass, base_config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) dimmers = config[CONF_DIMMERS] - load_platform(hass, 'light', DOMAIN, {CONF_DIMMERS: dimmers}, base_config) + load_platform(hass, "light", DOMAIN, {CONF_DIMMERS: dimmers}, base_config) for key_config in config[CONF_KEYPADS]: addr = key_config[CONF_ADDR] @@ -83,7 +94,7 @@ def setup(hass, base_config): return True -class HomeworksDevice(): +class HomeworksDevice: """Base class of a Homeworks device.""" def __init__(self, controller, addr, name): @@ -95,7 +106,7 @@ class HomeworksDevice(): @property def unique_id(self): """Return a unique identifier.""" - return 'homeworks.{}'.format(self._addr) + return "homeworks.{}".format(self._addr) @property def name(self): @@ -122,19 +133,18 @@ class HomeworksKeypadEvent: self._name = name self._id = slugify(self._name) signal = ENTITY_SIGNAL.format(self._addr) - async_dispatcher_connect( - self._hass, signal, self._update_callback) + async_dispatcher_connect(self._hass, signal, self._update_callback) @callback def _update_callback(self, msg_type, values): """Fire events if button is pressed or released.""" - from pyhomeworks.pyhomeworks import ( - HW_BUTTON_PRESSED, HW_BUTTON_RELEASED) + from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED + if msg_type == HW_BUTTON_PRESSED: event = EVENT_BUTTON_PRESS elif msg_type == HW_BUTTON_RELEASED: event = EVENT_BUTTON_RELEASE else: return - data = {CONF_ID: self._id, CONF_NAME: self._name, 'button': values[1]} + data = {CONF_ID: self._id, CONF_NAME: self._name, "button": values[1]} self._hass.bus.async_fire(event, data) diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index 710be7c0077..d1854b4dbf3 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -1,15 +1,19 @@ """Support for Lutron Homeworks lights.""" import logging -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) +from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light from homeassistant.const import CONF_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( - CONF_ADDR, CONF_DIMMERS, CONF_RATE, ENTITY_SIGNAL, HOMEWORKS_CONTROLLER, - HomeworksDevice) + CONF_ADDR, + CONF_DIMMERS, + CONF_RATE, + ENTITY_SIGNAL, + HOMEWORKS_CONTROLLER, + HomeworksDevice, +) _LOGGER = logging.getLogger(__name__) @@ -22,8 +26,9 @@ def setup_platform(hass, config, add_entities, discover_info=None): controller = hass.data[HOMEWORKS_CONTROLLER] devs = [] for dimmer in discover_info[CONF_DIMMERS]: - dev = HomeworksLight(controller, dimmer[CONF_ADDR], - dimmer[CONF_NAME], dimmer[CONF_RATE]) + dev = HomeworksLight( + controller, dimmer[CONF_ADDR], dimmer[CONF_NAME], dimmer[CONF_RATE] + ) devs.append(dev) add_entities(devs, True) @@ -41,9 +46,8 @@ class HomeworksLight(HomeworksDevice, Light): async def async_added_to_hass(self): """Call when entity is added to hass.""" signal = ENTITY_SIGNAL.format(self._addr) - _LOGGER.debug('connecting %s', signal) - async_dispatcher_connect( - self.hass, signal, self._update_callback) + _LOGGER.debug("connecting %s", signal) + async_dispatcher_connect(self.hass, signal, self._update_callback) self._controller.request_dimmer_level(self._addr) @property @@ -73,13 +77,13 @@ class HomeworksLight(HomeworksDevice, Light): def _set_brightness(self, level): """Send the brightness level to the device.""" self._controller.fade_dim( - float((level*100.)/255.), self._rate, - 0, self._addr) + float((level * 100.0) / 255.0), self._rate, 0, self._addr + ) @property def device_state_attributes(self): """Supported attributes.""" - return {'homeworks_address': self._addr} + return {"homeworks_address": self._addr} @property def is_on(self): @@ -92,7 +96,7 @@ class HomeworksLight(HomeworksDevice, Light): from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED if msg_type == HW_LIGHT_CHANGED: - self._level = int((values[1] * 255.)/100.) + self._level = int((values[1] * 255.0) / 100.0) if self._level != 0: self._prev_level = self._level self.async_schedule_update_ha_state() diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 78420c98dee..62a370f60fa 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -9,71 +9,93 @@ import somecomfort from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate.const import ( - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - FAN_AUTO, FAN_DIFFUSE, FAN_ON, - SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + FAN_AUTO, + FAN_DIFFUSE, + FAN_ON, + SUPPORT_AUX_HEAT, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, - HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_FAN, + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, PRESET_AWAY, + PRESET_NONE, ) from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_TEMPERATURE, CONF_REGION) + CONF_PASSWORD, + CONF_USERNAME, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + ATTR_TEMPERATURE, + CONF_REGION, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_FAN_ACTION = 'fan_action' +ATTR_FAN_ACTION = "fan_action" -CONF_COOL_AWAY_TEMPERATURE = 'away_cool_temperature' -CONF_HEAT_AWAY_TEMPERATURE = 'away_heat_temperature' +CONF_COOL_AWAY_TEMPERATURE = "away_cool_temperature" +CONF_HEAT_AWAY_TEMPERATURE = "away_heat_temperature" DEFAULT_COOL_AWAY_TEMPERATURE = 88 DEFAULT_HEAT_AWAY_TEMPERATURE = 61 -DEFAULT_REGION = 'eu' -REGIONS = ['eu', 'us'] +DEFAULT_REGION = "eu" +REGIONS = ["eu", "us"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_COOL_AWAY_TEMPERATURE, - default=DEFAULT_COOL_AWAY_TEMPERATURE): vol.Coerce(int), - vol.Optional(CONF_HEAT_AWAY_TEMPERATURE, - default=DEFAULT_HEAT_AWAY_TEMPERATURE): vol.Coerce(int), - vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(REGIONS), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional( + CONF_COOL_AWAY_TEMPERATURE, default=DEFAULT_COOL_AWAY_TEMPERATURE + ): vol.Coerce(int), + vol.Optional( + CONF_HEAT_AWAY_TEMPERATURE, default=DEFAULT_HEAT_AWAY_TEMPERATURE + ): vol.Coerce(int), + vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(REGIONS), + } +) HVAC_MODE_TO_HW_MODE = { - 'SwitchOffAllowed': {HVAC_MODE_OFF: 'off'}, - 'SwitchAutoAllowed': {HVAC_MODE_HEAT_COOL: 'auto'}, - 'SwitchCoolAllowed': {HVAC_MODE_COOL: 'cool'}, - 'SwitchHeatAllowed': {HVAC_MODE_HEAT: 'heat'}, + "SwitchOffAllowed": {HVAC_MODE_OFF: "off"}, + "SwitchAutoAllowed": {HVAC_MODE_HEAT_COOL: "auto"}, + "SwitchCoolAllowed": {HVAC_MODE_COOL: "cool"}, + "SwitchHeatAllowed": {HVAC_MODE_HEAT: "heat"}, } HW_MODE_TO_HVAC_MODE = { - 'off': HVAC_MODE_OFF, - 'emheat': HVAC_MODE_HEAT, - 'heat': HVAC_MODE_HEAT, - 'cool': HVAC_MODE_COOL, - 'auto': HVAC_MODE_HEAT_COOL, + "off": HVAC_MODE_OFF, + "emheat": HVAC_MODE_HEAT, + "heat": HVAC_MODE_HEAT, + "cool": HVAC_MODE_COOL, + "auto": HVAC_MODE_HEAT_COOL, } HW_MODE_TO_HA_HVAC_ACTION = { - 'off': CURRENT_HVAC_OFF, - 'fan': CURRENT_HVAC_IDLE, - 'heat': CURRENT_HVAC_HEAT, - 'cool': CURRENT_HVAC_COOL, + "off": CURRENT_HVAC_IDLE, + "fan": CURRENT_HVAC_FAN, + "heat": CURRENT_HVAC_HEAT, + "cool": CURRENT_HVAC_COOL, } FAN_MODE_TO_HW = { - 'fanModeOnAllowed': {FAN_ON: 'on'}, - 'fanModeAutoAllowed': {FAN_AUTO: 'auto'}, - 'fanModeCirculateAllowed': {FAN_DIFFUSE: 'circulate'}, + "fanModeOnAllowed": {FAN_ON: "on"}, + "fanModeAutoAllowed": {FAN_AUTO: "auto"}, + "fanModeCirculateAllowed": {FAN_DIFFUSE: "circulate"}, } HW_FAN_MODE_TO_HA = { - 'on': FAN_ON, - 'auto': FAN_AUTO, - 'circulate': FAN_DIFFUSE, - 'follow schedule': FAN_AUTO, + "on": FAN_ON, + "auto": FAN_AUTO, + "circulate": FAN_DIFFUSE, + "follow schedule": FAN_AUTO, } @@ -82,42 +104,53 @@ def setup_platform(hass, config, add_entities, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - if config.get(CONF_REGION) == 'us': + if config.get(CONF_REGION) == "us": try: client = somecomfort.SomeComfort(username, password) except somecomfort.AuthError: _LOGGER.error("Failed to login to honeywell account %s", username) return except somecomfort.SomeComfortError: - _LOGGER.error("Failed to initialize the Honeywell client: " - "Check your configuration (username, password), " - "or maybe you have exceeded the API rate limit?") + _LOGGER.error( + "Failed to initialize the Honeywell client: " + "Check your configuration (username, password), " + "or maybe you have exceeded the API rate limit?" + ) return - dev_id = config.get('thermostat') - loc_id = config.get('location') + dev_id = config.get("thermostat") + loc_id = config.get("location") cool_away_temp = config.get(CONF_COOL_AWAY_TEMPERATURE) heat_away_temp = config.get(CONF_HEAT_AWAY_TEMPERATURE) - add_entities([HoneywellUSThermostat(client, device, cool_away_temp, - heat_away_temp, username, password) - for location in client.locations_by_id.values() - for device in location.devices_by_id.values() - if ((not loc_id or location.locationid == loc_id) and - (not dev_id or device.deviceid == dev_id))]) + add_entities( + [ + HoneywellUSThermostat( + client, device, cool_away_temp, heat_away_temp, username, password + ) + for location in client.locations_by_id.values() + for device in location.devices_by_id.values() + if ( + (not loc_id or location.locationid == loc_id) + and (not dev_id or device.deviceid == dev_id) + ) + ] + ) return _LOGGER.warning( "The honeywell component has been deprecated for EU (i.e. non-US) " "systems. For EU-based systems, use the evohome component, " - "see: https://home-assistant.io/components/evohome") + "see: https://home-assistant.io/components/evohome" + ) class HoneywellUSThermostat(ClimateDevice): """Representation of a Honeywell US Thermostat.""" - def __init__(self, client, device, cool_away_temp, - heat_away_temp, username, password): + def __init__( + self, client, device, cool_away_temp, heat_away_temp, username, password + ): """Initialize the thermostat.""" self._client = client self._device = device @@ -127,32 +160,38 @@ class HoneywellUSThermostat(ClimateDevice): self._username = username self._password = password - self._supported_features = (SUPPORT_PRESET_MODE | - SUPPORT_TARGET_TEMPERATURE | - SUPPORT_TARGET_TEMPERATURE_RANGE) + _LOGGER.debug( + # noqa; pylint: disable=protected-access + "latestData = %s ", + device._data, + ) - # pylint: disable=protected-access - _LOGGER.debug("uiData = %s ", device._data['uiData']) - - # not all honeywell HVACs upport all modes - mappings = [v for k, v in HVAC_MODE_TO_HW_MODE.items() - if k in device._data['uiData']] + # not all honeywell HVACs support all modes + mappings = [v for k, v in HVAC_MODE_TO_HW_MODE.items() if device.raw_ui_data[k]] self._hvac_mode_map = {k: v for d in mappings for k, v in d.items()} - if device._data['canControlHumidification']: + self._supported_features = ( + SUPPORT_PRESET_MODE + | SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + ) + + # noqa; pylint: disable=protected-access + if device._data["canControlHumidification"]: self._supported_features |= SUPPORT_TARGET_HUMIDITY - if device._data['uiData']['SwitchEmergencyHeatAllowed']: + + if device.raw_ui_data["SwitchEmergencyHeatAllowed"]: self._supported_features |= SUPPORT_AUX_HEAT - if not device._data['hasFan']: + if not device._data["hasFan"]: # pylint: disable=protected-access return - self._supported_features |= SUPPORT_FAN_MODE # not all honeywell fans support all modes - mappings = [v for k, v in FAN_MODE_TO_HW.items() - if k in device._data['fanData']] + mappings = [v for k, v in FAN_MODE_TO_HW.items() if device.raw_fan_data[k]] self._fan_mode_map = {k: v for d in mappings for k, v in d.items()} + self._supported_features |= SUPPORT_FAN_MODE + @property def name(self) -> Optional[str]: """Return the name of the honeywell, if any.""" @@ -161,11 +200,10 @@ class HoneywellUSThermostat(ClimateDevice): @property def device_state_attributes(self) -> Dict[str, Any]: """Return the device specific state attributes.""" - # pylint: disable=protected-access data = {} - if self._device._data['hasFan']: - data[ATTR_FAN_ACTION] = \ - 'running' if self._device.fan_running else 'idle' + data[ATTR_FAN_ACTION] = "running" if self._device.fan_running else "idle" + if self._device.raw_dr_data: + data["dr_phase"] = self._device.raw_dr_data.get("Phase") return data @property @@ -173,11 +211,28 @@ class HoneywellUSThermostat(ClimateDevice): """Return the list of supported features.""" return self._supported_features + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + if self.hvac_mode in [HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL]: + return self._device.raw_ui_data["CoolLowerSetptLimit"] + if self.hvac_mode == HVAC_MODE_HEAT: + return self._device.raw_ui_data["HeatLowerSetptLimit"] + return None + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + if self.hvac_mode == HVAC_MODE_COOL: + return self._device.raw_ui_data["CoolUpperSetptLimit"] + if self.hvac_mode in [HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL]: + return self._device.raw_ui_data["HeatUpperSetptLimit"] + return None + @property def temperature_unit(self) -> str: """Return the unit of measurement.""" - return (TEMP_CELSIUS if self._device.temperature_unit == 'C' - else TEMP_FAHRENHEIT) + return TEMP_CELSIUS if self._device.temperature_unit == "C" else TEMP_FAHRENHEIT @property def current_humidity(self) -> Optional[int]: @@ -197,6 +252,8 @@ class HoneywellUSThermostat(ClimateDevice): @property def hvac_action(self) -> Optional[str]: """Return the current running hvac operation if supported.""" + if self.hvac_mode == HVAC_MODE_OFF: + return None return HW_MODE_TO_HA_HVAC_ACTION[self._device.equipment_output_status] @property @@ -209,19 +266,23 @@ class HoneywellUSThermostat(ClimateDevice): """Return the temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_COOL: return self._device.setpoint_cool - if self.hvac_mode != HVAC_MODE_HEAT: + if self.hvac_mode == HVAC_MODE_HEAT: return self._device.setpoint_heat return None @property def target_temperature_high(self) -> Optional[float]: """Return the highbound target temperature we try to reach.""" - return self._device.setpoint_cool + if self.hvac_mode == HVAC_MODE_HEAT_COOL: + return self._device.setpoint_cool + return None @property def target_temperature_low(self) -> Optional[float]: """Return the lowbound target temperature we try to reach.""" - return self._device.setpoint_heat + if self.hvac_mode == HVAC_MODE_HEAT_COOL: + return self._device.setpoint_heat + return None @property def preset_mode(self) -> Optional[str]: @@ -231,12 +292,12 @@ class HoneywellUSThermostat(ClimateDevice): @property def preset_modes(self) -> Optional[List[str]]: """Return a list of available preset modes.""" - return [PRESET_AWAY] + return [PRESET_NONE, PRESET_AWAY] @property def is_aux_heat(self) -> Optional[str]: """Return true if aux heater.""" - return self._device.system_mode == 'emheat' + return self._device.system_mode == "emheat" @property def fan_mode(self) -> Optional[str]: @@ -259,19 +320,17 @@ class HoneywellUSThermostat(ClimateDevice): # Set hold if this is not the case if getattr(self._device, "hold_{}".format(mode)) is False: # Get next period key - next_period_key = '{}NextPeriod'.format(mode.capitalize()) + next_period_key = "{}NextPeriod".format(mode.capitalize()) # Get next period raw value next_period = self._device.raw_ui_data.get(next_period_key) # Get next period time hour, minute = divmod(next_period * 15, 60) # Set hold time - setattr(self._device, - "hold_{}".format(mode), - datetime.time(hour, minute)) + setattr( + self._device, "hold_{}".format(mode), datetime.time(hour, minute) + ) # Set temperature - setattr(self._device, - "setpoint_{}".format(mode), - temperature) + setattr(self._device, "setpoint_{}".format(mode), temperature) except somecomfort.SomeComfortError: _LOGGER.error("Temperature %.1f out of range", temperature) @@ -311,21 +370,23 @@ class HoneywellUSThermostat(ClimateDevice): # Get current mode mode = self._device.system_mode except somecomfort.SomeComfortError: - _LOGGER.error('Can not get system mode') + _LOGGER.error("Can not get system mode") return try: # Set permanent hold - setattr(self._device, - "hold_{}".format(mode), - True) + setattr(self._device, "hold_{}".format(mode), True) # Set temperature - setattr(self._device, - "setpoint_{}".format(mode), - getattr(self, "_{}_away_temp".format(mode))) + setattr( + self._device, + "setpoint_{}".format(mode), + getattr(self, "_{}_away_temp".format(mode)), + ) except somecomfort.SomeComfortError: - _LOGGER.error('Temperature %.1f out of range', - getattr(self, "_{}_away_temp".format(mode))) + _LOGGER.error( + "Temperature %.1f out of range", + getattr(self, "_{}_away_temp".format(mode)), + ) def _turn_away_mode_off(self) -> None: """Turn away off.""" @@ -335,7 +396,7 @@ class HoneywellUSThermostat(ClimateDevice): self._device.hold_cool = False self._device.hold_heat = False except somecomfort.SomeComfortError: - _LOGGER.error('Can not stop hold mode') + _LOGGER.error("Can not stop hold mode") def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" @@ -346,11 +407,14 @@ class HoneywellUSThermostat(ClimateDevice): def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" - self._device.system_mode = 'emheat' + self._device.system_mode = "emheat" def turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" - self._device.system_mode = 'auto' + if HVAC_MODE_HEAT in self.hvac_modes: + self.set_hvac_mode(HVAC_MODE_HEAT) + else: + self.set_hvac_mode(HVAC_MODE_OFF) def _retry(self) -> bool: """Recreate a new somecomfort client. @@ -359,21 +423,20 @@ class HoneywellUSThermostat(ClimateDevice): will succeed, is to recreate a new somecomfort client. """ try: - self._client = somecomfort.SomeComfort( - self._username, self._password) + self._client = somecomfort.SomeComfort(self._username, self._password) except somecomfort.AuthError: - _LOGGER.error("Failed to login to honeywell account %s", - self._username) + _LOGGER.error("Failed to login to honeywell account %s", self._username) return False except somecomfort.SomeComfortError as ex: - _LOGGER.error("Failed to initialize honeywell client: %s", - str(ex)) + _LOGGER.error("Failed to initialize honeywell client: %s", str(ex)) return False - devices = [device - for location in self._client.locations_by_id.values() - for device in location.devices_by_id.values() - if device.name == self._device.name] + devices = [ + device + for location in self._client.locations_by_id.values() + for device in location.devices_by_id.values() + if device.name == self._device.name + ] if len(devices) != 1: _LOGGER.error("Failed to find device %s", self._device.name) @@ -389,12 +452,20 @@ class HoneywellUSThermostat(ClimateDevice): try: self._device.refresh() break - except (somecomfort.client.APIRateLimited, OSError, - requests.exceptions.ReadTimeout) as exp: + except ( + somecomfort.client.APIRateLimited, + OSError, + requests.exceptions.ReadTimeout, + ) as exp: retries -= 1 if retries == 0: raise exp if not self._retry(): raise exp - _LOGGER.error( - "SomeComfort update failed, Retrying - Error: %s", exp) + _LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp) + + _LOGGER.debug( + # noqa; pylint: disable=protected-access + "latestData = %s ", + self._device._data, + ) diff --git a/homeassistant/components/hook/switch.py b/homeassistant/components/hook/switch.py index abe2040b091..d26f35e2dfc 100644 --- a/homeassistant/components/hook/switch.py +++ b/homeassistant/components/hook/switch.py @@ -6,28 +6,35 @@ import voluptuous as vol import async_timeout import aiohttp -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -HOOK_ENDPOINT = 'https://api.gethook.io/v1/' +HOOK_ENDPOINT = "https://api.gethook.io/v1/" TIMEOUT = 10 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Exclusive(CONF_PASSWORD, 'hook_secret', msg='hook: provide ' + - 'username/password OR token'): cv.string, - vol.Exclusive(CONF_TOKEN, 'hook_secret', msg='hook: provide ' + - 'username/password OR token'): cv.string, - vol.Inclusive(CONF_USERNAME, 'hook_auth'): cv.string, - vol.Inclusive(CONF_PASSWORD, 'hook_auth'): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Exclusive( + CONF_PASSWORD, + "hook_secret", + msg="hook: provide " + "username/password OR token", + ): cv.string, + vol.Exclusive( + CONF_TOKEN, + "hook_secret", + msg="hook: provide " + "username/password OR token", + ): cv.string, + vol.Inclusive(CONF_USERNAME, "hook_auth"): cv.string, + vol.Inclusive(CONF_PASSWORD, "hook_auth"): cv.string, + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up Hook by getting the access token and list of actions.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -38,10 +45,9 @@ async def async_setup_platform(hass, config, async_add_entities, try: with async_timeout.timeout(TIMEOUT): response = await websession.post( - '{}{}'.format(HOOK_ENDPOINT, 'user/login'), - data={ - 'username': username, - 'password': password}) + "{}{}".format(HOOK_ENDPOINT, "user/login"), + data={"username": username, "password": password}, + ) # The Hook API returns JSON but calls it 'text/html'. Setting # content_type=None disables aiohttp's content-type validation. data = await response.json(content_type=None) @@ -50,7 +56,7 @@ async def async_setup_platform(hass, config, async_add_entities, return False try: - token = data['data']['token'] + token = data["data"]["token"] except KeyError: _LOGGER.error("No token. Check username and password") return False @@ -58,21 +64,18 @@ async def async_setup_platform(hass, config, async_add_entities, try: with async_timeout.timeout(TIMEOUT): response = await websession.get( - '{}{}'.format(HOOK_ENDPOINT, 'device'), - params={"token": token}) + "{}{}".format(HOOK_ENDPOINT, "device"), params={"token": token} + ) data = await response.json(content_type=None) except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed getting devices: %s", error) return False async_add_entities( - HookSmartHome( - hass, - token, - d['device_id'], - d['device_name']) - for lst in data['data'] - for d in lst) + HookSmartHome(hass, token, d["device_id"], d["device_name"]) + for lst in data["data"] + for d in lst + ) class HookSmartHome(SwitchDevice): @@ -85,8 +88,7 @@ class HookSmartHome(SwitchDevice): self._state = False self._id = device_id self._name = device_name - _LOGGER.debug( - "Creating Hook object: ID: %s Name: %s", self._id, self._name) + _LOGGER.debug("Creating Hook object: ID: %s Name: %s", self._id, self._name) @property def name(self): @@ -104,8 +106,7 @@ class HookSmartHome(SwitchDevice): _LOGGER.debug("Sending: %s", url) websession = async_get_clientsession(self.hass) with async_timeout.timeout(TIMEOUT): - response = await websession.get( - url, params={"token": self._token}) + response = await websession.get(url, params={"token": self._token}) data = await response.json(content_type=None) except (asyncio.TimeoutError, aiohttp.ClientError) as error: @@ -113,21 +114,19 @@ class HookSmartHome(SwitchDevice): return False _LOGGER.debug("Got: %s", data) - return data['return_value'] == '1' + return data["return_value"] == "1" async def async_turn_on(self, **kwargs): """Turn the device on asynchronously.""" _LOGGER.debug("Turning on: %s", self._name) - url = '{}{}{}{}'.format( - HOOK_ENDPOINT, 'device/trigger/', self._id, '/On') + url = "{}{}{}{}".format(HOOK_ENDPOINT, "device/trigger/", self._id, "/On") success = await self._send(url) self._state = success async def async_turn_off(self, **kwargs): """Turn the device off asynchronously.""" _LOGGER.debug("Turning off: %s", self._name) - url = '{}{}{}{}'.format( - HOOK_ENDPOINT, 'device/trigger/', self._id, '/Off') + url = "{}{}{}{}".format(HOOK_ENDPOINT, "device/trigger/", self._id, "/Off") success = await self._send(url) # If it wasn't successful, keep state as true self._state = not success diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index ab72b051f1b..8bed30e88f3 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -5,34 +5,53 @@ import logging import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON) + MEDIA_TYPE_CHANNEL, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) + CONF_HOST, + CONF_NAME, + CONF_PORT, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Horizon' +DEFAULT_NAME = "Horizon" DEFAULT_PORT = 5900 MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -SUPPORT_HORIZON = SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PLAY | \ - SUPPORT_PLAY_MEDIA | SUPPORT_PREVIOUS_TRACK | SUPPORT_TURN_ON | \ - SUPPORT_TURN_OFF +SUPPORT_HORIZON = ( + SUPPORT_NEXT_TRACK + | SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -142,8 +161,11 @@ class HorizonDevice(MediaPlayerDevice): except ValueError: _LOGGER.error("Invalid channel: %s", media_id) else: - _LOGGER.error("Invalid media type %s. Supported type: %s", - media_type, MEDIA_TYPE_CHANNEL) + _LOGGER.error( + "Invalid media type %s. Supported type: %s", + media_type, + MEDIA_TYPE_CHANNEL, + ) def _select_channel(self, channel): """Select a channel (taken from einder library, thx).""" @@ -163,8 +185,9 @@ class HorizonDevice(MediaPlayerDevice): elif channel: self._client.select_channel(channel) except OSError as msg: - _LOGGER.error("%s disconnected: %s. Trying to reconnect...", - self._name, msg) + _LOGGER.error( + "%s disconnected: %s. Trying to reconnect...", self._name, msg + ) # for reconnect, first gracefully disconnect self._client.disconnect() @@ -173,8 +196,7 @@ class HorizonDevice(MediaPlayerDevice): self._client.connect() self._client.authorize() except AuthenticationError as msg: - _LOGGER.error("Authentication to %s failed: %s", self._name, - msg) + _LOGGER.error("Authentication to %s failed: %s", self._name, msg) return except OSError as msg: # occurs if horizon box is offline diff --git a/homeassistant/components/hp_ilo/manifest.json b/homeassistant/components/hp_ilo/manifest.json index 3df6632e47a..a3d5853541f 100644 --- a/homeassistant/components/hp_ilo/manifest.json +++ b/homeassistant/components/hp_ilo/manifest.json @@ -3,7 +3,7 @@ "name": "Hp ilo", "documentation": "https://www.home-assistant.io/components/hp_ilo", "requirements": [ - "python-hpilo==3.9" + "python-hpilo==4.3" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index 46fde885613..1ad70c06397 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -6,9 +6,16 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_MONITORED_VARIABLES, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_SENSOR_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, - CONF_VALUE_TEMPLATE) + CONF_HOST, + CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SENSOR_TYPE, + CONF_UNIT_OF_MEASUREMENT, + CONF_USERNAME, + CONF_VALUE_TEMPLATE, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -21,35 +28,43 @@ DEFAULT_PORT = 443 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) SENSOR_TYPES = { - 'server_name': ['Server Name', 'get_server_name'], - 'server_fqdn': ['Server FQDN', 'get_server_fqdn'], - 'server_host_data': ['Server Host Data', 'get_host_data'], - 'server_oa_info': ['Server Onboard Administrator Info', 'get_oa_info'], - 'server_power_status': ['Server Power state', 'get_host_power_status'], - 'server_power_readings': ['Server Power readings', 'get_power_readings'], - 'server_power_on_time': ['Server Power On time', - 'get_server_power_on_time'], - 'server_asset_tag': ['Server Asset Tag', 'get_asset_tag'], - 'server_uid_status': ['Server UID light', 'get_uid_status'], - 'server_health': ['Server Health', 'get_embedded_health'], - 'network_settings': ['Network Settings', 'get_network_settings'] + "server_name": ["Server Name", "get_server_name"], + "server_fqdn": ["Server FQDN", "get_server_fqdn"], + "server_host_data": ["Server Host Data", "get_host_data"], + "server_oa_info": ["Server Onboard Administrator Info", "get_oa_info"], + "server_power_status": ["Server Power state", "get_host_power_status"], + "server_power_readings": ["Server Power readings", "get_power_readings"], + "server_power_on_time": ["Server Power On time", "get_server_power_on_time"], + "server_asset_tag": ["Server Asset Tag", "get_asset_tag"], + "server_uid_status": ["Server UID light", "get_uid_status"], + "server_health": ["Server Health", "get_embedded_health"], + "network_settings": ["Network Settings", "get_network_settings"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=[]): - vol.All(cv.ensure_list, [vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_SENSOR_TYPE): - vol.All(cv.string, vol.In(SENSOR_TYPES)), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template - })]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_SENSOR_TYPE): vol.All( + cv.string, vol.In(SENSOR_TYPES) + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } + ) + ], + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -74,12 +89,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HpIloSensor( hass=hass, hp_ilo_data=hp_ilo_data, - sensor_name='{} {}'.format( - config.get(CONF_NAME), monitored_variable[CONF_NAME]), + sensor_name="{} {}".format( + config.get(CONF_NAME), monitored_variable[CONF_NAME] + ), sensor_type=monitored_variable[CONF_SENSOR_TYPE], sensor_value_template=monitored_variable.get(CONF_VALUE_TEMPLATE), - unit_of_measurement=monitored_variable.get( - CONF_UNIT_OF_MEASUREMENT)) + unit_of_measurement=monitored_variable.get(CONF_UNIT_OF_MEASUREMENT), + ) devices.append(new_device) add_entities(devices, True) @@ -88,8 +104,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class HpIloSensor(Entity): """Representation of a HP iLO sensor.""" - def __init__(self, hass, hp_ilo_data, sensor_type, sensor_name, - sensor_value_template, unit_of_measurement): + def __init__( + self, + hass, + hp_ilo_data, + sensor_type, + sensor_name, + sensor_value_template, + unit_of_measurement, + ): """Initialize the HP iLO sensor.""" self._hass = hass self._name = sensor_name @@ -161,8 +184,14 @@ class HpIloData: try: self.data = hpilo.Ilo( - hostname=self._host, login=self._login, - password=self._password, port=self._port) - except (hpilo.IloError, hpilo.IloCommunicationError, - hpilo.IloLoginFailed) as error: + hostname=self._host, + login=self._login, + password=self._password, + port=self._port, + ) + except ( + hpilo.IloError, + hpilo.IloCommunicationError, + hpilo.IloLoginFailed, + ) as error: raise ValueError("Unable to init HP ILO, {}".format(error)) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index c8cd207da3e..18882968cf9 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta from functools import partial +from urllib.parse import urlparse import json import logging import time @@ -15,27 +16,37 @@ from homeassistant.components import websocket_api from homeassistant.components.frontend import add_manifest_json_key from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, URL_ROOT) + HTTP_BAD_REQUEST, + HTTP_INTERNAL_SERVER_ERROR, + HTTP_UNAUTHORIZED, + URL_ROOT, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.util import ensure_unique_string from homeassistant.util.json import load_json, save_json from homeassistant.components.notify import ( - ATTR_DATA, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, DOMAIN, - PLATFORM_SCHEMA, BaseNotificationService) + ATTR_DATA, + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + DOMAIN, + PLATFORM_SCHEMA, + BaseNotificationService, +) _LOGGER = logging.getLogger(__name__) -REGISTRATIONS_FILE = 'html5_push_registrations.conf' +REGISTRATIONS_FILE = "html5_push_registrations.conf" -SERVICE_DISMISS = 'html5_dismiss' +SERVICE_DISMISS = "html5_dismiss" -ATTR_GCM_SENDER_ID = 'gcm_sender_id' -ATTR_GCM_API_KEY = 'gcm_api_key' -ATTR_VAPID_PUB_KEY = 'vapid_pub_key' -ATTR_VAPID_PRV_KEY = 'vapid_prv_key' -ATTR_VAPID_EMAIL = 'vapid_email' +ATTR_GCM_SENDER_ID = "gcm_sender_id" +ATTR_GCM_API_KEY = "gcm_api_key" +ATTR_VAPID_PUB_KEY = "vapid_pub_key" +ATTR_VAPID_PRV_KEY = "vapid_prv_key" +ATTR_VAPID_EMAIL = "vapid_email" def gcm_api_deprecated(value): @@ -45,92 +56,114 @@ def gcm_api_deprecated(value): "Configuring html5_push_notifications via the GCM api" " has been deprecated and will stop working after April 11," " 2019. Use the VAPID configuration instead. For instructions," - " see https://www.home-assistant.io/components/notify.html5/") + " see https://www.home-assistant.io/components/notify.html5/" + ) return value -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(ATTR_GCM_SENDER_ID): - vol.All(cv.string, gcm_api_deprecated), - vol.Optional(ATTR_GCM_API_KEY): cv.string, - vol.Optional(ATTR_VAPID_PUB_KEY): cv.string, - vol.Optional(ATTR_VAPID_PRV_KEY): cv.string, - vol.Optional(ATTR_VAPID_EMAIL): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(ATTR_GCM_SENDER_ID): vol.All(cv.string, gcm_api_deprecated), + vol.Optional(ATTR_GCM_API_KEY): cv.string, + vol.Optional(ATTR_VAPID_PUB_KEY): cv.string, + vol.Optional(ATTR_VAPID_PRV_KEY): cv.string, + vol.Optional(ATTR_VAPID_EMAIL): cv.string, + } +) -ATTR_SUBSCRIPTION = 'subscription' -ATTR_BROWSER = 'browser' -ATTR_NAME = 'name' +ATTR_SUBSCRIPTION = "subscription" +ATTR_BROWSER = "browser" +ATTR_NAME = "name" -ATTR_ENDPOINT = 'endpoint' -ATTR_KEYS = 'keys' -ATTR_AUTH = 'auth' -ATTR_P256DH = 'p256dh' -ATTR_EXPIRATIONTIME = 'expirationTime' +ATTR_ENDPOINT = "endpoint" +ATTR_KEYS = "keys" +ATTR_AUTH = "auth" +ATTR_P256DH = "p256dh" +ATTR_EXPIRATIONTIME = "expirationTime" -ATTR_TAG = 'tag' -ATTR_ACTION = 'action' -ATTR_ACTIONS = 'actions' -ATTR_TYPE = 'type' -ATTR_URL = 'url' -ATTR_DISMISS = 'dismiss' -ATTR_PRIORITY = 'priority' -DEFAULT_PRIORITY = 'normal' -ATTR_TTL = 'ttl' +ATTR_TAG = "tag" +ATTR_ACTION = "action" +ATTR_ACTIONS = "actions" +ATTR_TYPE = "type" +ATTR_URL = "url" +ATTR_DISMISS = "dismiss" +ATTR_PRIORITY = "priority" +DEFAULT_PRIORITY = "normal" +ATTR_TTL = "ttl" DEFAULT_TTL = 86400 -ATTR_JWT = 'jwt' +ATTR_JWT = "jwt" -WS_TYPE_APPKEY = 'notify/html5/appkey' -SCHEMA_WS_APPKEY = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_APPKEY -}) +WS_TYPE_APPKEY = "notify/html5/appkey" +SCHEMA_WS_APPKEY = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_APPKEY} +) # The number of days after the moment a notification is sent that a JWT # is valid. JWT_VALID_DAYS = 7 KEYS_SCHEMA = vol.All( - dict, vol.Schema({ - vol.Required(ATTR_AUTH): cv.string, - vol.Required(ATTR_P256DH): cv.string, - }) + dict, + vol.Schema( + {vol.Required(ATTR_AUTH): cv.string, vol.Required(ATTR_P256DH): cv.string} + ), ) SUBSCRIPTION_SCHEMA = vol.All( - dict, vol.Schema({ - # pylint: disable=no-value-for-parameter - vol.Required(ATTR_ENDPOINT): vol.Url(), - vol.Required(ATTR_KEYS): KEYS_SCHEMA, - vol.Optional(ATTR_EXPIRATIONTIME): vol.Any(None, cv.positive_int), - }) + dict, + vol.Schema( + { + # pylint: disable=no-value-for-parameter + vol.Required(ATTR_ENDPOINT): vol.Url(), + vol.Required(ATTR_KEYS): KEYS_SCHEMA, + vol.Optional(ATTR_EXPIRATIONTIME): vol.Any(None, cv.positive_int), + } + ), ) -DISMISS_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_DATA): dict, -}) +DISMISS_SERVICE_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_DATA): dict, + } +) -REGISTER_SCHEMA = vol.Schema({ - vol.Required(ATTR_SUBSCRIPTION): SUBSCRIPTION_SCHEMA, - vol.Required(ATTR_BROWSER): vol.In(['chrome', 'firefox']), - vol.Optional(ATTR_NAME): cv.string -}) +REGISTER_SCHEMA = vol.Schema( + { + vol.Required(ATTR_SUBSCRIPTION): SUBSCRIPTION_SCHEMA, + vol.Required(ATTR_BROWSER): vol.In(["chrome", "firefox"]), + vol.Optional(ATTR_NAME): cv.string, + } +) -CALLBACK_EVENT_PAYLOAD_SCHEMA = vol.Schema({ - vol.Required(ATTR_TAG): cv.string, - vol.Required(ATTR_TYPE): vol.In(['received', 'clicked', 'closed']), - vol.Required(ATTR_TARGET): cv.string, - vol.Optional(ATTR_ACTION): cv.string, - vol.Optional(ATTR_DATA): dict, -}) +CALLBACK_EVENT_PAYLOAD_SCHEMA = vol.Schema( + { + vol.Required(ATTR_TAG): cv.string, + vol.Required(ATTR_TYPE): vol.In(["received", "clicked", "closed"]), + vol.Required(ATTR_TARGET): cv.string, + vol.Optional(ATTR_ACTION): cv.string, + vol.Optional(ATTR_DATA): dict, + } +) -NOTIFY_CALLBACK_EVENT = 'html5_notification' +NOTIFY_CALLBACK_EVENT = "html5_notification" # Badge and timestamp are Chrome specific (not in official spec) HTML5_SHOWNOTIFICATION_PARAMETERS = ( - 'actions', 'badge', 'body', 'dir', 'icon', 'image', 'lang', - 'renotify', 'requireInteraction', 'tag', 'timestamp', 'vibrate') + "actions", + "badge", + "body", + "dir", + "icon", + "image", + "lang", + "renotify", + "requireInteraction", + "tag", + "timestamp", + "vibrate", +) def get_service(hass, config, discovery_info=None): @@ -147,27 +180,24 @@ def get_service(hass, config, discovery_info=None): vapid_email = config.get(ATTR_VAPID_EMAIL) def websocket_appkey(hass, connection, msg): - connection.send_message( - websocket_api.result_message(msg['id'], vapid_pub_key)) + connection.send_message(websocket_api.result_message(msg["id"], vapid_pub_key)) hass.components.websocket_api.async_register_command( WS_TYPE_APPKEY, websocket_appkey, SCHEMA_WS_APPKEY ) - hass.http.register_view( - HTML5PushRegistrationView(registrations, json_path)) + hass.http.register_view(HTML5PushRegistrationView(registrations, json_path)) hass.http.register_view(HTML5PushCallbackView(registrations)) gcm_api_key = config.get(ATTR_GCM_API_KEY) gcm_sender_id = config.get(ATTR_GCM_SENDER_ID) if gcm_sender_id is not None: - add_manifest_json_key( - ATTR_GCM_SENDER_ID, config.get(ATTR_GCM_SENDER_ID)) + add_manifest_json_key(ATTR_GCM_SENDER_ID, config.get(ATTR_GCM_SENDER_ID)) return HTML5NotificationService( - hass, gcm_api_key, vapid_prv_key, vapid_email, registrations, - json_path) + hass, gcm_api_key, vapid_prv_key, vapid_email, registrations, json_path + ) def _load_config(filename): @@ -182,8 +212,8 @@ def _load_config(filename): class HTML5PushRegistrationView(HomeAssistantView): """Accepts push registrations from a browser.""" - url = '/api/notify.html5' - name = 'api:notify.html5' + url = "/api/notify.html5" + name = "api:notify.html5" def __init__(self, registrations, json_path): """Init HTML5PushRegistrationView.""" @@ -195,12 +225,11 @@ class HTML5PushRegistrationView(HomeAssistantView): try: data = await request.json() except ValueError: - return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) try: data = REGISTER_SCHEMA(data) except vol.Invalid as ex: - return self.json_message( - humanize_error(data, ex), HTTP_BAD_REQUEST) + return self.json_message(humanize_error(data, ex), HTTP_BAD_REQUEST) devname = data.get(ATTR_NAME) data.pop(ATTR_NAME, None) @@ -211,12 +240,10 @@ class HTML5PushRegistrationView(HomeAssistantView): self.registrations[name] = data try: - hass = request.app['hass'] + hass = request.app["hass"] - await hass.async_add_job(save_json, self.json_path, - self.registrations) - return self.json_message( - 'Push notification subscriber registered.') + await hass.async_add_job(save_json, self.json_path, self.registrations) + return self.json_message("Push notification subscriber registered.") except HomeAssistantError: if previous_registration is not None: self.registrations[name] = previous_registration @@ -224,7 +251,8 @@ class HTML5PushRegistrationView(HomeAssistantView): self.registrations.pop(name) return self.json_message( - 'Error saving registration.', HTTP_INTERNAL_SERVER_ERROR) + "Error saving registration.", HTTP_INTERNAL_SERVER_ERROR + ) def find_registration_name(self, data, suggested=None): """Find a registration name matching data or generate a unique one.""" @@ -233,15 +261,14 @@ class HTML5PushRegistrationView(HomeAssistantView): subscription = registration.get(ATTR_SUBSCRIPTION) if subscription.get(ATTR_ENDPOINT) == endpoint: return key - return ensure_unique_string(suggested or 'unnamed device', - self.registrations) + return ensure_unique_string(suggested or "unnamed device", self.registrations) async def delete(self, request): """Delete a registration.""" try: data = await request.json() except ValueError: - return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) subscription = data.get(ATTR_SUBSCRIPTION) @@ -254,29 +281,29 @@ class HTML5PushRegistrationView(HomeAssistantView): if not found: # If not found, unregistering was already done. Return 200 - return self.json_message('Registration not found.') + return self.json_message("Registration not found.") reg = self.registrations.pop(found) try: - hass = request.app['hass'] + hass = request.app["hass"] - await hass.async_add_job(save_json, self.json_path, - self.registrations) + await hass.async_add_job(save_json, self.json_path, self.registrations) except HomeAssistantError: self.registrations[found] = reg return self.json_message( - 'Error saving registration.', HTTP_INTERNAL_SERVER_ERROR) + "Error saving registration.", HTTP_INTERNAL_SERVER_ERROR + ) - return self.json_message('Push notification subscriber unregistered.') + return self.json_message("Push notification subscriber unregistered.") class HTML5PushCallbackView(HomeAssistantView): """Accepts push registrations from a browser.""" requires_auth = False - url = '/api/notify.html5/callback' - name = 'api:notify.html5/callback' + url = "/api/notify.html5/callback" + name = "api:notify.html5/callback" def __init__(self, registrations): """Init HTML5PushCallbackView.""" @@ -300,36 +327,40 @@ class HTML5PushCallbackView(HomeAssistantView): except jwt.exceptions.DecodeError: pass - return self.json_message('No target found in JWT', - status_code=HTTP_UNAUTHORIZED) + return self.json_message( + "No target found in JWT", status_code=HTTP_UNAUTHORIZED + ) # The following is based on code from Auth0 # https://auth0.com/docs/quickstart/backend/python def check_authorization_header(self, request): """Check the authorization header.""" import jwt + auth = request.headers.get(AUTHORIZATION, None) if not auth: - return self.json_message('Authorization header is expected', - status_code=HTTP_UNAUTHORIZED) + return self.json_message( + "Authorization header is expected", status_code=HTTP_UNAUTHORIZED + ) parts = auth.split() - if parts[0].lower() != 'bearer': - return self.json_message('Authorization header must ' - 'start with Bearer', - status_code=HTTP_UNAUTHORIZED) + if parts[0].lower() != "bearer": + return self.json_message( + "Authorization header must " "start with Bearer", + status_code=HTTP_UNAUTHORIZED, + ) if len(parts) != 2: - return self.json_message('Authorization header must ' - 'be Bearer token', - status_code=HTTP_UNAUTHORIZED) + return self.json_message( + "Authorization header must " "be Bearer token", + status_code=HTTP_UNAUTHORIZED, + ) token = parts[1] try: payload = self.decode_jwt(token) except jwt.exceptions.InvalidTokenError: - return self.json_message('token is invalid', - status_code=HTTP_UNAUTHORIZED) + return self.json_message("token is invalid", status_code=HTTP_UNAUTHORIZED) return payload async def post(self, request): @@ -341,7 +372,7 @@ class HTML5PushCallbackView(HomeAssistantView): try: data = await request.json() except ValueError: - return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) event_payload = { ATTR_TAG: data.get(ATTR_TAG), @@ -358,20 +389,20 @@ class HTML5PushCallbackView(HomeAssistantView): try: event_payload = CALLBACK_EVENT_PAYLOAD_SCHEMA(event_payload) except vol.Invalid as ex: - _LOGGER.warning("Callback event payload is not valid: %s", - humanize_error(event_payload, ex)) + _LOGGER.warning( + "Callback event payload is not valid: %s", + humanize_error(event_payload, ex), + ) - event_name = '{}.{}'.format(NOTIFY_CALLBACK_EVENT, - event_payload[ATTR_TYPE]) - request.app['hass'].bus.fire(event_name, event_payload) - return self.json({'status': 'ok', 'event': event_payload[ATTR_TYPE]}) + event_name = "{}.{}".format(NOTIFY_CALLBACK_EVENT, event_payload[ATTR_TYPE]) + request.app["hass"].bus.fire(event_name, event_payload) + return self.json({"status": "ok", "event": event_payload[ATTR_TYPE]}) class HTML5NotificationService(BaseNotificationService): """Implement the notification service for HTML5.""" - def __init__(self, hass, gcm_key, vapid_prv, vapid_email, registrations, - json_path): + def __init__(self, hass, gcm_key, vapid_prv, vapid_email, registrations, json_path): """Initialize the service.""" self._gcm_key = gcm_key self._vapid_prv = vapid_prv @@ -393,8 +424,11 @@ class HTML5NotificationService(BaseNotificationService): await self.async_dismiss(**kwargs) hass.services.async_register( - DOMAIN, SERVICE_DISMISS, async_dismiss_message, - schema=DISMISS_SERVICE_SCHEMA) + DOMAIN, + SERVICE_DISMISS, + async_dismiss_message, + schema=DISMISS_SERVICE_SCHEMA, + ) @property def targets(self): @@ -408,11 +442,7 @@ class HTML5NotificationService(BaseNotificationService): """Dismisses a notification.""" data = kwargs.get(ATTR_DATA) tag = data.get(ATTR_TAG) if data else "" - payload = { - ATTR_TAG: tag, - ATTR_DISMISS: True, - ATTR_DATA: {} - } + payload = {ATTR_TAG: tag, ATTR_DISMISS: True, ATTR_DATA: {}} self._push_message(payload, **kwargs) @@ -421,19 +451,18 @@ class HTML5NotificationService(BaseNotificationService): This method must be run in the event loop. """ - await self.hass.async_add_executor_job( - partial(self.dismiss, **kwargs)) + await self.hass.async_add_executor_job(partial(self.dismiss, **kwargs)) def send_message(self, message="", **kwargs): """Send a message to a user.""" tag = str(uuid.uuid4()) payload = { - 'badge': '/static/images/notification-badge.png', - 'body': message, + "badge": "/static/images/notification-badge.png", + "body": message, ATTR_DATA: {}, - 'icon': '/static/icons/favicon-192x192.png', + "icon": "/static/icons/favicon-192x192.png", ATTR_TAG: tag, - ATTR_TITLE: kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + ATTR_TITLE: kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), } data = kwargs.get(ATTR_DATA) @@ -452,8 +481,10 @@ class HTML5NotificationService(BaseNotificationService): payload[ATTR_DATA] = data_tmp - if (payload[ATTR_DATA].get(ATTR_URL) is None and - payload.get(ATTR_ACTIONS) is None): + if ( + payload[ATTR_DATA].get(ATTR_URL) is None + and payload.get(ATTR_ACTIONS) is None + ): payload[ATTR_DATA][ATTR_URL] = URL_ROOT self._push_message(payload, **kwargs) @@ -465,9 +496,9 @@ class HTML5NotificationService(BaseNotificationService): timestamp = int(time.time()) ttl = int(kwargs.get(ATTR_TTL, DEFAULT_TTL)) priority = kwargs.get(ATTR_PRIORITY, DEFAULT_PRIORITY) - if priority not in ['normal', 'high']: + if priority not in ["normal", "high"]: priority = DEFAULT_PRIORITY - payload['timestamp'] = (timestamp*1000) # Javascript ms since epoch + payload["timestamp"] = timestamp * 1000 # Javascript ms since epoch targets = kwargs.get(ATTR_TARGET) if not targets: @@ -478,42 +509,39 @@ class HTML5NotificationService(BaseNotificationService): try: info = REGISTER_SCHEMA(info) except vol.Invalid: - _LOGGER.error("%s is not a valid HTML5 push notification" - " target", target) + _LOGGER.error( + "%s is not a valid HTML5 push notification" " target", target + ) continue payload[ATTR_DATA][ATTR_JWT] = add_jwt( - timestamp, target, payload[ATTR_TAG], - info[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH]) + timestamp, + target, + payload[ATTR_TAG], + info[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH], + ) webpusher = WebPusher(info[ATTR_SUBSCRIPTION]) if self._vapid_prv and self._vapid_email: vapid_headers = create_vapid_headers( - self._vapid_email, info[ATTR_SUBSCRIPTION], - self._vapid_prv) - vapid_headers.update({ - 'urgency': priority, - 'priority': priority - }) + self._vapid_email, info[ATTR_SUBSCRIPTION], self._vapid_prv + ) + vapid_headers.update({"urgency": priority, "priority": priority}) response = webpusher.send( - data=json.dumps(payload), - headers=vapid_headers, - ttl=ttl + data=json.dumps(payload), headers=vapid_headers, ttl=ttl ) else: # Only pass the gcm key if we're actually using GCM # If we don't, notifications break on FireFox - gcm_key = self._gcm_key \ - if 'googleapis.com' \ - in info[ATTR_SUBSCRIPTION][ATTR_ENDPOINT] \ + gcm_key = ( + self._gcm_key + if "googleapis.com" in info[ATTR_SUBSCRIPTION][ATTR_ENDPOINT] else None - response = webpusher.send( - json.dumps(payload), gcm_key=gcm_key, ttl=ttl ) + response = webpusher.send(json.dumps(payload), gcm_key=gcm_key, ttl=ttl) if response.status_code == 410: _LOGGER.info("Notification channel has expired") reg = self.registrations.pop(target) - if not save_json(self.registrations_json_path, - self.registrations): + if not save_json(self.registrations_json_path, self.registrations): self.registrations[target] = reg _LOGGER.error("Error saving registration") else: @@ -523,27 +551,27 @@ class HTML5NotificationService(BaseNotificationService): def add_jwt(timestamp, target, tag, jwt_secret): """Create JWT json to put into payload.""" import jwt - jwt_exp = (datetime.fromtimestamp(timestamp) + - timedelta(days=JWT_VALID_DAYS)) - jwt_claims = {'exp': jwt_exp, 'nbf': timestamp, - 'iat': timestamp, ATTR_TARGET: target, - ATTR_TAG: tag} - return jwt.encode(jwt_claims, jwt_secret).decode('utf-8') + + jwt_exp = datetime.fromtimestamp(timestamp) + timedelta(days=JWT_VALID_DAYS) + jwt_claims = { + "exp": jwt_exp, + "nbf": timestamp, + "iat": timestamp, + ATTR_TARGET: target, + ATTR_TAG: tag, + } + return jwt.encode(jwt_claims, jwt_secret).decode("utf-8") def create_vapid_headers(vapid_email, subscription_info, vapid_private_key): """Create encrypted headers to send to WebPusher.""" from py_vapid import Vapid - try: - from urllib.parse import urlparse - except ImportError: # pragma: no cover - from urlparse import urlparse - if (vapid_email and vapid_private_key and - ATTR_ENDPOINT in subscription_info): + + if vapid_email and vapid_private_key and ATTR_ENDPOINT in subscription_info: url = urlparse(subscription_info.get(ATTR_ENDPOINT)) vapid_claims = { - 'sub': 'mailto:{}'.format(vapid_email), - 'aud': "{}://{}".format(url.scheme, url.netloc) + "sub": "mailto:{}".format(vapid_email), + "aud": "{}://{}".format(url.scheme, url.netloc), } vapid = Vapid.from_string(private_key=vapid_private_key) return vapid.sign(vapid_claims) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 7731c96c9ac..84c7d15a580 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -10,7 +10,10 @@ from aiohttp.web_exceptions import HTTPMovedPermanently import voluptuous as vol from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, SERVER_PORT) + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + SERVER_PORT, +) import homeassistant.helpers.config_validation as cv import homeassistant.util as hass_util from homeassistant.util import ssl as ssl_util @@ -18,41 +21,36 @@ from homeassistant.util.logging import HideSensitiveDataFilter from .auth import setup_auth from .ban import setup_bans -from .const import ( # noqa - KEY_AUTHENTICATED, - KEY_HASS, - KEY_HASS_USER, - KEY_REAL_IP, -) +from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER, KEY_REAL_IP # noqa from .cors import setup_cors from .real_ip import setup_real_ip from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa -DOMAIN = 'http' +DOMAIN = "http" -CONF_API_PASSWORD = 'api_password' -CONF_SERVER_HOST = 'server_host' -CONF_SERVER_PORT = 'server_port' -CONF_BASE_URL = 'base_url' -CONF_SSL_CERTIFICATE = 'ssl_certificate' -CONF_SSL_PEER_CERTIFICATE = 'ssl_peer_certificate' -CONF_SSL_KEY = 'ssl_key' -CONF_CORS_ORIGINS = 'cors_allowed_origins' -CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' -CONF_TRUSTED_PROXIES = 'trusted_proxies' -CONF_TRUSTED_NETWORKS = 'trusted_networks' -CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' -CONF_IP_BAN_ENABLED = 'ip_ban_enabled' -CONF_SSL_PROFILE = 'ssl_profile' +CONF_API_PASSWORD = "api_password" +CONF_SERVER_HOST = "server_host" +CONF_SERVER_PORT = "server_port" +CONF_BASE_URL = "base_url" +CONF_SSL_CERTIFICATE = "ssl_certificate" +CONF_SSL_PEER_CERTIFICATE = "ssl_peer_certificate" +CONF_SSL_KEY = "ssl_key" +CONF_CORS_ORIGINS = "cors_allowed_origins" +CONF_USE_X_FORWARDED_FOR = "use_x_forwarded_for" +CONF_TRUSTED_PROXIES = "trusted_proxies" +CONF_TRUSTED_NETWORKS = "trusted_networks" +CONF_LOGIN_ATTEMPTS_THRESHOLD = "login_attempts_threshold" +CONF_IP_BAN_ENABLED = "ip_ban_enabled" +CONF_SSL_PROFILE = "ssl_profile" -SSL_MODERN = 'modern' -SSL_INTERMEDIATE = 'intermediate' +SSL_MODERN = "modern" +SSL_INTERMEDIATE = "intermediate" _LOGGER = logging.getLogger(__name__) -DEFAULT_SERVER_HOST = '0.0.0.0' -DEFAULT_DEVELOPMENT = '0' +DEFAULT_SERVER_HOST = "0.0.0.0" +DEFAULT_DEVELOPMENT = "0" NO_LOGIN_ATTEMPT_THRESHOLD = -1 @@ -65,7 +63,8 @@ def trusted_networks_deprecated(value): "Configuring trusted_networks via the http integration has been" " deprecated. Use the trusted networks auth provider instead." " For instructions, see https://www.home-assistant.io/docs/" - "authentication/providers/#trusted-networks") + "authentication/providers/#trusted-networks" + ) return value @@ -78,49 +77,54 @@ def api_password_deprecated(value): "Configuring api_password via the http integration has been" " deprecated. Use the legacy api password auth provider instead." " For instructions, see https://www.home-assistant.io/docs/" - "authentication/providers/#legacy-api-password") + "authentication/providers/#legacy-api-password" + ) return value -HTTP_SCHEMA = vol.Schema({ - vol.Optional(CONF_API_PASSWORD): - vol.All(cv.string, api_password_deprecated), - vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, - vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, - vol.Optional(CONF_BASE_URL): cv.string, - vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, - vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, - vol.Optional(CONF_SSL_KEY): cv.isfile, - vol.Optional(CONF_CORS_ORIGINS, default=[]): - vol.All(cv.ensure_list, [cv.string]), - vol.Inclusive(CONF_USE_X_FORWARDED_FOR, 'proxy'): cv.boolean, - vol.Inclusive(CONF_TRUSTED_PROXIES, 'proxy'): - vol.All(cv.ensure_list, [ip_network]), - vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): - vol.All(cv.ensure_list, [ip_network], trusted_networks_deprecated), - vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, - default=NO_LOGIN_ATTEMPT_THRESHOLD): - vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), - vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean, - vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): - vol.In([SSL_INTERMEDIATE, SSL_MODERN]), -}) +HTTP_SCHEMA = vol.Schema( + { + vol.Optional(CONF_API_PASSWORD): vol.All(cv.string, api_password_deprecated), + vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, + vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, + vol.Optional(CONF_BASE_URL): cv.string, + vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, + vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, + vol.Optional(CONF_SSL_KEY): cv.isfile, + vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Inclusive(CONF_USE_X_FORWARDED_FOR, "proxy"): cv.boolean, + vol.Inclusive(CONF_TRUSTED_PROXIES, "proxy"): vol.All( + cv.ensure_list, [ip_network] + ), + vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): vol.All( + cv.ensure_list, [ip_network], trusted_networks_deprecated + ), + vol.Optional( + CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD + ): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), + vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean, + vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In( + [SSL_INTERMEDIATE, SSL_MODERN] + ), + } +) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: HTTP_SCHEMA, -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA) class ApiConfig: """Configuration settings for API server.""" - def __init__(self, host: str, port: Optional[int] = SERVER_PORT, - use_ssl: bool = False) -> None: + def __init__( + self, host: str, port: Optional[int] = SERVER_PORT, use_ssl: bool = False + ) -> None: """Initialize a new API config object.""" self.host = host self.port = port - host = host.rstrip('/') + host = host.rstrip("/") if host.startswith(("http://", "https://")): self.base_url = host elif use_ssl: @@ -129,7 +133,7 @@ class ApiConfig: self.base_url = "http://{}".format(host) if port is not None: - self.base_url += ':{}'.format(port) + self.base_url += ":{}".format(port) async def async_setup(hass, config): @@ -153,8 +157,9 @@ async def async_setup(hass, config): ssl_profile = conf[CONF_SSL_PROFILE] if api_password is not None: - logging.getLogger('aiohttp.access').addFilter( - HideSensitiveDataFilter(api_password)) + logging.getLogger("aiohttp.access").addFilter( + HideSensitiveDataFilter(api_password) + ) server = HomeAssistantHTTP( hass, @@ -203,11 +208,21 @@ async def async_setup(hass, config): class HomeAssistantHTTP: """HTTP server for Home Assistant.""" - def __init__(self, hass, - ssl_certificate, ssl_peer_certificate, - ssl_key, server_host, server_port, cors_origins, - use_x_forwarded_for, trusted_proxies, - login_threshold, is_ban_enabled, ssl_profile): + def __init__( + self, + hass, + ssl_certificate, + ssl_peer_certificate, + ssl_key, + server_host, + server_port, + cors_origins, + use_x_forwarded_for, + trusted_proxies, + login_threshold, + is_ban_enabled, + ssl_profile, + ): """Initialize the HTTP Home Assistant server.""" app = self.app = web.Application(middlewares=[]) app[KEY_HASS] = hass @@ -246,13 +261,13 @@ class HomeAssistantHTTP: # Instantiate the view, if needed view = view() - if not hasattr(view, 'url'): + if not hasattr(view, "url"): class_name = view.__class__.__name__ raise AttributeError( '{0} missing required attribute "url"'.format(class_name) ) - if not hasattr(view, 'name'): + if not hasattr(view, "name"): class_name = view.__class__.__name__ raise AttributeError( '{0} missing required attribute "name"'.format(class_name) @@ -269,11 +284,12 @@ class HomeAssistantHTTP: for the redirect, otherwise it has to be a string with placeholders in rule syntax. """ - def redirect(request): + + async def redirect(request): """Redirect to location.""" raise HTTPMovedPermanently(redirect_to) - self.app.router.add_route('GET', url, redirect) + self.app.router.add_route("GET", url, redirect) def register_static_path(self, url_path, path, cache_headers=True): """Register a folder or file to serve as a static path.""" @@ -286,15 +302,18 @@ class HomeAssistantHTTP: return if cache_headers: + async def serve_file(request): """Serve file from disk.""" return web.FileResponse(path, headers=CACHE_HEADERS) + else: + async def serve_file(request): """Serve file from disk.""" return web.FileResponse(path) - self.app.router.add_route('GET', url_path, serve_file) + self.app.router.add_route("GET", url_path, serve_file) async def start(self): """Start the aiohttp server.""" @@ -305,18 +324,21 @@ class HomeAssistantHTTP: else: context = ssl_util.server_context_modern() await self.hass.async_add_executor_job( - context.load_cert_chain, self.ssl_certificate, - self.ssl_key) + context.load_cert_chain, self.ssl_certificate, self.ssl_key + ) except OSError as error: - _LOGGER.error("Could not read SSL certificate from %s: %s", - self.ssl_certificate, error) + _LOGGER.error( + "Could not read SSL certificate from %s: %s", + self.ssl_certificate, + error, + ) return if self.ssl_peer_certificate: context.verify_mode = ssl.CERT_REQUIRED await self.hass.async_add_executor_job( - context.load_verify_locations, - self.ssl_peer_certificate) + context.load_verify_locations, self.ssl_peer_certificate + ) else: context = None @@ -330,13 +352,15 @@ class HomeAssistantHTTP: self.runner = web.AppRunner(self.app) await self.runner.setup() - self.site = web.TCPSite(self.runner, self.server_host, - self.server_port, ssl_context=context) + self.site = web.TCPSite( + self.runner, self.server_host, self.server_port, ssl_context=context + ) try: await self.site.start() except OSError as error: - _LOGGER.error("Failed to create HTTP server at port %d: %s", - self.server_port, error) + _LOGGER.error( + "Failed to create HTTP server at port %d: %s", self.server_port, error + ) async def stop(self): """Stop the aiohttp server.""" diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 0d8e327e086..c65cb6a2e94 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -12,17 +12,13 @@ from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.core import callback from homeassistant.util import dt as dt_util -from .const import ( - KEY_AUTHENTICATED, - KEY_HASS_USER, - KEY_REAL_IP, -) +from .const import KEY_AUTHENTICATED, KEY_HASS_USER, KEY_REAL_IP _LOGGER = logging.getLogger(__name__) -DATA_API_PASSWORD = 'api_password' -DATA_SIGN_SECRET = 'http.auth.sign_secret' -SIGN_QUERY_PARAM = 'authSig' +DATA_API_PASSWORD = "api_password" +DATA_SIGN_SECRET = "http.auth.sign_secret" +SIGN_QUERY_PARAM = "authSig" @callback @@ -34,12 +30,20 @@ def async_sign_path(hass, refresh_token_id, path, expiration): secret = hass.data[DATA_SIGN_SECRET] = generate_secret() now = dt_util.utcnow() - return "{}?{}={}".format(path, SIGN_QUERY_PARAM, jwt.encode({ - 'iss': refresh_token_id, - 'path': path, - 'iat': now, - 'exp': now + expiration, - }, secret, algorithm='HS256').decode()) + return "{}?{}={}".format( + path, + SIGN_QUERY_PARAM, + jwt.encode( + { + "iss": refresh_token_id, + "path": path, + "iat": now, + "exp": now + expiration, + }, + secret, + algorithm="HS256", + ).decode(), + ) @callback @@ -53,7 +57,7 @@ def setup_auth(hass, app): trusted_networks = [] for prv in hass.auth.auth_providers: - if prv.type == 'trusted_networks': + if prv.type == "trusted_networks": trusted_networks += prv.trusted_networks async def async_validate_auth_header(request): @@ -63,42 +67,41 @@ def setup_auth(hass, app): Basic auth_type is legacy code, should be removed with api_password. """ try: - auth_type, auth_val = \ - request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) + auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(" ", 1) except ValueError: # If no space in authorization header return False - if auth_type == 'Bearer': - refresh_token = await hass.auth.async_validate_access_token( - auth_val) + if auth_type == "Bearer": + refresh_token = await hass.auth.async_validate_access_token(auth_val) if refresh_token is None: return False request[KEY_HASS_USER] = refresh_token.user return True - if auth_type == 'Basic' and support_legacy: - decoded = base64.b64decode(auth_val).decode('utf-8') + if auth_type == "Basic" and support_legacy: + decoded = base64.b64decode(auth_val).decode("utf-8") try: - username, password = decoded.split(':', 1) + username, password = decoded.split(":", 1) except ValueError: # If no ':' in decoded return False - if username != 'homeassistant': + if username != "homeassistant": return False - user = await legacy_api_password.async_validate_password( - hass, password) + user = await legacy_api_password.async_validate_password(hass, password) if user is None: return False request[KEY_HASS_USER] = user _LOGGER.info( - 'Basic auth with api_password is going to deprecate,' - ' please use a bearer token to access %s from %s', - request.path, request[KEY_REAL_IP]) + "Basic auth with api_password is going to deprecate," + " please use a bearer token to access %s from %s", + request.path, + request[KEY_REAL_IP], + ) old_auth_warning.add(request.path) return True @@ -118,18 +121,15 @@ def setup_auth(hass, app): try: claims = jwt.decode( - signature, - secret, - algorithms=['HS256'], - options={'verify_iss': False} + signature, secret, algorithms=["HS256"], options={"verify_iss": False} ) except jwt.InvalidTokenError: return False - if claims['path'] != request.path: + if claims["path"] != request.path: return False - refresh_token = await hass.auth.async_get_refresh_token(claims['iss']) + refresh_token = await hass.auth.async_get_refresh_token(claims["iss"]) if refresh_token is None: return False @@ -141,8 +141,7 @@ def setup_auth(hass, app): """Test if request is from a trusted ip.""" ip_addr = request[KEY_REAL_IP] - if not any(ip_addr in trusted_network - for trusted_network in trusted_networks): + if not any(ip_addr in trusted_network for trusted_network in trusted_networks): return False user = await hass.auth.async_get_owner() @@ -154,8 +153,7 @@ def setup_auth(hass, app): async def async_validate_legacy_api_password(request, password): """Validate api_password.""" - user = await legacy_api_password.async_validate_password( - hass, password) + user = await legacy_api_password.async_validate_password(hass, password) if user is None: return False @@ -167,49 +165,63 @@ def setup_auth(hass, app): """Authenticate as middleware.""" authenticated = False - if (HTTP_HEADER_HA_AUTH in request.headers or - DATA_API_PASSWORD in request.query): + if HTTP_HEADER_HA_AUTH in request.headers or DATA_API_PASSWORD in request.query: if request.path not in old_auth_warning: _LOGGER.log( logging.INFO if support_legacy else logging.WARNING, - 'api_password is going to deprecate. You need to use a' - ' bearer token to access %s from %s', - request.path, request[KEY_REAL_IP]) + "api_password is going to deprecate. You need to use a" + " bearer token to access %s from %s", + request.path, + request[KEY_REAL_IP], + ) old_auth_warning.add(request.path) - if (hdrs.AUTHORIZATION in request.headers and - await async_validate_auth_header(request)): + if hdrs.AUTHORIZATION in request.headers and await async_validate_auth_header( + request + ): # it included both use_auth and api_password Basic auth authenticated = True # We first start with a string check to avoid parsing query params # for every request. - elif (request.method == "GET" and SIGN_QUERY_PARAM in request.query and - await async_validate_signed_request(request)): + elif ( + request.method == "GET" + and SIGN_QUERY_PARAM in request.query + and await async_validate_signed_request(request) + ): authenticated = True - elif (trusted_networks and - await async_validate_trusted_networks(request)): + elif trusted_networks and await async_validate_trusted_networks(request): if request.path not in old_auth_warning: # When removing this, don't forget to remove the print logic # in http/view.py - request['deprecate_warning_message'] = \ - 'Access from trusted networks without auth token is ' \ - 'going to be removed in Home Assistant 0.96. Configure ' \ - 'the trusted networks auth provider or use long-lived ' \ - 'access tokens to access {} from {}'.format( - request.path, request[KEY_REAL_IP]) + request["deprecate_warning_message"] = ( + "Access from trusted networks without auth token is " + "going to be removed in Home Assistant 0.96. Configure " + "the trusted networks auth provider or use long-lived " + "access tokens to access {} from {}".format( + request.path, request[KEY_REAL_IP] + ) + ) old_auth_warning.add(request.path) authenticated = True - elif (support_legacy and HTTP_HEADER_HA_AUTH in request.headers and - await async_validate_legacy_api_password( - request, request.headers[HTTP_HEADER_HA_AUTH])): + elif ( + support_legacy + and HTTP_HEADER_HA_AUTH in request.headers + and await async_validate_legacy_api_password( + request, request.headers[HTTP_HEADER_HA_AUTH] + ) + ): authenticated = True - elif (support_legacy and DATA_API_PASSWORD in request.query and - await async_validate_legacy_api_password( - request, request.query[DATA_API_PASSWORD])): + elif ( + support_legacy + and DATA_API_PASSWORD in request.query + and await async_validate_legacy_api_password( + request, request.query[DATA_API_PASSWORD] + ) + ): authenticated = True request[KEY_AUTHENTICATED] = authenticated diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 1cb610e71a6..db8d2ade959 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -18,19 +18,19 @@ from .const import KEY_REAL_IP _LOGGER = logging.getLogger(__name__) -KEY_BANNED_IPS = 'ha_banned_ips' -KEY_FAILED_LOGIN_ATTEMPTS = 'ha_failed_login_attempts' -KEY_LOGIN_THRESHOLD = 'ha_login_threshold' +KEY_BANNED_IPS = "ha_banned_ips" +KEY_FAILED_LOGIN_ATTEMPTS = "ha_failed_login_attempts" +KEY_LOGIN_THRESHOLD = "ha_login_threshold" -NOTIFICATION_ID_BAN = 'ip-ban' -NOTIFICATION_ID_LOGIN = 'http-login' +NOTIFICATION_ID_BAN = "ip-ban" +NOTIFICATION_ID_LOGIN = "http-login" -IP_BANS_FILE = 'ip_bans.yaml' -ATTR_BANNED_AT = 'banned_at' +IP_BANS_FILE = "ip_bans.yaml" +ATTR_BANNED_AT = "banned_at" -SCHEMA_IP_BAN_ENTRY = vol.Schema({ - vol.Optional('banned_at'): vol.Any(None, cv.datetime) -}) +SCHEMA_IP_BAN_ENTRY = vol.Schema( + {vol.Optional("banned_at"): vol.Any(None, cv.datetime)} +) @callback @@ -43,7 +43,8 @@ def setup_bans(hass, app, login_threshold): async def ban_startup(app): """Initialize bans when app starts up.""" app[KEY_BANNED_IPS] = await async_load_ip_bans_config( - hass, hass.config.path(IP_BANS_FILE)) + hass, hass.config.path(IP_BANS_FILE) + ) app.on_startup.append(ban_startup) @@ -57,8 +58,9 @@ async def ban_middleware(request, handler): # Verify if IP is not banned ip_address_ = request[KEY_REAL_IP] - is_banned = any(ip_ban.ip_address == ip_address_ - for ip_ban in request.app[KEY_BANNED_IPS]) + is_banned = any( + ip_ban.ip_address == ip_address_ for ip_ban in request.app[KEY_BANNED_IPS] + ) if is_banned: raise HTTPForbidden() @@ -72,12 +74,14 @@ async def ban_middleware(request, handler): def log_invalid_auth(func): """Decorate function to handle invalid auth or failed login attempts.""" + async def handle_req(view, request, *args, **kwargs): """Try to log failed login attempts if response status >= 400.""" resp = await func(view, request, *args, **kwargs) if resp.status >= 400: await process_wrong_login(request) return resp + return handle_req @@ -89,35 +93,40 @@ async def process_wrong_login(request): """ remote_addr = request[KEY_REAL_IP] - msg = ('Login attempt or request with invalid authentication ' - 'from {}'.format(remote_addr)) + msg = "Login attempt or request with invalid authentication " "from {}".format( + remote_addr + ) _LOGGER.warning(msg) - hass = request.app['hass'] + hass = request.app["hass"] hass.components.persistent_notification.async_create( - msg, 'Login attempt failed', NOTIFICATION_ID_LOGIN) + msg, "Login attempt failed", NOTIFICATION_ID_LOGIN + ) # Check if ban middleware is loaded - if (KEY_BANNED_IPS not in request.app or - request.app[KEY_LOGIN_THRESHOLD] < 1): + if KEY_BANNED_IPS not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: return request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1 - if (request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >= - request.app[KEY_LOGIN_THRESHOLD]): + if ( + request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] + >= request.app[KEY_LOGIN_THRESHOLD] + ): new_ban = IpBan(remote_addr) request.app[KEY_BANNED_IPS].append(new_ban) await hass.async_add_job( - update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban) + update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban + ) - _LOGGER.warning( - "Banned IP %s for too many login attempts", remote_addr) + _LOGGER.warning("Banned IP %s for too many login attempts", remote_addr) hass.components.persistent_notification.async_create( - 'Too many login attempts from {}'.format(remote_addr), - 'Banning IP address', NOTIFICATION_ID_BAN) + "Too many login attempts from {}".format(remote_addr), + "Banning IP address", + NOTIFICATION_ID_BAN, + ) async def process_success_login(request): @@ -130,14 +139,16 @@ async def process_success_login(request): remote_addr = request[KEY_REAL_IP] # Check if ban middleware is loaded - if (KEY_BANNED_IPS not in request.app or - request.app[KEY_LOGIN_THRESHOLD] < 1): + if KEY_BANNED_IPS not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: return - if remote_addr in request.app[KEY_FAILED_LOGIN_ATTEMPTS] and \ - request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] > 0: - _LOGGER.debug('Login success, reset failed login attempts counter' - ' from %s', remote_addr) + if ( + remote_addr in request.app[KEY_FAILED_LOGIN_ATTEMPTS] + and request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] > 0 + ): + _LOGGER.debug( + "Login success, reset failed login attempts counter" " from %s", remote_addr + ) request.app[KEY_FAILED_LOGIN_ATTEMPTS].pop(remote_addr) @@ -159,13 +170,13 @@ async def async_load_ip_bans_config(hass: HomeAssistant, path: str): except FileNotFoundError: return ip_list except HomeAssistantError as err: - _LOGGER.error('Unable to load %s: %s', path, str(err)) + _LOGGER.error("Unable to load %s: %s", path, str(err)) return ip_list for ip_ban, ip_info in list_.items(): try: ip_info = SCHEMA_IP_BAN_ENTRY(ip_info) - ip_list.append(IpBan(ip_ban, ip_info['banned_at'])) + ip_list.append(IpBan(ip_ban, ip_info["banned_at"])) except vol.Invalid as err: _LOGGER.error("Failed to load IP ban %s: %s", ip_info, err) continue @@ -175,9 +186,11 @@ async def async_load_ip_bans_config(hass: HomeAssistant, path: str): def update_ip_bans_config(path: str, ip_ban: IpBan): """Update config file with new banned IP address.""" - with open(path, 'a') as out: - ip_ = {str(ip_ban.ip_address): { - ATTR_BANNED_AT: ip_ban.banned_at.strftime("%Y-%m-%dT%H:%M:%S") - }} - out.write('\n') + with open(path, "a") as out: + ip_ = { + str(ip_ban.ip_address): { + ATTR_BANNED_AT: ip_ban.banned_at.strftime("%Y-%m-%dT%H:%M:%S") + } + } + out.write("\n") out.write(dump(ip_)) diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index f26220e63d1..9392e790d62 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -1,5 +1,5 @@ """HTTP specific constants.""" -KEY_AUTHENTICATED = 'ha_authenticated' -KEY_HASS = 'hass' -KEY_HASS_USER = 'hass_user' -KEY_REAL_IP = 'ha_real_ip' +KEY_AUTHENTICATED = "ha_authenticated" +KEY_HASS = "hass" +KEY_HASS_USER = "hass_user" +KEY_REAL_IP = "ha_real_ip" diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 419b62be2c6..5c24ecbebed 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,15 +1,19 @@ """Provide CORS support for the HTTP component.""" -from aiohttp.web_urldispatcher import Resource, ResourceRoute +from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION -from homeassistant.const import ( - HTTP_HEADER_HA_AUTH, HTTP_HEADER_X_REQUESTED_WITH) +from homeassistant.const import HTTP_HEADER_HA_AUTH, HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback ALLOWED_CORS_HEADERS = [ - ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE, - HTTP_HEADER_HA_AUTH, AUTHORIZATION] -VALID_CORS_TYPES = (Resource, ResourceRoute) + ORIGIN, + ACCEPT, + HTTP_HEADER_X_REQUESTED_WITH, + CONTENT_TYPE, + HTTP_HEADER_HA_AUTH, + AUTHORIZATION, +] +VALID_CORS_TYPES = (Resource, ResourceRoute, StaticResource) @callback @@ -17,18 +21,21 @@ def setup_cors(app, origins): """Set up CORS.""" import aiohttp_cors - cors = aiohttp_cors.setup(app, defaults={ - host: aiohttp_cors.ResourceOptions( - allow_headers=ALLOWED_CORS_HEADERS, - allow_methods='*', - ) for host in origins - }) + cors = aiohttp_cors.setup( + app, + defaults={ + host: aiohttp_cors.ResourceOptions( + allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*" + ) + for host in origins + }, + ) cors_added = set() def _allow_cors(route, config=None): """Allow CORS on a route.""" - if hasattr(route, 'resource'): + if hasattr(route, "resource"): path = route.resource else: path = route @@ -44,19 +51,21 @@ def setup_cors(app, origins): cors.add(route, config) cors_added.add(path) - app['allow_cors'] = lambda route: _allow_cors(route, { - '*': aiohttp_cors.ResourceOptions( - allow_headers=ALLOWED_CORS_HEADERS, - allow_methods='*', - ) - }) + app["allow_cors"] = lambda route: _allow_cors( + route, + { + "*": aiohttp_cors.ResourceOptions( + allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*" + ) + }, + ) if not origins: return async def cors_startup(app): """Initialize CORS when app starts up.""" - for route in list(app.router.routes()): - _allow_cors(route) + for resource in list(app.router.resources()): + _allow_cors(resource) app.on_startup.append(cors_startup) diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 98686e5cabd..8d6ac0b1ceb 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -23,6 +23,7 @@ class RequestDataValidator: def __call__(self, method): """Decorate a function.""" + @wraps(method) async def wrapper(view, request, *args, **kwargs): """Wrap a request handler with data validation.""" @@ -30,18 +31,18 @@ class RequestDataValidator: try: data = await request.json() except ValueError: - if not self._allow_empty or \ - (await request.content.read()) != b'': - _LOGGER.error('Invalid JSON received.') - return view.json_message('Invalid JSON.', 400) + if not self._allow_empty or (await request.content.read()) != b"": + _LOGGER.error("Invalid JSON received.") + return view.json_message("Invalid JSON.", 400) data = {} try: - kwargs['data'] = self._schema(data) + kwargs["data"] = self._schema(data) except vol.Invalid as err: - _LOGGER.error('Data does not match schema: %s', err) + _LOGGER.error("Data does not match schema: %s", err) return view.json_message( - 'Message format incorrect: {}'.format(err), 400) + "Message format incorrect: {}".format(err), 400 + ) result = await method(view, request, *args, **kwargs) return result diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index 9bbf30bd9d1..c38e5d0b592 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -12,21 +12,25 @@ from .const import KEY_REAL_IP @callback def setup_real_ip(app, use_x_forwarded_for, trusted_proxies): """Create IP Ban middleware for the app.""" + @middleware async def real_ip_middleware(request, handler): """Real IP middleware.""" - connected_ip = ip_address( - request.transport.get_extra_info('peername')[0]) + connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) request[KEY_REAL_IP] = connected_ip # Only use the XFF header if enabled, present, and from a trusted proxy try: - if (use_x_forwarded_for and - X_FORWARDED_FOR in request.headers and - any(connected_ip in trusted_proxy - for trusted_proxy in trusted_proxies)): + if ( + use_x_forwarded_for + and X_FORWARDED_FOR in request.headers + and any( + connected_ip in trusted_proxy for trusted_proxy in trusted_proxies + ) + ): request[KEY_REAL_IP] = ip_address( - request.headers.get(X_FORWARDED_FOR).split(', ')[-1]) + request.headers.get(X_FORWARDED_FOR).split(", ")[-1] + ) except ValueError: pass diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 4fac9bf1ae9..f78ce81d884 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -16,7 +16,7 @@ class CachingStaticResource(StaticResource): """Static Resource handler that will add cache headers.""" async def _handle(self, request): - rel_url = request.match_info['filename'] + rel_url = request.match_info["filename"] try: filename = Path(rel_url) if filename.anchor: @@ -40,5 +40,6 @@ class CachingStaticResource(StaticResource): return await super()._handle(request) if filepath.is_file(): return FileResponse( - filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS) + filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS + ) raise HTTPNotFound diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index ea9ca6ac31f..35e74b7c2c0 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -5,7 +5,10 @@ import logging from aiohttp import web from aiohttp.web_exceptions import ( - HTTPBadRequest, HTTPInternalServerError, HTTPUnauthorized) + HTTPBadRequest, + HTTPInternalServerError, + HTTPUnauthorized, +) import voluptuous as vol from homeassistant import exceptions @@ -31,7 +34,7 @@ class HomeAssistantView: # pylint: disable=no-self-use def context(self, request): """Generate a context from a request.""" - user = request.get('hass_user') + user = request.get("hass_user") if user is None: return Context() @@ -42,32 +45,33 @@ class HomeAssistantView: try: msg = json.dumps( result, sort_keys=True, cls=JSONEncoder, allow_nan=False - ).encode('UTF-8') + ).encode("UTF-8") except (ValueError, TypeError) as err: - _LOGGER.error('Unable to serialize to JSON: %s\n%s', err, result) + _LOGGER.error("Unable to serialize to JSON: %s\n%s", err, result) raise HTTPInternalServerError response = web.Response( - body=msg, content_type=CONTENT_TYPE_JSON, status=status_code, - headers=headers) + body=msg, + content_type=CONTENT_TYPE_JSON, + status=status_code, + headers=headers, + ) response.enable_compression() return response - def json_message(self, message, status_code=200, message_code=None, - headers=None): + def json_message(self, message, status_code=200, message_code=None, headers=None): """Return a JSON message response.""" - data = {'message': message} + data = {"message": message} if message_code is not None: - data['code'] = message_code + data["code"] = message_code return self.json(data, status_code, headers=headers) def register(self, app, router): """Register the view with a router.""" - assert self.url is not None, 'No url set for view' + assert self.url is not None, "No url set for view" urls = [self.url] + self.extra_urls routes = [] - for method in ('get', 'post', 'delete', 'put', 'patch', 'head', - 'options'): + for method in ("get", "post", "delete", "put", "patch", "head", "options"): handler = getattr(self, method, None) if not handler: @@ -82,13 +86,14 @@ class HomeAssistantView: return for route in routes: - app['allow_cors'](route) + app["allow_cors"](route) def request_handler_factory(view, handler): """Wrap the handler classes.""" - assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \ - "Handler should be a coroutine or a callback." + assert asyncio.iscoroutinefunction(handler) or is_callback( + handler + ), "Handler should be a coroutine or a callback." async def handle(request): """Handle incoming request.""" @@ -99,14 +104,18 @@ def request_handler_factory(view, handler): if view.requires_auth: if authenticated: - if 'deprecate_warning_message' in request: - _LOGGER.warning(request['deprecate_warning_message']) + if "deprecate_warning_message" in request: + _LOGGER.warning(request["deprecate_warning_message"]) await process_success_login(request) else: raise HTTPUnauthorized() - _LOGGER.debug('Serving %s to %s (auth: %s)', - request.path, request.get(KEY_REAL_IP), authenticated) + _LOGGER.debug( + "Serving %s to %s (auth: %s)", + request.path, + request.get(KEY_REAL_IP), + authenticated, + ) try: result = handler(request, **request.match_info) @@ -130,12 +139,13 @@ def request_handler_factory(view, handler): result, status_code = result if isinstance(result, str): - result = result.encode('utf-8') + result = result.encode("utf-8") elif result is None: - result = b'' + result = b"" elif not isinstance(result, bytes): - assert False, ('Result should be None, string, bytes or Response. ' - 'Got: {}').format(result) + assert False, ( + "Result should be None, string, bytes or Response. " "Got: {}" + ).format(result) return web.Response(body=result, status=status_code) diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index 01c2b0399b9..c2223720eb5 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -14,24 +14,25 @@ from homeassistant.util.temperature import celsius_to_fahrenheit _LOGGER = logging.getLogger(__name__) -CONF_I2C_BUS = 'i2c_bus' +CONF_I2C_BUS = "i2c_bus" DEFAULT_I2C_BUS = 1 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -DEFAULT_NAME = 'HTU21D Sensor' +DEFAULT_NAME = "HTU21D Sensor" -SENSOR_TEMPERATURE = 'temperature' -SENSOR_HUMIDITY = 'humidity' +SENSOR_TEMPERATURE = "temperature" +SENSOR_HUMIDITY = "humidity" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), + } +) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HTU21D sensor.""" import smbus # pylint: disable=import-error from i2csense.htu21d import HTU21D # pylint: disable=import-error @@ -41,17 +42,17 @@ async def async_setup_platform(hass, config, async_add_entities, temp_unit = hass.config.units.temperature_unit bus = smbus.SMBus(config.get(CONF_I2C_BUS)) - sensor = await hass.async_add_job( - partial(HTU21D, bus, logger=_LOGGER) - ) + sensor = await hass.async_add_job(partial(HTU21D, bus, logger=_LOGGER)) if not sensor.sample_ok: _LOGGER.error("HTU21D sensor not detected in bus %s", bus_number) return False sensor_handler = await hass.async_add_job(HTU21DHandler, sensor) - dev = [HTU21DSensor(sensor_handler, name, SENSOR_TEMPERATURE, temp_unit), - HTU21DSensor(sensor_handler, name, SENSOR_HUMIDITY, '%')] + dev = [ + HTU21DSensor(sensor_handler, name, SENSOR_TEMPERATURE, temp_unit), + HTU21DSensor(sensor_handler, name, SENSOR_HUMIDITY, "%"), + ] async_add_entities(dev) @@ -75,7 +76,7 @@ class HTU21DSensor(Entity): def __init__(self, htu21d_client, name, variable, unit): """Initialize the sensor.""" - self._name = '{}_{}'.format(name, variable) + self._name = "{}_{}".format(name, variable) self._variable = variable self._unit_of_measurement = unit self._client = htu21d_client diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 8e401dfd239..51d0dc5d3a2 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -1,14 +1,24 @@ """Support for Huawei LTE routers.""" from datetime import timedelta from functools import reduce +from urllib.parse import urlparse +import ipaddress import logging import operator +from typing import Any, Callable import voluptuous as vol import attr +from getmac import get_mac_address +from huawei_lte_api.AuthorizedConnection import AuthorizedConnection +from huawei_lte_api.Client import Client +from huawei_lte_api.exceptions import ResponseErrorNotSupportedException from homeassistant.const import ( - CONF_URL, CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, + CONF_URL, + CONF_USERNAME, + CONF_PASSWORD, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle @@ -17,20 +27,30 @@ _LOGGER = logging.getLogger(__name__) # dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. # https://github.com/quandyfactory/dicttoxml/issues/60 -logging.getLogger('dicttoxml').setLevel(logging.WARNING) +logging.getLogger("dicttoxml").setLevel(logging.WARNING) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) -DOMAIN = 'huawei_lte' -DATA_KEY = 'huawei_lte' +DOMAIN = "huawei_lte" +DATA_KEY = "huawei_lte" -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ - vol.Required(CONF_URL): cv.url, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - })]) -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_URL): cv.url, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) @attr.s @@ -38,19 +58,14 @@ class RouterData: """Class for router state.""" client = attr.ib() + mac = attr.ib() device_information = attr.ib(init=False, factory=dict) device_signal = attr.ib(init=False, factory=dict) - traffic_statistics = attr.ib(init=False, factory=dict) + monitoring_traffic_statistics = attr.ib(init=False, factory=dict) wlan_host_list = attr.ib(init=False, factory=dict) _subscriptions = attr.ib(init=False, factory=set) - def __attrs_post_init__(self) -> None: - """Fetch device information once, for serial number in @unique_ids.""" - self.subscribe("device_information") - self._update() - self.unsubscribe("device_information") - def __getitem__(self, path: str): """ Get value corresponding to a dotted path. @@ -81,19 +96,23 @@ class RouterData: def _update(self) -> None: debugging = _LOGGER.isEnabledFor(logging.DEBUG) - if debugging or "device_information" in self._subscriptions: - self.device_information = self.client.device.information() - _LOGGER.debug("device_information=%s", self.device_information) - if debugging or "device_signal" in self._subscriptions: - self.device_signal = self.client.device.signal() - _LOGGER.debug("device_signal=%s", self.device_signal) - if debugging or "traffic_statistics" in self._subscriptions: - self.traffic_statistics = \ - self.client.monitoring.traffic_statistics() - _LOGGER.debug("traffic_statistics=%s", self.traffic_statistics) - if debugging or "wlan_host_list" in self._subscriptions: - self.wlan_host_list = self.client.wlan.host_list() - _LOGGER.debug("wlan_host_list=%s", self.wlan_host_list) + + def get_data(path: str, func: Callable[[None], Any]) -> None: + if debugging or path in self._subscriptions: + try: + setattr(self, path, func()) + except ResponseErrorNotSupportedException as ex: + _LOGGER.warning("%s not supported by device", path, exc_info=ex) + self._subscriptions.discard(path) + finally: + _LOGGER.debug("%s=%s", path, getattr(self, path)) + + get_data("device_information", self.client.device.information) + get_data("device_signal", self.client.device.signal) + get_data( + "monitoring_traffic_statistics", self.client.monitoring.traffic_statistics + ) + get_data("wlan_host_list", self.client.wlan.host_list) @attr.s @@ -123,21 +142,28 @@ def setup(hass, config) -> bool: def _setup_lte(hass, lte_config) -> None: """Set up Huawei LTE router.""" - from huawei_lte_api.AuthorizedConnection import AuthorizedConnection - from huawei_lte_api.Client import Client - url = lte_config[CONF_URL] username = lte_config[CONF_USERNAME] password = lte_config[CONF_PASSWORD] - connection = AuthorizedConnection( - url, - username=username, - password=password, - ) + # Get MAC address for use in unique ids. Being able to use something + # from the API would be nice, but all of that seems to be available only + # through authenticated calls (e.g. device_information.SerialNumber), and + # we want this available and the same when unauthenticated too. + host = urlparse(url).hostname + try: + if ipaddress.ip_address(host).version == 6: + mode = "ip6" + else: + mode = "ip" + except ValueError: + mode = "hostname" + mac = get_mac_address(**{mode: host}) + + connection = AuthorizedConnection(url, username=username, password=password) client = Client(connection) - data = RouterData(client) + data = RouterData(client, mac) hass.data[DATA_KEY].data[url] = data def cleanup(event): diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 552bfb90703..878a819aaae 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -5,15 +5,11 @@ import attr import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA, DeviceScanner, -) +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, DeviceScanner from homeassistant.const import CONF_URL from . import DATA_KEY, RouterData -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_URL): cv.url, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_URL): cv.url}) HOSTS_PATH = "wlan_host_list.Hosts" diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index bfdc6f167aa..85077511768 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -1,8 +1,9 @@ { "domain": "huawei_lte", - "name": "Huawei lte", + "name": "Huawei LTE", "documentation": "https://www.home-assistant.io/components/huawei_lte", "requirements": [ + "getmac==0.8.1", "huawei-lte-api==1.2.0" ], "dependencies": [], diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 2222c1333dd..31804f722c6 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -5,7 +5,10 @@ import voluptuous as vol import attr from homeassistant.components.notify import ( - BaseNotificationService, ATTR_TARGET, PLATFORM_SCHEMA) + BaseNotificationService, + ATTR_TARGET, + PLATFORM_SCHEMA, +) from homeassistant.const import CONF_RECIPIENT, CONF_URL import homeassistant.helpers.config_validation as cv @@ -13,10 +16,12 @@ from . import DATA_KEY _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_URL): cv.url, - vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [cv.string]), -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_URL): cv.url, + vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [cv.string]), + } +) async def async_get_service(hass, config, discovery_info=None): @@ -45,8 +50,7 @@ class HuaweiLteSmsNotificationService(BaseNotificationService): return try: - resp = data.client.sms.send_sms( - phone_numbers=targets, message=message) + resp = data.client.sms.send_sms(phone_numbers=targets, message=message) _LOGGER.debug("Sent to %s: %s", targets, resp) except ResponseErrorException as ex: _LOGGER.error("Could not send to %s: %s", targets, ex) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 5dac3c2c787..e72bd3aa438 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -1,14 +1,17 @@ """Support for Huawei LTE sensors.""" import logging import re +from typing import Optional import attr import voluptuous as vol -from homeassistant.const import ( - CONF_URL, CONF_MONITORED_CONDITIONS, STATE_UNKNOWN, +from homeassistant.const import CONF_URL, CONF_MONITORED_CONDITIONS, STATE_UNKNOWN +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + DEVICE_CLASS_SIGNAL_STRENGTH, ) -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -16,7 +19,8 @@ from . import DATA_KEY, RouterData _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME_TEMPLATE = 'Huawei {} {}' +DEFAULT_NAME_TEMPLATE = "Huawei {} {}" +DEFAULT_DEVICE_NAME = "LTE" DEFAULT_SENSORS = [ "device_information.WanIPAddress", @@ -27,92 +31,105 @@ DEFAULT_SENSORS = [ ] SENSOR_META = { - "device_information.SoftwareVersion": dict( - name="Software version", - ), - "device_information.WanIPAddress": dict( - name="WAN IP address", - icon="mdi:ip", - ), - "device_information.WanIPv6Address": dict( - name="WAN IPv6 address", - icon="mdi:ip", - ), - "device_signal.band": dict( - name="Band", - ), - "device_signal.cell_id": dict( - name="Cell ID", - ), - "device_signal.lac": dict( - name="LAC", - ), + "device_information.SoftwareVersion": dict(name="Software version"), + "device_information.WanIPAddress": dict(name="WAN IP address", icon="mdi:ip"), + "device_information.WanIPv6Address": dict(name="WAN IPv6 address", icon="mdi:ip"), + "device_signal.band": dict(name="Band"), + "device_signal.cell_id": dict(name="Cell ID"), + "device_signal.lac": dict(name="LAC"), "device_signal.mode": dict( name="Mode", - formatter=lambda x: ({ - '0': '2G', - '2': '3G', - '7': '4G', - }.get(x, 'Unknown'), None), - ), - "device_signal.pci": dict( - name="PCI", + formatter=lambda x: ({"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), None), ), + "device_signal.pci": dict(name="PCI"), "device_signal.rsrq": dict( name="RSRQ", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrq.php - icon=lambda x: - (x is None or x < -11) and "mdi:signal-cellular-outline" - or x < -8 and "mdi:signal-cellular-1" - or x < -5 and "mdi:signal-cellular-2" - or "mdi:signal-cellular-3" + icon=lambda x: (x is None or x < -11) + and "mdi:signal-cellular-outline" + or x < -8 + and "mdi:signal-cellular-1" + or x < -5 + and "mdi:signal-cellular-2" + or "mdi:signal-cellular-3", ), "device_signal.rsrp": dict( name="RSRP", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrp.php - icon=lambda x: - (x is None or x < -110) and "mdi:signal-cellular-outline" - or x < -95 and "mdi:signal-cellular-1" - or x < -80 and "mdi:signal-cellular-2" - or "mdi:signal-cellular-3" + icon=lambda x: (x is None or x < -110) + and "mdi:signal-cellular-outline" + or x < -95 + and "mdi:signal-cellular-1" + or x < -80 + and "mdi:signal-cellular-2" + or "mdi:signal-cellular-3", ), "device_signal.rssi": dict( name="RSSI", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # https://eyesaas.com/wi-fi-signal-strength/ - icon=lambda x: - (x is None or x < -80) and "mdi:signal-cellular-outline" - or x < -70 and "mdi:signal-cellular-1" - or x < -60 and "mdi:signal-cellular-2" - or "mdi:signal-cellular-3" + icon=lambda x: (x is None or x < -80) + and "mdi:signal-cellular-outline" + or x < -70 + and "mdi:signal-cellular-1" + or x < -60 + and "mdi:signal-cellular-2" + or "mdi:signal-cellular-3", ), "device_signal.sinr": dict( name="SINR", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/sinr.php - icon=lambda x: - (x is None or x < 0) and "mdi:signal-cellular-outline" - or x < 5 and "mdi:signal-cellular-1" - or x < 10 and "mdi:signal-cellular-2" - or "mdi:signal-cellular-3" + icon=lambda x: (x is None or x < 0) + and "mdi:signal-cellular-outline" + or x < 5 + and "mdi:signal-cellular-1" + or x < 10 + and "mdi:signal-cellular-2" + or "mdi:signal-cellular-3", ), } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_URL): cv.url, - vol.Optional( - CONF_MONITORED_CONDITIONS, default=DEFAULT_SENSORS): cv.ensure_list, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_URL): cv.url, + vol.Optional( + CONF_MONITORED_CONDITIONS, default=DEFAULT_SENSORS + ): cv.ensure_list, + } +) -def setup_platform( - hass, config, add_entities, discovery_info): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up Huawei LTE sensor devices.""" data = hass.data[DATA_KEY].get_data(config) sensors = [] for path in config.get(CONF_MONITORED_CONDITIONS): + if path == "traffic_statistics": # backwards compatibility + path = "monitoring_traffic_statistics" data.subscribe(path) sensors.append(HuaweiLteSensor(data, path, SENSOR_META.get(path, {}))) - add_entities(sensors, True) + # Pre-0.97 unique id migration. Old ones used the device serial number + # (see comments in HuaweiLteData._setup_lte for more info), as well as + # had a bug that joined the path str with periods, not the path components, + # resulting e.g. *_device_signal.sinr to end up as + # *_d.e.v.i.c.e._.s.i.g.n.a.l...s.i.n.r + entreg = await entity_registry.async_get_registry(hass) + for entid, ent in entreg.entities.items(): + if ent.platform != "huawei_lte": + continue + for sensor in sensors: + oldsuf = ".".join(sensor.path) + if ent.unique_id.endswith(f"_{oldsuf}"): + entreg.async_update_entity(entid, new_unique_id=sensor.unique_id) + _LOGGER.debug( + "Updated entity %s unique id to %s", entid, sensor.unique_id + ) + + async_add_entities(sensors, True) def format_default(value): @@ -120,8 +137,7 @@ def format_default(value): unit = None if value is not None: # Clean up value and infer unit, e.g. -71dBm, 15 dB - match = re.match( - r"(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value)) + match = re.match(r"(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value)) if match: try: value = float(match.group("value")) @@ -136,7 +152,7 @@ class HuaweiLteSensor(Entity): """Huawei LTE sensor entity.""" data = attr.ib(type=RouterData) - path = attr.ib(type=list) + path = attr.ib(type=str) meta = attr.ib(type=dict) _state = attr.ib(init=False, default=STATE_UNKNOWN) @@ -145,23 +161,28 @@ class HuaweiLteSensor(Entity): @property def unique_id(self) -> str: """Return unique ID for sensor.""" - return "{}_{}".format( - self.data["device_information.SerialNumber"], - ".".join(self.path), - ) + return "{}-{}".format(self.data.mac, self.path) @property def name(self) -> str: """Return sensor name.""" - dname = self.data["device_information.DeviceName"] + try: + dname = self.data["device_information.DeviceName"] + except KeyError: + dname = None vname = self.meta.get("name", self.path) - return DEFAULT_NAME_TEMPLATE.format(dname, vname) + return DEFAULT_NAME_TEMPLATE.format(dname or DEFAULT_DEVICE_NAME, vname) @property def state(self): """Return sensor state.""" return self._state + @property + def device_class(self) -> Optional[str]: + """Return sensor device class.""" + return self.meta.get("device_class") + @property def unit_of_measurement(self): """Return sensor's unit of measurement.""" diff --git a/homeassistant/components/huawei_router/device_tracker.py b/homeassistant/components/huawei_router/device_tracker.py index 88e2a57a579..08b7c9ec859 100644 --- a/homeassistant/components/huawei_router/device_tracker.py +++ b/homeassistant/components/huawei_router/device_tracker.py @@ -9,16 +9,21 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + } +) def get_scanner(hass, config): @@ -28,14 +33,14 @@ def get_scanner(hass, config): return scanner -Device = namedtuple('Device', ['name', 'ip', 'mac', 'state']) +Device = namedtuple("Device", ["name", "ip", "mac", "state"]) class HuaweiDeviceScanner(DeviceScanner): """This class queries a router running HUAWEI firmware.""" - ARRAY_REGEX = re.compile(r'var UserDevinfo = new Array\((.*)null\);') - DEVICE_REGEX = re.compile(r'new USERDevice\((.*?)\),') + ARRAY_REGEX = re.compile(r"var UserDevinfo = new Array\((.*)null\);") + DEVICE_REGEX = re.compile(r"new USERDevice\((.*?)\),") DEVICE_ATTR_REGEX = re.compile( '"(?P.*?)","(?P.*?)",' '"(?P.*?)","(?P.*?)",' @@ -43,14 +48,15 @@ class HuaweiDeviceScanner(DeviceScanner): '"(?P.*?)","(?P.*?)",' '"(?P