├── .HA_VERSION ├── .gitattributes ├── .gitignore ├── HASS Cheatsheet.md ├── HA_screenshot_1.png ├── README.md ├── alert.yaml ├── alexaIntentSchema.json ├── automation_lab.yaml ├── automations.yaml ├── binary_sensor.yaml ├── blueprints ├── automation │ └── homeassistant │ │ ├── motion_light.yaml │ │ └── notify_leaving_zone.yaml └── script │ └── homeassistant │ └── confirmable_notification.yaml ├── chromedriver.sh ├── configuration.yaml ├── cover.yaml ├── custom_components ├── aarlo │ ├── __init__.py │ ├── alarm_control_panel.py │ ├── binary_sensor.py │ ├── camera.py │ ├── light.py │ ├── manifest.json │ ├── media_player.py │ ├── pyaarlo │ │ ├── __init__.py │ │ ├── backend.py │ │ ├── background.py │ │ ├── base.py │ │ ├── camera.py │ │ ├── cfg.py │ │ ├── constant.py │ │ ├── device.py │ │ ├── doorbell.py │ │ ├── light.py │ │ ├── media.py │ │ ├── sseclient.py │ │ ├── storage.py │ │ ├── tfa.py │ │ └── util.py │ ├── sensor.py │ ├── services.yaml │ └── switch.py ├── alexa_media │ ├── .translations │ │ ├── de.json │ │ ├── en.json │ │ ├── it.json │ │ ├── nl.json │ │ └── pl.json │ ├── __init__.py │ ├── alarm_control_panel.py │ ├── alexa_entity.py │ ├── alexa_media.py │ ├── config_flow.py │ ├── const.py │ ├── helpers.py │ ├── light.py │ ├── manifest.json │ ├── media_player.py │ ├── notify.py │ ├── sensor.py │ ├── services.py │ ├── services.yaml │ ├── strings.json │ └── switch.py ├── androidtv │ ├── __init__.py │ ├── manifest.json │ ├── media_player.py │ └── services.yaml ├── authenticated │ ├── __init__.py │ ├── const.py │ ├── manifest.json │ ├── providers.py │ └── sensor.py ├── composite │ ├── __init__.py │ ├── const.py │ └── device_tracker.py ├── enable_debug.py ├── futures_cnn │ ├── __init__.py │ └── sensor.py ├── google_scholar │ ├── __init__.py │ └── sensor.py ├── hacs │ ├── __init__.py │ ├── config_flow.py │ ├── const.py │ ├── manifest.json │ ├── repositories │ │ ├── __init__.py │ │ ├── appdaemon.py │ │ ├── integration.py │ │ ├── plugin.py │ │ ├── python_script.py │ │ └── theme.py │ └── sensor.py ├── moon_here │ ├── __init__.py │ └── sensor.py ├── personalcapital │ ├── __init__.py │ ├── manifest.json │ └── sensor.py ├── samsungtv_custom │ ├── __init__.py │ ├── media_player.py │ ├── samsungctl_080b │ │ ├── RUN_ME.py │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── application.py │ │ ├── art_mode.py │ │ ├── cec_control │ │ │ ├── __init__.py │ │ │ └── dispatcher.py │ │ ├── config.py │ │ ├── exceptions.py │ │ ├── interactive.py │ │ ├── key_mappings.py │ │ ├── power_handler.py │ │ ├── remote.py │ │ ├── remote_encrypted │ │ │ ├── __init__.py │ │ │ ├── aes.py │ │ │ ├── crypto.py │ │ │ ├── keys.py │ │ │ └── rijndael │ │ │ │ ├── __init__.py │ │ │ │ ├── constants.py │ │ │ │ ├── paddings.py │ │ │ │ └── rijndael.py │ │ ├── remote_legacy.py │ │ ├── remote_websocket.py │ │ ├── upnp │ │ │ ├── UPNP_Device │ │ │ │ ├── __init__.py │ │ │ │ ├── __main__.py │ │ │ │ ├── action.py │ │ │ │ ├── adapter_addresses.py │ │ │ │ ├── data_type.py │ │ │ │ ├── discover.py │ │ │ │ ├── embedded_device.py │ │ │ │ ├── icon.py │ │ │ │ ├── instance_singleton.py │ │ │ │ ├── listen.py │ │ │ │ ├── service.py │ │ │ │ ├── upnp_class.py │ │ │ │ └── xmlns.py │ │ │ ├── __init__.py │ │ │ └── discover.py │ │ ├── utils.py │ │ ├── wake_on_lan.py │ │ └── websocket_base.py │ ├── samsungctl_qled │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── application.py │ │ ├── exceptions.py │ │ ├── interactive.py │ │ ├── remote.py │ │ ├── remote_legacy.py │ │ ├── remote_websocket.py │ │ └── upnp.py │ └── samsungtvws │ │ ├── __init__.py │ │ └── remote.py ├── samsungtv_smart │ ├── __init__.py │ ├── api │ │ ├── samsungws.py │ │ ├── shortcuts.py │ │ ├── smartthings.py │ │ └── upnp.py │ ├── config_flow.py │ ├── const.py │ ├── diagnostics.py │ ├── logo.py │ ├── media_player.py │ └── services.yaml ├── samsungtv_tizen │ ├── __init__.py │ ├── exceptions.py │ ├── media_player.py │ ├── shortcuts.py │ ├── smartthings.py │ ├── upnp.py │ └── websockets.py ├── sectorperformance │ ├── __init__.py │ └── sensor.py ├── tuya_v2 │ ├── __init__.py │ ├── aes_cbc.py │ ├── alarm_control_panel.py │ ├── base.py │ ├── binary_sensor.py │ ├── climate.py │ ├── config_flow.py │ ├── const.py │ ├── cover.py │ ├── fan.py │ ├── humidifier.py │ ├── light.py │ ├── number.py │ ├── scene.py │ ├── select.py │ ├── sensor.py │ ├── switch.py │ └── vacuum.py ├── wyzeapi │ ├── __init__.py │ ├── alarm_control_panel.py │ ├── binary_sensor.py │ ├── climate.py │ ├── config_flow.py │ ├── const.py │ ├── light.py │ ├── lock.py │ ├── sensor.py │ ├── switch.py │ └── token_manager.py ├── wyzesense │ ├── __init__.py │ ├── binary_sensor.py │ ├── manifest.json │ ├── services.yaml │ └── wyzesense_custom.py └── yahoo_earnings │ ├── __init__.py │ ├── manifest.json │ └── sensor.py ├── customize.yaml ├── customize_glob.yaml ├── emulated_hue_ids.json ├── frontend.yaml ├── gitupdate.sh ├── groups.yaml ├── ha_ss_1.png ├── ha_ss_2.png ├── ha_ss_3.png ├── ha_ss_4.png ├── ha_ss_5.png ├── history.yaml ├── input_boolean.yaml ├── input_button.yaml ├── input_datetime.yaml ├── input_number.yaml ├── input_select.yaml ├── input_text.yaml ├── intent_script.yaml ├── ios.yaml ├── ip_bans.yaml ├── logbook.yaml ├── lovelace.yaml ├── packages ├── homebridge.yaml ├── opencv.yaml └── synology_surveillance.yaml ├── python_scripts ├── dark_sky_friendly_names.py ├── fade_in_light.py ├── initialize_tracker.py ├── light_counter.py ├── meta_device_tracker.py ├── ring_download.py ├── shellies_discovery.py └── toggle_state.py ├── recorder.yaml ├── scenes.yaml ├── scripts.yaml ├── sensor.yaml ├── shell_scripts ├── alexa_remote_control.sh ├── alexa_remote_control_plain.sh ├── alexa_wrapper.sh ├── facebox.sh └── image_classification.sh ├── switches.yaml ├── themes ├── oxfordblue │ └── oxfordblue.yaml ├── prev_themes.yaml └── sweetpink │ └── sweetpink.yaml ├── ui-lovelace.yaml ├── update.sh ├── update_files.sh ├── www ├── assets │ └── images │ │ ├── skullcanyon.jpeg │ │ └── vintage-colors-blur.jpg ├── community │ ├── button-card │ │ ├── button-card.js │ │ └── button-card.js.gz │ ├── config-template-card │ │ ├── config-template-card.js │ │ └── config-template-card.js.gz │ ├── list-card │ │ ├── list-card.js │ │ └── list-card.js.gz │ ├── lovelace-auto-entities │ │ ├── auto-entities.js │ │ └── auto-entities.js.gz │ ├── lovelace-card-mod │ │ ├── card-mod.js │ │ └── card-mod.js.gz │ ├── lovelace-card-tools │ │ ├── card-tools.js │ │ └── card-tools.js.gz │ ├── lovelace-hass-aarlo │ │ ├── hass-aarlo.js │ │ └── hass-aarlo.js.gz │ ├── lovelace-layout-card │ │ ├── layout-card.js │ │ └── layout-card.js.gz │ ├── lovelace-slider-entity-row │ │ ├── slider-entity-row.js │ │ └── slider-entity-row.js.gz │ ├── mini-graph-card │ │ ├── mini-graph-card-bundle.js │ │ └── mini-graph-card-bundle.js.gz │ ├── mini-media-player │ │ ├── mini-media-player-bundle.js │ │ └── mini-media-player-bundle.js.gz │ ├── pc-card │ │ ├── pc-card.js │ │ └── pc-card.js.gz │ ├── restriction-card │ │ ├── restriction-card.js │ │ └── restriction-card.js.gz │ ├── secondaryinfo-entity-row │ │ ├── secondaryinfo-entity-row.js │ │ └── secondaryinfo-entity-row.js.gz │ ├── simple-thermostat │ │ ├── simple-thermostat.js │ │ └── simple-thermostat.js.gz │ └── unused-card │ │ ├── unused-card.js │ │ └── unused-card.js.gz └── custom_ui │ ├── canvas-gauge-card.js │ ├── card-modder.js │ ├── circle-sensor-card.js │ ├── ext-weblink.js │ ├── gap-card.js │ ├── layout-card.js │ └── weather-card.js └── z-wave_relay.png /.HA_VERSION: -------------------------------------------------------------------------------- 1 | 2021.1.0 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ## GITATTRIBUTES FOR WEB PROJECTS 2 | # 3 | # These settings are for any web project. 4 | # 5 | # Details per file setting: 6 | # text These files should be normalized (i.e. convert CRLF to LF). 7 | # binary These files are binary and should be left untouched. 8 | # 9 | # Note that binary is a macro for -text -diff. 10 | ###################################################################### 11 | 12 | ## AUTO-DETECT - Handle line endings automatically for files detected 13 | ## as text and leave all files detected as binary untouched. 14 | ## This will handle all files NOT defined below. 15 | * text=auto 16 | 17 | ## SOURCE CODE 18 | *.bat text 19 | *.coffee text 20 | *.css text 21 | *.htm text 22 | *.html text 23 | *.inc text 24 | *.ini text 25 | *.js text 26 | *.jsx text 27 | *.json text 28 | *.less text 29 | *.php text 30 | *.pl text 31 | *.py text 32 | *.rb text 33 | *.sass text 34 | *.scm text 35 | *.scss text 36 | *.sh text 37 | *.sql text 38 | *.styl text 39 | *.ts text 40 | *.xml text 41 | *.xhtml text 42 | 43 | ## DOCUMENTATION 44 | *.markdown text 45 | *.md text 46 | *.mdwn text 47 | *.mdown text 48 | *.mkd text 49 | *.mkdn text 50 | *.mdtxt text 51 | *.mdtext text 52 | *.txt text 53 | AUTHORS text 54 | CHANGELOG text 55 | CHANGES text 56 | CONTRIBUTING text 57 | COPYING text 58 | INSTALL text 59 | license text 60 | LICENSE text 61 | NEWS text 62 | readme text 63 | *README* text 64 | TODO text 65 | 66 | ## TEMPLATES 67 | *.dot text 68 | *.ejs text 69 | *.haml text 70 | *.handlebars text 71 | *.hbs text 72 | *.hbt text 73 | *.jade text 74 | *.latte text 75 | *.mustache text 76 | *.phtml text 77 | *.tmpl text 78 | 79 | ## LINTERS 80 | .csslintrc text 81 | .eslintrc text 82 | .jscsrc text 83 | .jshintrc text 84 | .jshintignore text 85 | .stylelintrc text 86 | 87 | ## CONFIGS 88 | *.bowerrc text 89 | *.cnf text 90 | *.conf text 91 | *.config text 92 | .editorconfig text 93 | .gitattributes text 94 | .gitconfig text 95 | .gitignore text 96 | .htaccess text 97 | *.npmignore text 98 | *.yaml text 99 | *.yml text 100 | Makefile text 101 | makefile text 102 | 103 | ## HEROKU 104 | Procfile text 105 | .slugignore text 106 | 107 | ## GRAPHICS 108 | *.ai binary 109 | *.bmp binary 110 | *.eps binary 111 | *.gif binary 112 | *.ico binary 113 | *.jng binary 114 | *.jp2 binary 115 | *.jpg binary 116 | *.jpeg binary 117 | *.jpx binary 118 | *.jxr binary 119 | *.pdf binary 120 | *.png binary 121 | *.psb binary 122 | *.psd binary 123 | *.svg text 124 | *.svgz binary 125 | *.tif binary 126 | *.tiff binary 127 | *.wbmp binary 128 | *.webp binary 129 | 130 | ## AUDIO 131 | *.kar binary 132 | *.m4a binary 133 | *.mid binary 134 | *.midi binary 135 | *.mp3 binary 136 | *.ogg binary 137 | *.ra binary 138 | 139 | ## VIDEO 140 | *.3gpp binary 141 | *.3gp binary 142 | *.as binary 143 | *.asf binary 144 | *.asx binary 145 | *.fla binary 146 | *.flv binary 147 | *.m4v binary 148 | *.mng binary 149 | *.mov binary 150 | *.mp4 binary 151 | *.mpeg binary 152 | *.mpg binary 153 | *.swc binary 154 | *.swf binary 155 | *.webm binary 156 | 157 | ## ARCHIVES 158 | *.7z binary 159 | *.gz binary 160 | *.rar binary 161 | *.tar binary 162 | *.zip binary 163 | 164 | ## FONTS 165 | *.ttf binary 166 | *.eot binary 167 | *.otf binary 168 | *.woff binary 169 | *.woff2 binary 170 | 171 | ## EXECUTABLES 172 | *.exe binary 173 | *.pyc binary 174 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | # Python cache 5 | __pycache__ 6 | *.pyc 7 | 8 | # Other 9 | *.uuid 10 | *.conf 11 | *.db 12 | *.db-journal 13 | *.log 14 | *.noload 15 | *.txt 16 | *.sqlite 17 | *.xml 18 | *.backup 19 | *.json 20 | life360.sh 21 | .ip_authenticated.yaml 22 | ip_bans.yaml 23 | .config_entries.json 24 | *.google.token 25 | .google.token 26 | .ring_cache.pickle 27 | .spotify-token-cache 28 | .storage 29 | abodepy_cache.pickle 30 | camera_recording.py 31 | home-assistant.env 32 | home-assistant.* 33 | known_devices.yaml 34 | entity_registry.yaml 35 | secrets.yaml 36 | google_calendars.yaml 37 | SERVICE_ACCOUNT.json 38 | components 39 | deps 40 | tts 41 | www/icons 42 | www/floorplans 43 | www/community 44 | custom_components/hacs 45 | downloads 46 | icloud 47 | dlib_faces 48 | dlib_nofaces 49 | dlib_known_faces 50 | dlib_unknown_faces 51 | .cloud 52 | *.pickle 53 | .pc-session 54 | google*.deb 55 | .homekit.state 56 | -------------------------------------------------------------------------------- /HA_screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/HA_screenshot_1.png -------------------------------------------------------------------------------- /alert.yaml: -------------------------------------------------------------------------------- 1 | abode_updates: 2 | name: Abode updates are off 3 | done_message: Abode updates enabled 4 | entity_id: input_boolean.abodeupdate 5 | state: 'off' 6 | repeat: 30 7 | can_acknowledge: True 8 | skip_first: True 9 | notifiers: 10 | - ios_abode_updates 11 | home_automation: 12 | name: Home automation is disabled 13 | done_message: Home automation enabled 14 | entity_id: input_boolean.homeautomation 15 | state: 'off' 16 | repeat: 30 17 | can_acknowledge: True 18 | skip_first: True 19 | notifiers: 20 | - ios_home_automation 21 | -------------------------------------------------------------------------------- /blueprints/automation/homeassistant/motion_light.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Motion-activated Light 3 | description: Turn on a light when motion is detected. 4 | domain: automation 5 | source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml 6 | input: 7 | motion_entity: 8 | name: Motion Sensor 9 | selector: 10 | entity: 11 | domain: binary_sensor 12 | device_class: motion 13 | light_target: 14 | name: Light 15 | selector: 16 | target: 17 | entity: 18 | domain: light 19 | no_motion_wait: 20 | name: Wait time 21 | description: Time to leave the light on after last motion is detected. 22 | default: 120 23 | selector: 24 | number: 25 | min: 0 26 | max: 3600 27 | unit_of_measurement: seconds 28 | 29 | # If motion is detected within the delay, 30 | # we restart the script. 31 | mode: restart 32 | max_exceeded: silent 33 | 34 | trigger: 35 | platform: state 36 | entity_id: !input motion_entity 37 | from: "off" 38 | to: "on" 39 | 40 | action: 41 | - service: light.turn_on 42 | target: !input light_target 43 | - wait_for_trigger: 44 | platform: state 45 | entity_id: !input motion_entity 46 | from: "on" 47 | to: "off" 48 | - delay: !input no_motion_wait 49 | - service: light.turn_off 50 | target: !input light_target 51 | -------------------------------------------------------------------------------- /blueprints/automation/homeassistant/notify_leaving_zone.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Zone Notification 3 | description: Send a notification to a device when a person leaves a specific zone. 4 | domain: automation 5 | source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml 6 | input: 7 | person_entity: 8 | name: Person 9 | selector: 10 | entity: 11 | domain: person 12 | zone_entity: 13 | name: Zone 14 | selector: 15 | entity: 16 | domain: zone 17 | notify_device: 18 | name: Device to notify 19 | description: Device needs to run the official Home Assistant app to receive notifications. 20 | selector: 21 | device: 22 | integration: mobile_app 23 | 24 | trigger: 25 | platform: state 26 | entity_id: !input person_entity 27 | 28 | variables: 29 | zone_entity: !input zone_entity 30 | # This is the state of the person when it's in this zone. 31 | zone_state: "{{ states[zone_entity].name }}" 32 | person_entity: !input person_entity 33 | person_name: "{{ states[person_entity].name }}" 34 | 35 | condition: 36 | condition: template 37 | value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" 38 | 39 | action: 40 | domain: mobile_app 41 | type: notify 42 | device_id: !input notify_device 43 | message: "{{ person_name }} has left {{ zone_state }}" 44 | -------------------------------------------------------------------------------- /blueprints/script/homeassistant/confirmable_notification.yaml: -------------------------------------------------------------------------------- 1 | blueprint: 2 | name: Confirmable Notification 3 | description: >- 4 | A script that sends an actionable notification with a confirmation before 5 | running the specified action. 6 | domain: script 7 | source_url: https://github.com/home-assistant/core/blob/master/homeassistant/components/script/blueprints/confirmable_notification.yaml 8 | input: 9 | notify_device: 10 | name: Device to notify 11 | description: Device needs to run the official Home Assistant app to receive notifications. 12 | selector: 13 | device: 14 | integration: mobile_app 15 | title: 16 | name: "Title" 17 | description: "The title of the button shown in the notification." 18 | default: "" 19 | selector: 20 | text: 21 | message: 22 | name: "Message" 23 | description: "The message body" 24 | selector: 25 | text: 26 | confirm_text: 27 | name: "Confirmation Text" 28 | description: "Text to show on the confirmation button" 29 | default: "Confirm" 30 | selector: 31 | text: 32 | confirm_action: 33 | name: "Confirmation Action" 34 | description: "Action to run when notification is confirmed" 35 | default: [] 36 | selector: 37 | action: 38 | dismiss_text: 39 | name: "Dismiss Text" 40 | description: "Text to show on the dismiss button" 41 | default: "Dismiss" 42 | selector: 43 | text: 44 | dismiss_action: 45 | name: "Dismiss Action" 46 | description: "Action to run when notification is dismissed" 47 | default: [] 48 | selector: 49 | action: 50 | 51 | mode: restart 52 | 53 | sequence: 54 | - alias: "Send notification" 55 | domain: mobile_app 56 | type: notify 57 | device_id: !input notify_device 58 | title: !input title 59 | message: !input message 60 | data: 61 | actions: 62 | - action: "CONFIRM" 63 | title: !input confirm_text 64 | - action: "DISMISS" 65 | title: !input dismiss_text 66 | - alias: "Awaiting response" 67 | wait_for_trigger: 68 | - platform: event 69 | event_type: mobile_app_notification_action 70 | - choose: 71 | - conditions: "{{ wait.trigger.event.data.action == 'CONFIRM' }}" 72 | sequence: !input confirm_action 73 | - conditions: "{{ wait.trigger.event.data.action == 'DISMISS' }}" 74 | sequence: !input dismiss_action 75 | -------------------------------------------------------------------------------- /chromedriver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # https://developers.supportbee.com/blog/setting-up-cucumber-to-run-with-Chrome-on-Linux/ 3 | # https://gist.github.com/curtismcmullan/7be1a8c1c841a9d8db2c 4 | # http://stackoverflow.com/questions/10792403/how-do-i-get-chrome-working-with-selenium-using-php-webdriver 5 | # http://stackoverflow.com/questions/26133486/how-to-specify-binary-path-for-remote-chromedriver-in-codeception 6 | # http://stackoverflow.com/questions/40262682/how-to-run-selenium-3-x-with-chrome-driver-through-terminal 7 | # http://askubuntu.com/questions/760085/how-do-you-install-google-chrome-on-ubuntu-16-04 8 | 9 | # Versions 10 | CHROME_DRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE` 11 | SELENIUM_STANDALONE_VERSION=3.4.0 12 | SELENIUM_SUBDIR=$(echo "$SELENIUM_STANDALONE_VERSION" | cut -d"." -f-2) 13 | 14 | # Remove existing downloads and binaries so we can start from scratch. 15 | sudo apt-get remove google-chrome-stable 16 | rm ~/selenium-server-standalone-*.jar 17 | rm ~/chromedriver_linux64.zip 18 | sudo rm /usr/local/bin/chromedriver 19 | sudo rm /usr/local/bin/selenium-server-standalone.jar 20 | 21 | # Install dependencies. 22 | sudo apt-get update 23 | sudo apt-get install -y unzip openjdk-8-jre-headless xvfb libxi6 libgconf-2-4 24 | 25 | # Install Chrome. 26 | sudo curl -sS -o - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add 27 | sudo echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list 28 | sudo apt-get -y update 29 | sudo apt-get -y install google-chrome-stable 30 | 31 | # Install ChromeDriver. 32 | wget -N http://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip -P ~/ 33 | unzip ~/chromedriver_linux64.zip -d ~/ 34 | rm ~/chromedriver_linux64.zip 35 | sudo mv -f ~/chromedriver /usr/local/bin/chromedriver 36 | sudo chown root:root /usr/local/bin/chromedriver 37 | sudo chmod 0755 /usr/local/bin/chromedriver 38 | 39 | # Install Selenium. 40 | wget -N http://selenium-release.storage.googleapis.com/$SELENIUM_SUBDIR/selenium-server-standalone-$SELENIUM_STANDALONE_VERSION.jar -P ~/ 41 | sudo mv -f ~/selenium-server-standalone-$SELENIUM_STANDALONE_VERSION.jar /usr/local/bin/selenium-server-standalone.jar 42 | sudo chown root:root /usr/local/bin/selenium-server-standalone.jar 43 | sudo chmod 0755 /usr/local/bin/selenium-server-standalone.jar 44 | -------------------------------------------------------------------------------- /cover.yaml: -------------------------------------------------------------------------------- 1 | - platform: template 2 | covers: 3 | garagedoor: 4 | friendly_name: 'Garage Door' 5 | value_template: > 6 | {%- if states.binary_sensor.garage_door -%} 7 | {{ states.binary_sensor.garage_door.state == 'on' }} 8 | {%- else -%} 9 | true 10 | {%- endif -%} 11 | open_cover: 12 | service: switch.turn_on 13 | entity_id: switch.shelly_garage 14 | close_cover: 15 | service: switch.turn_on 16 | entity_id: switch.shelly_garage 17 | stop_cover: 18 | service: switch.turn_on 19 | entity_id: switch.shelly_garage 20 | icon_template: > 21 | {%- if states.binary_sensor.garage_door.state == 'on' -%} 22 | mdi:garage-open 23 | {%- else -%} 24 | mdi:garage 25 | {%- endif -%} 26 | -------------------------------------------------------------------------------- /custom_components/aarlo/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "aarlo", 3 | "name": "Arlo Camera Support", 4 | "documentation": "https://github.com/twrecked/hass-aarlo/blob/master/README.md", 5 | "issue_tracker": "https://github.com/twrecked/hass-aarlo/issues", 6 | "dependencies": ["ffmpeg"], 7 | "codeowners": ["@twrecked"], 8 | "requirements": ["unidecode","cloudscraper>=1.2.58", "paho-mqtt"], 9 | "version": "0.7.2b7", 10 | "iot_class": "cloud_push" 11 | } 12 | -------------------------------------------------------------------------------- /custom_components/aarlo/pyaarlo/light.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | 3 | from .constant import BATTERY_KEY, BRIGHTNESS_KEY, LAMP_STATE_KEY, MOTION_DETECTED_KEY 4 | from .device import ArloChildDevice 5 | 6 | 7 | class ArloLight(ArloChildDevice): 8 | def __init__(self, name, arlo, attrs): 9 | """An Arlo Light. 10 | 11 | :param name: name of light 12 | :param arlo: controlling arlo instance 13 | :param attrs: initial attributes give by Arlo 14 | """ 15 | super().__init__(name, arlo, attrs) 16 | 17 | @property 18 | def resource_type(self): 19 | return "lights" 20 | 21 | def _event_handler(self, resource, event): 22 | self._arlo.debug(self.name + " LIGHT got one " + resource) 23 | 24 | # pass on to lower layer 25 | super()._event_handler(resource, event) 26 | 27 | @property 28 | def is_on(self): 29 | return self._load(LAMP_STATE_KEY, "off") == "on" 30 | 31 | def turn_on(self, brightness=None, rgb=None): 32 | """Turn the light on. 33 | 34 | :param brightness: how bright to make the light 35 | :param rgb: what color to make the light 36 | """ 37 | properties = {LAMP_STATE_KEY: "on"} 38 | if brightness is not None: 39 | properties[BRIGHTNESS_KEY] = brightness 40 | if rgb is not None: 41 | # properties["single"] = rgb_to_hex(rgb) 42 | pass 43 | 44 | self._arlo.debug("{} sending {}".format(self._name, pprint.pformat(properties))) 45 | self._arlo.be.notify( 46 | base=self.base_station, 47 | body={ 48 | "action": "set", 49 | "properties": properties, 50 | "publishResponse": True, 51 | "resource": self.resource_id, 52 | }, 53 | ) 54 | return True 55 | 56 | def turn_off(self): 57 | """Turn the light off.""" 58 | self._arlo.be.notify( 59 | base=self.base_station, 60 | body={ 61 | "action": "set", 62 | "properties": {LAMP_STATE_KEY: "off"}, 63 | "publishResponse": True, 64 | "resource": self.resource_id, 65 | }, 66 | ) 67 | return True 68 | 69 | def set_brightness(self, brightness): 70 | """Set the light brightness. 71 | 72 | :param brightness: brightness to use (0-255) 73 | """ 74 | self._arlo.be.notify( 75 | base=self.base_station, 76 | body={ 77 | "action": "set", 78 | "properties": {BRIGHTNESS_KEY: brightness}, 79 | "publishResponse": True, 80 | "resource": self.resource_id, 81 | }, 82 | ) 83 | return True 84 | 85 | def has_capability(self, cap): 86 | if cap in (MOTION_DETECTED_KEY, BATTERY_KEY): 87 | return True 88 | return super().has_capability(cap) 89 | -------------------------------------------------------------------------------- /custom_components/aarlo/pyaarlo/storage.py: -------------------------------------------------------------------------------- 1 | import fnmatch 2 | import pickle 3 | import pprint 4 | import threading 5 | 6 | 7 | class ArloStorage(object): 8 | def __init__(self, arlo): 9 | self._arlo = arlo 10 | self._state_file = self._arlo.cfg.state_file 11 | self.db = {} 12 | self.lock = threading.Lock() 13 | self.load() 14 | 15 | def _ekey(self, key): 16 | return key if not isinstance(key, list) else "/".join(key) 17 | 18 | def _keys_matching(self, key): 19 | mkeys = [] 20 | ekey = self._ekey(key) 21 | for mkey in self.db: 22 | if fnmatch.fnmatch(mkey, ekey): 23 | mkeys.append(mkey) 24 | return mkeys 25 | 26 | def load(self): 27 | if self._state_file is not None: 28 | try: 29 | with self.lock: 30 | with open(self._state_file, "rb") as dump: 31 | self.db = pickle.load(dump) 32 | except Exception: 33 | self._arlo.debug("file not read") 34 | 35 | def save(self): 36 | if self._state_file is not None: 37 | try: 38 | with self.lock: 39 | with open(self._state_file, "wb") as dump: 40 | pickle.dump(self.db, dump) 41 | except Exception: 42 | self._arlo.warning("file not written") 43 | 44 | def file_name(self): 45 | return self._state_file 46 | 47 | def get(self, key, default=None): 48 | with self.lock: 49 | ekey = self._ekey(key) 50 | return self.db.get(ekey, default) 51 | 52 | def get_matching(self, key, default=None): 53 | with self.lock: 54 | gets = [] 55 | for mkey in self._keys_matching(key): 56 | gets.append((mkey, self.db.get(mkey, default))) 57 | return gets 58 | 59 | def keys_matching(self, key): 60 | with self.lock: 61 | return self._keys_matching(key) 62 | 63 | def set(self, key, value): 64 | ekey = self._ekey(key) 65 | output = "set:" + ekey + "=" + str(value) 66 | self._arlo.debug(output[:80]) 67 | with self.lock: 68 | self.db[ekey] = value 69 | return value 70 | 71 | def unset(self, key): 72 | with self.lock: 73 | del self.db[self._ekey(key)] 74 | 75 | def clear(self): 76 | with self.lock: 77 | self.db = {} 78 | 79 | def dump(self): 80 | with self.lock: 81 | pprint.pprint(self.db) 82 | -------------------------------------------------------------------------------- /custom_components/aarlo/pyaarlo/util.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import time 3 | from datetime import datetime, timezone 4 | 5 | import requests 6 | 7 | 8 | def utc_to_local(utc_dt): 9 | return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None) 10 | 11 | 12 | def the_epoch(): 13 | return utc_to_local(datetime.fromtimestamp(0, tz=timezone.utc)) 14 | 15 | 16 | def arlotime_to_time(timestamp): 17 | """Convert Arlo timestamp to Unix timestamp.""" 18 | return int(timestamp / 1000) 19 | 20 | 21 | def arlotime_to_datetime(timestamp): 22 | """Convert Arlo timestamp to Python datetime.""" 23 | return utc_to_local(datetime.fromtimestamp(int(timestamp / 1000), tz=timezone.utc)) 24 | 25 | 26 | def arlotime_strftime(timestamp, date_format="%Y-%m-%dT%H:%M:%S"): 27 | """Convert Arlo timestamp to time string.""" 28 | return arlotime_to_datetime(timestamp).strftime(date_format) 29 | 30 | 31 | def time_to_arlotime(timestamp=None): 32 | """Convert Unix timestamp to Arlo timestamp.""" 33 | if timestamp is None: 34 | timestamp = time.time() 35 | return int(timestamp * 1000) 36 | 37 | 38 | def now_strftime(date_format="%Y-%m-%dT%H:%M:%S"): 39 | """Convert now to time string.""" 40 | return datetime.now().strftime(date_format) 41 | 42 | 43 | def days_until(when): 44 | now = datetime.now() 45 | when = datetime.utcfromtimestamp(when) 46 | if when <= now: 47 | return 0 48 | return (when - now).days 49 | 50 | 51 | def httptime_to_datetime(http_timestamp): 52 | """Convert HTTP timestamp to Python datetime.""" 53 | return utc_to_local(datetime.strptime(http_timestamp, "%a, %d %b %Y %H:%M:%S GMT")) 54 | 55 | 56 | def httptime_strftime(http_timestamp, date_format="%Y-%m-%dT%H:%M:%S"): 57 | """Convert HTTP timestamp to time string.""" 58 | return httptime_to_datetime(http_timestamp).strftime(date_format) 59 | 60 | 61 | def _http_get(url): 62 | """Download HTTP data.""" 63 | 64 | if url is None: 65 | return None 66 | 67 | try: 68 | ret = requests.get(url) 69 | except requests.exceptions.SSLError: 70 | return None 71 | except Exception: 72 | return None 73 | 74 | if ret.status_code != 200: 75 | return None 76 | return ret 77 | 78 | 79 | def http_get(url, filename=None): 80 | """Download HTTP data.""" 81 | 82 | ret = _http_get(url) 83 | if ret is None: 84 | return False 85 | 86 | if filename is None: 87 | return ret.content 88 | 89 | with open(filename, "wb") as data: 90 | data.write(ret.content) 91 | return True 92 | 93 | 94 | def http_get_img(url, ignore_date=False): 95 | """Download HTTP image data.""" 96 | 97 | ret = _http_get(url) 98 | if ret is None: 99 | return None, datetime.now().astimezone() 100 | 101 | date = None 102 | if not ignore_date: 103 | date = ret.headers.get("Last-Modified", None) 104 | if date is not None: 105 | date = httptime_to_datetime(date) 106 | if date is None: 107 | date = datetime.now().astimezone() 108 | 109 | return ret.content, date 110 | 111 | 112 | def http_stream(url, chunk=4096): 113 | """Generate stream for a given record video. 114 | 115 | :param url: url of stream to read 116 | :param chunk: chunk bytes to read per time 117 | :returns generator object 118 | """ 119 | ret = requests.get(url, stream=True) 120 | ret.raise_for_status() 121 | for data in ret.iter_content(chunk): 122 | yield data 123 | 124 | 125 | def rgb_to_hex(rgb): 126 | """Convert HA color to Arlo color.""" 127 | return "#{:02x}{:02x}{:02x}".format(rgb[0], rgb[1], rgb[2]) 128 | 129 | 130 | def hex_to_rgb(h): 131 | """Convert Arlo color to HA color.""" 132 | return {"red": int(h[1:3], 16), "green": int(h[3:5], 16), "blue": int(h[5:7], 16)} 133 | 134 | 135 | def to_b64(in_str): 136 | """Convert a string into a base64 string.""" 137 | return base64.b64encode(in_str.encode()).decode() 138 | -------------------------------------------------------------------------------- /custom_components/aarlo/services.yaml: -------------------------------------------------------------------------------- 1 | # Describes the format for available virtual services 2 | 3 | camera_request_snapshot: 4 | description: Request a camera takes a snapshot 5 | fields: 6 | entity_id: 7 | description: Name(s) of entities to use 8 | example: 'camera.aarlo_front_camera' 9 | 10 | camera_request_snapshot_to_file: 11 | description: Request a camera takes a snapshot and write it to a file 12 | fields: 13 | entity_id: 14 | description: Name(s) of entities to use 15 | example: 'camera.aarlo_front_camera' 16 | filename: 17 | description: string (required) where to save the snapshot 18 | example: 'snapshot.jpg' 19 | 20 | camera_request_video_to_file: 21 | description: Request a camera records a video and write it to a file 22 | fields: 23 | entity_id: 24 | description: Name(s) of entities to use 25 | example: 'camera.aarlo_front_camera' 26 | filename: 27 | description: string (required) where to save the video 28 | example: 'snapshot.mp4' 29 | 30 | camera_stop_activity: 31 | description: Stop all stream activity on a camera 32 | fields: 33 | entity_id: 34 | description: Name(s) of entities to use 35 | example: 'camera.aarlo_front_camera' 36 | 37 | camera_start_recording: 38 | description: Request a camera start recording to cloud 39 | fields: 40 | entity_id: 41 | description: Name(s) of entities to use 42 | example: 'camera.aarlo_front_camera' 43 | duration: 44 | description: duration to record for in seconds 45 | example: 30 46 | 47 | camera_stop_recording: 48 | description: Request a camera stop recording to cloud 49 | fields: 50 | entity_id: 51 | description: Name(s) of entities to use 52 | example: 'camera.aarlo_front_camera' 53 | 54 | alarm_set_mode: 55 | description: Set the mode of a base station. 56 | fields: 57 | entity_id: 58 | description: Name(s) of entities to use 59 | example: 'alarm_control_panel.aarlo_front' 60 | mode: 61 | description: mode or schedule to change to 62 | example: 'home' 63 | 64 | siren_on: 65 | description: Turn on a siren. 66 | fields: 67 | entity_id: 68 | description: name(s) of entities to use 69 | example: 'alarm_control_panel.aarlo_front' 70 | volume: 71 | description: volume to use 72 | example: 6 73 | duration: 74 | description: duration to turn on for in seconds 75 | example: 30 76 | 77 | sirens_on: 78 | description: Turn on all sirens. 79 | fields: 80 | volume: 81 | description: volume to use 82 | example: 6 83 | duration: 84 | description: duration to turn on for in seconds 85 | example: 30 86 | 87 | siren_off: 88 | description: Turn off a siren. 89 | fields: 90 | entity_id: 91 | description: Name(s) of entities to turn off 92 | example: 'alarm_control_panel.aarlo_front' 93 | 94 | sirens_off: 95 | description: Turn off all sirens. 96 | 97 | inject_response: 98 | description: Inject a json packet into the Arlo event stream 99 | fields: 100 | filename: 101 | description: File in /config containing json packet. 102 | example: cry-off.json 103 | 104 | -------------------------------------------------------------------------------- /custom_components/alexa_media/alexa_media.py: -------------------------------------------------------------------------------- 1 | """ 2 | Alexa Devices Base Class. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | 6 | For more details about this platform, please refer to the documentation at 7 | https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 8 | """ 9 | 10 | import logging 11 | from typing import Dict, Text # noqa pylint: disable=unused-import 12 | 13 | from alexapy import AlexaAPI, hide_email 14 | 15 | from .const import DATA_ALEXAMEDIA 16 | 17 | _LOGGER = logging.getLogger(__name__) 18 | 19 | 20 | class AlexaMedia: 21 | """Implementation of Alexa Media Base object.""" 22 | 23 | def __init__(self, device, login) -> None: 24 | # pylint: disable=unexpected-keyword-arg 25 | """Initialize the Alexa device.""" 26 | 27 | # Class info 28 | self._login = login 29 | self.alexa_api = AlexaAPI(device, login) 30 | self.email = login.email 31 | self.account = hide_email(login.email) 32 | 33 | def check_login_changes(self): 34 | """Update Login object if it has changed.""" 35 | # _LOGGER.debug("Checking if Login object has changed") 36 | try: 37 | login = self.hass.data[DATA_ALEXAMEDIA]["accounts"][self.email]["login_obj"] 38 | except (AttributeError, KeyError): 39 | return 40 | # _LOGGER.debug("Login object %s closed status: %s", login, login.session.closed) 41 | # _LOGGER.debug( 42 | # "Alexaapi %s closed status: %s", 43 | # self.alexa_api, 44 | # self.alexa_api._session.closed, 45 | # ) 46 | if self.alexa_api.update_login(login): 47 | _LOGGER.debug("Login object has changed; updating") 48 | self._login = login 49 | self.email = login.email 50 | self.account = hide_email(login.email) 51 | -------------------------------------------------------------------------------- /custom_components/alexa_media/const.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support to interface with Alexa Devices. 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | 6 | For more details about this platform, please refer to the documentation at 7 | https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 8 | """ 9 | from datetime import timedelta 10 | 11 | __version__ = "3.10.15" 12 | PROJECT_URL = "https://github.com/custom-components/alexa_media_player/" 13 | ISSUE_URL = f"{PROJECT_URL}issues" 14 | 15 | DOMAIN = "alexa_media" 16 | DATA_ALEXAMEDIA = "alexa_media" 17 | 18 | PLAY_SCAN_INTERVAL = 20 19 | SCAN_INTERVAL = timedelta(seconds=60) 20 | MIN_TIME_BETWEEN_SCANS = SCAN_INTERVAL 21 | MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) 22 | 23 | ALEXA_COMPONENTS = [ 24 | "media_player", 25 | ] 26 | DEPENDENT_ALEXA_COMPONENTS = [ 27 | "notify", 28 | "switch", 29 | "sensor", 30 | "alarm_control_panel", 31 | "light", 32 | ] 33 | 34 | HTTP_COOKIE_HEADER = "# HTTP Cookie File" 35 | CONF_ACCOUNTS = "accounts" 36 | CONF_COOKIES_TXT = "cookies_txt" 37 | CONF_DEBUG = "debug" 38 | CONF_HASS_URL = "hass_url" 39 | CONF_INCLUDE_DEVICES = "include_devices" 40 | CONF_EXCLUDE_DEVICES = "exclude_devices" 41 | CONF_QUEUE_DELAY = "queue_delay" 42 | CONF_EXTENDED_ENTITY_DISCOVERY = "extended_entity_discovery" 43 | CONF_SECURITYCODE = "securitycode" 44 | CONF_OTPSECRET = "otp_secret" 45 | CONF_PROXY = "proxy" 46 | CONF_TOTP_REGISTER = "registered" 47 | CONF_OAUTH = "oauth" 48 | CONF_OAUTH_LOGIN = "oauth_login" 49 | DATA_LISTENER = "listener" 50 | 51 | EXCEPTION_TEMPLATE = "An exception of type {0} occurred. Arguments:\n{1!r}" 52 | 53 | DEFAULT_EXTENDED_ENTITY_DISCOVERY = False 54 | DEFAULT_QUEUE_DELAY = 1.5 55 | SERVICE_CLEAR_HISTORY = "clear_history" 56 | SERVICE_UPDATE_LAST_CALLED = "update_last_called" 57 | SERVICE_FORCE_LOGOUT = "force_logout" 58 | 59 | RECURRING_PATTERN = { 60 | None: "Never Repeat", 61 | "P1D": "Every day", 62 | "XXXX-WE": "Weekends", 63 | "XXXX-WD": "Weekdays", 64 | "XXXX-WXX-1": "Every Monday", 65 | "XXXX-WXX-2": "Every Tuesday", 66 | "XXXX-WXX-3": "Every Wednesday", 67 | "XXXX-WXX-4": "Every Thursday", 68 | "XXXX-WXX-5": "Every Friday", 69 | "XXXX-WXX-6": "Every Saturday", 70 | "XXXX-WXX-7": "Every Sunday", 71 | } 72 | 73 | RECURRING_PATTERN_ISO_SET = { 74 | None: {}, 75 | "P1D": {1, 2, 3, 4, 5, 6, 7}, 76 | "XXXX-WE": {6, 7}, 77 | "XXXX-WD": {1, 2, 3, 4, 5}, 78 | "XXXX-WXX-1": {1}, 79 | "XXXX-WXX-2": {2}, 80 | "XXXX-WXX-3": {3}, 81 | "XXXX-WXX-4": {4}, 82 | "XXXX-WXX-5": {5}, 83 | "XXXX-WXX-6": {6}, 84 | "XXXX-WXX-7": {7}, 85 | } 86 | 87 | ATTR_MESSAGE = "message" 88 | ATTR_EMAIL = "email" 89 | ATTR_NUM_ENTRIES = "entries" 90 | STARTUP = """ 91 | ------------------------------------------------------------------- 92 | {} 93 | Version: {} 94 | This is a custom component 95 | If you have any issues with this you need to open an issue here: 96 | {} 97 | ------------------------------------------------------------------- 98 | """.format( 99 | DOMAIN, __version__, ISSUE_URL 100 | ) 101 | 102 | AUTH_CALLBACK_PATH = "/auth/alexamedia/callback" 103 | AUTH_CALLBACK_NAME = "auth:alexamedia:callback" 104 | AUTH_PROXY_PATH = "/auth/alexamedia/proxy" 105 | AUTH_PROXY_NAME = "auth:alexamedia:proxy" 106 | -------------------------------------------------------------------------------- /custom_components/alexa_media/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "alexa_media", 3 | "name": "Alexa Media Player", 4 | "version": "3.10.15", 5 | "config_flow": true, 6 | "documentation": "https://github.com/custom-components/alexa_media_player/wiki", 7 | "issue_tracker": "https://github.com/custom-components/alexa_media_player/issues", 8 | "dependencies": ["persistent_notification", "http"], 9 | "codeowners": ["@alandtse", "@keatontaylor"], 10 | "requirements": ["alexapy==1.25.3", "packaging>=20.3", "wrapt>=1.12.1"], 11 | "iot_class": "cloud_polling" 12 | } 13 | -------------------------------------------------------------------------------- /custom_components/alexa_media/services.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | update_last_called: 3 | # Description of the service 4 | description: Forces update of last_called echo device for each Alexa account. 5 | # Different fields that your service accepts 6 | fields: 7 | # Key of the field 8 | email: 9 | # Description of the field 10 | description: List of Alexa accounts to update. If empty, will update all known accounts. 11 | # Example value that can be passed for this field 12 | example: "my_email@alexa.com" 13 | 14 | clear_history: 15 | # Description of the service 16 | description: Clear last entries from Alexa history for each Alexa account. 17 | # Different fields that your service accepts 18 | fields: 19 | # Key of the field 20 | email: 21 | # Description of the field 22 | description: List of Alexa accounts to update. If empty, will delete from all known accounts. 23 | # Example value that can be passed for this field 24 | example: "my_email@alexa.com" 25 | entries: 26 | # Description of the field 27 | description: Number of entries to clear from 1 to 50. If empty, clear 50. 28 | # Example value that can be passed for this field 29 | example: 50 30 | 31 | force_logout: 32 | # Description of the service 33 | description: Force logout of Alexa Login account and deletion of .pickle. Intended for debugging use. 34 | # Different fields that your service accepts 35 | fields: 36 | # Key of the field 37 | email: 38 | # Description of the field 39 | description: List of Alexa accounts to log out. If empty, will log out from all known accounts. 40 | # Example value that can be passed for this field 41 | example: "my_email@alexa.com" 42 | -------------------------------------------------------------------------------- /custom_components/androidtv/__init__.py: -------------------------------------------------------------------------------- 1 | """Support for functionality to interact with Android TV/Fire TV devices.""" 2 | -------------------------------------------------------------------------------- /custom_components/androidtv/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "androidtv", 3 | "name": "Android TV", 4 | "documentation": "https://www.home-assistant.io/integrations/androidtv", 5 | "requirements": [ 6 | "adb-shell[async]==0.2.1", 7 | "androidtv[async]==0.0.47", 8 | "pure-python-adb==0.2.2.dev0" 9 | ], 10 | "codeowners": ["@JeffLIrion"] 11 | } 12 | -------------------------------------------------------------------------------- /custom_components/androidtv/services.yaml: -------------------------------------------------------------------------------- 1 | # Describes the format for available Android TV and Fire TV services 2 | 3 | adb_command: 4 | description: Send an ADB command to an Android TV / Fire TV device. 5 | fields: 6 | entity_id: 7 | description: Name(s) of Android TV / Fire TV entities. 8 | example: "media_player.android_tv_living_room" 9 | command: 10 | description: Either a key command or an ADB shell command. 11 | example: "HOME" 12 | download: 13 | description: Download a file from your Android TV / Fire TV device to your Home Assistant instance. 14 | fields: 15 | entity_id: 16 | description: Name of Android TV / Fire TV entity. 17 | example: "media_player.android_tv_living_room" 18 | device_path: 19 | description: The filepath on the Android TV / Fire TV device. 20 | example: "/storage/emulated/0/Download/example.txt" 21 | local_path: 22 | description: The filepath on your Home Assistant instance. 23 | example: "/config/www/example.txt" 24 | upload: 25 | description: Upload a file from your Home Assistant instance to an Android TV / Fire TV device. 26 | fields: 27 | entity_id: 28 | description: Name(s) of Android TV / Fire TV entities. 29 | example: "media_player.android_tv_living_room" 30 | device_path: 31 | description: The filepath on the Android TV / Fire TV device. 32 | example: "/storage/emulated/0/Download/example.txt" 33 | local_path: 34 | description: The filepath on your Home Assistant instance. 35 | example: "/config/www/example.txt" 36 | learn_sendevent: 37 | description: Translate a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service. 38 | fields: 39 | entity_id: 40 | description: Name(s) of Android TV / Fire TV entities. 41 | example: "media_player.android_tv_living_room" 42 | -------------------------------------------------------------------------------- /custom_components/authenticated/__init__.py: -------------------------------------------------------------------------------- 1 | """The authenticated component.""" 2 | 3 | 4 | class AuthenticatedBaseException(Exception): 5 | """Base exception for Authenticated.""" 6 | -------------------------------------------------------------------------------- /custom_components/authenticated/const.py: -------------------------------------------------------------------------------- 1 | """Constants for authenticated.""" 2 | 3 | DOMAIN = "authenticated" 4 | INTEGRATION_VERSION = "21.9.0" 5 | ISSUE_URL = "https://github.com/custom-components/authenticated/issues" 6 | 7 | STARTUP = f""" 8 | ------------------------------------------------------------------- 9 | {DOMAIN} 10 | Version: {INTEGRATION_VERSION} 11 | This is a custom component 12 | If you have any issues with this you need to open an issue here: 13 | https://github.com/custom-components/authenticated/issues 14 | ------------------------------------------------------------------- 15 | """ 16 | 17 | 18 | CONF_NOTIFY = "enable_notification" 19 | CONF_EXCLUDE = "exclude" 20 | CONF_EXCLUDE_CLIENTS = "exclude_clients" 21 | CONF_PROVIDER = "provider" 22 | CONF_LOG_LOCATION = "log_location" 23 | 24 | OUTFILE = ".ip_authenticated.yaml" 25 | -------------------------------------------------------------------------------- /custom_components/authenticated/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "authenticated", 3 | "name": "Authenticated", 4 | "version": "21.9.0", 5 | "iot_class": "local_polling", 6 | "documentation": "https://github.com/custom-components/authenticated", 7 | "issue_tracker": "https://github.com/custom-components/authenticated/issues", 8 | "codeowners": [ 9 | "@ludeeus" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /custom_components/authenticated/providers.py: -------------------------------------------------------------------------------- 1 | """Providers""" 2 | import logging 3 | import requests 4 | from . import AuthenticatedBaseException 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | PROVIDERS = {} 9 | 10 | 11 | def register_provider(classname): 12 | """Decorator used to register providers.""" 13 | PROVIDERS[classname.name] = classname 14 | return classname 15 | 16 | 17 | class GeoProvider: 18 | """GeoProvider class.""" 19 | 20 | url = None 21 | 22 | def __init__(self, ipaddr): 23 | """Initialize.""" 24 | self.result = {} 25 | self.ipaddr = ipaddr 26 | 27 | @property 28 | def country(self): 29 | """Return country name or None.""" 30 | return self.result.get("country") 31 | 32 | @property 33 | def region(self): 34 | """Return region name or None.""" 35 | return self.result.get("region") 36 | 37 | @property 38 | def city(self): 39 | """Return city name or None.""" 40 | return self.result.get("city") 41 | 42 | @property 43 | def computed_result(self): 44 | """Return the computed result.""" 45 | if self.result is not None: 46 | return {"country": self.country, "region": self.region, "city": self.city} 47 | return None 48 | 49 | def update_geo_info(self): 50 | """Update Geo Information.""" 51 | self.result = {} 52 | try: 53 | api = self.url.format(self.ipaddr) 54 | header = {"user-agent": "Home Assistant/Python"} 55 | data = requests.get(api, headers=header, timeout=5).json() 56 | 57 | if data.get("error"): 58 | if data.get("reason") == "RateLimited": 59 | raise AuthenticatedBaseException( 60 | "RatelimitError, try a different provider." 61 | ) 62 | 63 | elif data.get("status", "success") == "error": 64 | return 65 | 66 | elif data.get("reserved"): 67 | return 68 | 69 | elif data.get("status", "success") == "fail": 70 | raise AuthenticatedBaseException( 71 | "[{}] - {}".format( 72 | self.ipaddr, data.get("message", "Unknown error.") 73 | ) 74 | ) 75 | 76 | self.result = data 77 | self.parse_data() 78 | except AuthenticatedBaseException as exception: 79 | _LOGGER.error(exception) 80 | except requests.exceptions.ConnectionError: 81 | pass 82 | 83 | def parse_data(self): 84 | """Parse data from geoprovider.""" 85 | self.result = self.result 86 | 87 | 88 | @register_provider 89 | class IPApi(GeoProvider): 90 | """IPApi class.""" 91 | 92 | url = "https://ipapi.co/{}/json" 93 | name = "ipapi" 94 | 95 | @property 96 | def country(self): 97 | """Return country name or None.""" 98 | return self.result.get("country_name") 99 | 100 | 101 | @register_provider 102 | class ExtremeIPLookup(GeoProvider): 103 | """IPApi class.""" 104 | 105 | url = "https://extreme-ip-lookup.com/json/{}" 106 | name = "extreme" 107 | 108 | 109 | @register_provider 110 | class IPVigilante(GeoProvider): 111 | """IPVigilante class.""" 112 | 113 | url = "https://ipvigilante.com/json/{}" 114 | name = "ipvigilante" 115 | 116 | def parse_data(self): 117 | """Parse data from geoprovider.""" 118 | self.result = self.result.get("data", {}) 119 | 120 | @property 121 | def country(self): 122 | """Return country name or None.""" 123 | return self.result.get("country_name") 124 | 125 | @property 126 | def region(self): 127 | """Return region name or None.""" 128 | return self.result.get("subdivision_1_name") 129 | 130 | @property 131 | def city(self): 132 | """Return city name or None.""" 133 | return self.result.get("city_name") 134 | -------------------------------------------------------------------------------- /custom_components/composite/__init__.py: -------------------------------------------------------------------------------- 1 | """Composite Device Tracker.""" 2 | import asyncio 3 | import logging 4 | 5 | import voluptuous as vol 6 | 7 | from homeassistant.requirements import async_process_requirements, RequirementsNotFound 8 | from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN 9 | from homeassistant.const import CONF_PLATFORM 10 | import homeassistant.helpers.config_validation as cv 11 | 12 | from .const import CONF_TIME_AS, DOMAIN, TZ_DEVICE_LOCAL, TZ_DEVICE_UTC 13 | 14 | CONF_TZ_FINDER = "tz_finder" 15 | DEFAULT_TZ_FINDER = "timezonefinderL==4.0.2" 16 | CONF_TZ_FINDER_CLASS = "tz_finder_class" 17 | TZ_FINDER_CLASS_OPTS = ["TimezoneFinder", "TimezoneFinderL"] 18 | 19 | CONFIG_SCHEMA = vol.Schema( 20 | { 21 | vol.Optional(DOMAIN, default=dict): vol.Schema( 22 | { 23 | vol.Optional(CONF_TZ_FINDER, default=DEFAULT_TZ_FINDER): cv.string, 24 | vol.Optional( 25 | CONF_TZ_FINDER_CLASS, default=TZ_FINDER_CLASS_OPTS[0] 26 | ): vol.In(TZ_FINDER_CLASS_OPTS), 27 | } 28 | ), 29 | }, 30 | extra=vol.ALLOW_EXTRA, 31 | ) 32 | 33 | _LOGGER = logging.getLogger(__name__) 34 | 35 | 36 | def setup(hass, config): 37 | if any( 38 | conf[CONF_TIME_AS] in (TZ_DEVICE_UTC, TZ_DEVICE_LOCAL) 39 | for conf in (config.get(DT_DOMAIN) or []) 40 | if conf[CONF_PLATFORM] == DOMAIN 41 | ): 42 | pkg = config[DOMAIN][CONF_TZ_FINDER] 43 | try: 44 | asyncio.run_coroutine_threadsafe( 45 | async_process_requirements( 46 | hass, "{}.{}".format(DOMAIN, DT_DOMAIN), [pkg] 47 | ), 48 | hass.loop, 49 | ).result() 50 | except RequirementsNotFound: 51 | _LOGGER.debug("Process requirements failed: %s", pkg) 52 | return False 53 | else: 54 | _LOGGER.debug("Process requirements suceeded: %s", pkg) 55 | 56 | if pkg.split("==")[0].strip().endswith("L"): 57 | from timezonefinderL import TimezoneFinder 58 | 59 | tf = TimezoneFinder() 60 | elif config[DOMAIN][CONF_TZ_FINDER_CLASS] == "TimezoneFinder": 61 | from timezonefinder import TimezoneFinder 62 | 63 | tf = TimezoneFinder() 64 | else: 65 | from timezonefinder import TimezoneFinderL 66 | 67 | tf = TimezoneFinderL() 68 | hass.data[DOMAIN] = tf 69 | 70 | return True 71 | -------------------------------------------------------------------------------- /custom_components/composite/const.py: -------------------------------------------------------------------------------- 1 | """Constants for Composite Integration.""" 2 | DOMAIN = "composite" 3 | 4 | CONF_REQ_MOVEMENT = "require_movement" 5 | CONF_TIME_AS = "time_as" 6 | 7 | TZ_UTC = "utc" 8 | TZ_LOCAL = "local" 9 | TZ_DEVICE_UTC = "device_or_utc" 10 | TZ_DEVICE_LOCAL = "device_or_local" 11 | # First item in list is default. 12 | TIME_AS_OPTS = [TZ_UTC, TZ_LOCAL, TZ_DEVICE_UTC, TZ_DEVICE_LOCAL] 13 | -------------------------------------------------------------------------------- /custom_components/enable_debug.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for enabling debug mode in HASS. 3 | 4 | Just add the following to yoru configuration.yaml 5 | enable_debug: 6 | """ 7 | DOMAIN = 'enable_debug' 8 | 9 | async def async_setup(hass, config): 10 | hass.loop.set_debug(True) 11 | return True 12 | -------------------------------------------------------------------------------- /custom_components/futures_cnn/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/custom_components/futures_cnn/__init__.py -------------------------------------------------------------------------------- /custom_components/google_scholar/__init__.py: -------------------------------------------------------------------------------- 1 | """The google_scholar sensor component.""" 2 | -------------------------------------------------------------------------------- /custom_components/google_scholar/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | @ Author : Alok Saboo 3 | @ Description : Obtain citations from Google Scholar 4 | """ 5 | 6 | import logging 7 | from datetime import date, datetime, timedelta 8 | 9 | import homeassistant.helpers.config_validation as cv 10 | import voluptuous as vol 11 | from homeassistant.components.sensor import PLATFORM_SCHEMA 12 | from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME 13 | from homeassistant.helpers.entity import Entity 14 | from homeassistant.util import Throttle 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | ATTRIBUTION = "Data provided by Google.com" 19 | DEFAULT_ICON = 'mdi:school' 20 | 21 | SCAN_INTERVAL = timedelta(hours=24) 22 | MIN_TIME_BETWEEN_UPDATES = timedelta(hours=24) 23 | 24 | CONF_AUTHOR = 'author' 25 | DEFAULT_NAME = 'Google Scholar' 26 | 27 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 28 | { 29 | vol.Required(CONF_AUTHOR): cv.string, 30 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 31 | } 32 | ) 33 | 34 | def setup_platform(hass, config, add_devices, discovery_info=None): 35 | """Set up the Google Scholar sensor.""" 36 | author = config.get(CONF_AUTHOR) 37 | if not author: 38 | msg = "Warning: No author configured." 39 | hass.components.persistent_notification.create(msg, "Sensor google_scholar") 40 | _LOGGER.warning(msg) 41 | return 42 | rest = GoogleScholarData(author) 43 | name = config.get(CONF_NAME) 44 | add_devices([GoogleScholarSensor(name, rest)], True) 45 | 46 | 47 | class GoogleScholarSensor(Entity): 48 | """Implementing the Google Scholar sensor.""" 49 | 50 | def __init__(self, name, rest): 51 | """Initialize the sensor.""" 52 | self._icon = DEFAULT_ICON 53 | self.rest = rest 54 | self._state = None 55 | self._name = name 56 | 57 | @property 58 | def name(self): 59 | """Return the name of the sensor.""" 60 | return self._name.rstrip() 61 | 62 | @property 63 | def icon(self): 64 | """Return the icon to use in the frontend, if any.""" 65 | icon = DEFAULT_ICON 66 | return icon 67 | 68 | @property 69 | def state(self): 70 | """Return the state of the sensor.""" 71 | try: 72 | self._state = float(self.rest.data['Citations']) 73 | except (KeyError, TypeError): 74 | self._state = None 75 | return self._state 76 | 77 | @property 78 | def device_state_attributes(self): 79 | """Return the state attributes of the sensor.""" 80 | attr = {} 81 | attr['Author'] = self.rest.data['Author'] 82 | attr['h-index'] = self.rest.data['hindex'] 83 | attr[ATTR_ATTRIBUTION] = ATTRIBUTION 84 | return attr 85 | 86 | @property 87 | def available(self): 88 | """Could the device be accessed during the last update call.""" 89 | return self.rest.available 90 | 91 | def update(self): 92 | """Update current date.""" 93 | self.rest.update() 94 | 95 | class GoogleScholarData(object): 96 | """Get data from yahoo.com.""" 97 | 98 | def __init__(self, author): 99 | """Initialize the data object.""" 100 | self._author = author 101 | self.data = None 102 | self.available = True 103 | 104 | @Throttle(MIN_TIME_BETWEEN_UPDATES) 105 | def update(self): 106 | """Get the latest data from Google Scholar.""" 107 | import scholarly 108 | try: 109 | results_json = {} 110 | results_json['Author'] = self._author 111 | author = self._author 112 | # Retrieve the author's data, fill-in, and print 113 | search_query = scholarly.search_author(author) 114 | author = next(search_query).fill() 115 | results_json['Citations'] = author.citedby 116 | results_json['hindex'] = author.hindex 117 | self.data = results_json 118 | self.available = True 119 | except requests.exceptions.ConnectionError: 120 | _LOGGER.error("Connection error") 121 | self.data = None 122 | self.available = False 123 | -------------------------------------------------------------------------------- /custom_components/hacs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "codeowners": [ 3 | "@ludeeus" 4 | ], 5 | "config_flow": true, 6 | "dependencies": [ 7 | "http", 8 | "websocket_api", 9 | "frontend", 10 | "persistent_notification", 11 | "lovelace" 12 | ], 13 | "documentation": "https://hacs.xyz/docs/configuration/start", 14 | "domain": "hacs", 15 | "iot_class": "cloud_polling", 16 | "issue_tracker": "https://github.com/hacs/integration/issues", 17 | "name": "HACS", 18 | "requirements": [ 19 | "aiogithubapi>=21.11.0" 20 | ], 21 | "version": "1.22.0" 22 | } -------------------------------------------------------------------------------- /custom_components/hacs/repositories/__init__.py: -------------------------------------------------------------------------------- 1 | """Initialize repositories.""" 2 | from __future__ import annotations 3 | 4 | from ..enums import HacsCategory 5 | from .appdaemon import HacsAppdaemonRepository 6 | from .base import HacsRepository 7 | from .integration import HacsIntegrationRepository 8 | from .netdaemon import HacsNetdaemonRepository 9 | from .plugin import HacsPluginRepository 10 | from .python_script import HacsPythonScriptRepository 11 | from .theme import HacsThemeRepository 12 | 13 | RERPOSITORY_CLASSES: dict[HacsCategory, HacsRepository] = { 14 | HacsCategory.THEME: HacsThemeRepository, 15 | HacsCategory.INTEGRATION: HacsIntegrationRepository, 16 | HacsCategory.PYTHON_SCRIPT: HacsPythonScriptRepository, 17 | HacsCategory.APPDAEMON: HacsAppdaemonRepository, 18 | HacsCategory.NETDAEMON: HacsNetdaemonRepository, 19 | HacsCategory.PLUGIN: HacsPluginRepository, 20 | } 21 | -------------------------------------------------------------------------------- /custom_components/hacs/repositories/appdaemon.py: -------------------------------------------------------------------------------- 1 | """Class for appdaemon apps in HACS.""" 2 | from __future__ import annotations 3 | 4 | from typing import TYPE_CHECKING 5 | 6 | from aiogithubapi import AIOGitHubAPIException 7 | 8 | from ..enums import HacsCategory 9 | from ..exceptions import HacsException 10 | from ..utils.decorator import concurrent 11 | from .base import HacsRepository 12 | 13 | if TYPE_CHECKING: 14 | from ..base import HacsBase 15 | 16 | 17 | class HacsAppdaemonRepository(HacsRepository): 18 | """Appdaemon apps in HACS.""" 19 | 20 | def __init__(self, hacs: HacsBase, full_name: str): 21 | """Initialize.""" 22 | super().__init__(hacs=hacs) 23 | self.data.full_name = full_name 24 | self.data.full_name_lower = full_name.lower() 25 | self.data.category = HacsCategory.APPDAEMON 26 | self.content.path.local = self.localpath 27 | self.content.path.remote = "apps" 28 | 29 | @property 30 | def localpath(self): 31 | """Return localpath.""" 32 | return f"{self.hacs.core.config_path}/appdaemon/apps/{self.data.name}" 33 | 34 | async def validate_repository(self): 35 | """Validate.""" 36 | await self.common_validate() 37 | 38 | # Custom step 1: Validate content. 39 | try: 40 | addir = await self.repository_object.get_contents("apps", self.ref) 41 | except AIOGitHubAPIException: 42 | raise HacsException( 43 | f"Repository structure for {self.ref.replace('tags/','')} is not compliant" 44 | ) from None 45 | 46 | if not isinstance(addir, list): 47 | self.validate.errors.append("Repository structure not compliant") 48 | 49 | self.content.path.remote = addir[0].path 50 | self.content.objects = await self.repository_object.get_contents( 51 | self.content.path.remote, self.ref 52 | ) 53 | 54 | # Handle potential errors 55 | if self.validate.errors: 56 | for error in self.validate.errors: 57 | if not self.hacs.status.startup: 58 | self.logger.error("%s %s", self, error) 59 | return self.validate.success 60 | 61 | @concurrent(concurrenttasks=10, backoff_time=5) 62 | async def update_repository(self, ignore_issues=False, force=False): 63 | """Update.""" 64 | if not await self.common_update(ignore_issues, force) and not force: 65 | return 66 | 67 | # Get appdaemon objects. 68 | if self.repository_manifest: 69 | if self.data.content_in_root: 70 | self.content.path.remote = "" 71 | 72 | if self.content.path.remote == "apps": 73 | addir = await self.repository_object.get_contents(self.content.path.remote, self.ref) 74 | self.content.path.remote = addir[0].path 75 | self.content.objects = await self.repository_object.get_contents( 76 | self.content.path.remote, self.ref 77 | ) 78 | 79 | # Set local path 80 | self.content.path.local = self.localpath 81 | -------------------------------------------------------------------------------- /custom_components/hacs/repositories/plugin.py: -------------------------------------------------------------------------------- 1 | """Class for plugins in HACS.""" 2 | from __future__ import annotations 3 | 4 | import json 5 | from typing import TYPE_CHECKING 6 | 7 | from ..exceptions import HacsException 8 | from ..utils.decorator import concurrent 9 | from .base import HacsRepository 10 | 11 | if TYPE_CHECKING: 12 | from ..base import HacsBase 13 | 14 | 15 | class HacsPluginRepository(HacsRepository): 16 | """Plugins in HACS.""" 17 | 18 | def __init__(self, hacs: HacsBase, full_name: str): 19 | """Initialize.""" 20 | super().__init__(hacs=hacs) 21 | self.data.full_name = full_name 22 | self.data.full_name_lower = full_name.lower() 23 | self.data.file_name = None 24 | self.data.category = "plugin" 25 | self.content.path.local = self.localpath 26 | 27 | @property 28 | def localpath(self): 29 | """Return localpath.""" 30 | return f"{self.hacs.core.config_path}/www/community/{self.data.full_name.split('/')[-1]}" 31 | 32 | async def validate_repository(self): 33 | """Validate.""" 34 | # Run common validation steps. 35 | await self.common_validate() 36 | 37 | # Custom step 1: Validate content. 38 | self.update_filenames() 39 | 40 | if self.content.path.remote is None: 41 | raise HacsException( 42 | f"Repository structure for {self.ref.replace('tags/','')} is not compliant" 43 | ) 44 | 45 | if self.content.path.remote == "release": 46 | self.content.single = True 47 | 48 | # Handle potential errors 49 | if self.validate.errors: 50 | for error in self.validate.errors: 51 | if not self.hacs.status.startup: 52 | self.logger.error("%s %s", self, error) 53 | return self.validate.success 54 | 55 | @concurrent(concurrenttasks=10, backoff_time=5) 56 | async def update_repository(self, ignore_issues=False, force=False): 57 | """Update.""" 58 | if not await self.common_update(ignore_issues, force) and not force: 59 | return 60 | 61 | # Get plugin objects. 62 | self.update_filenames() 63 | 64 | if self.content.path.remote is None: 65 | self.validate.errors.append( 66 | f"Repository structure for {self.ref.replace('tags/','')} is not compliant" 67 | ) 68 | 69 | if self.content.path.remote == "release": 70 | self.content.single = True 71 | 72 | async def get_package_content(self): 73 | """Get package content.""" 74 | try: 75 | package = await self.repository_object.get_contents("package.json", self.ref) 76 | package = json.loads(package.content) 77 | 78 | if package: 79 | self.data.authors = package["author"] 80 | except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except 81 | pass 82 | 83 | def update_filenames(self) -> None: 84 | """Get the filename to target.""" 85 | possible_locations = ("",) if self.data.content_in_root else ("release", "dist", "") 86 | 87 | # Handler for plug requirement 3 88 | if self.data.filename: 89 | valid_filenames = (self.data.filename,) 90 | else: 91 | valid_filenames = ( 92 | f"{self.data.name.replace('lovelace-', '')}.js", 93 | f"{self.data.name}.js", 94 | f"{self.data.name}.umd.js", 95 | f"{self.data.name}-bundle.js", 96 | ) 97 | 98 | for location in possible_locations: 99 | if location == "release": 100 | if not self.releases.objects: 101 | continue 102 | release = self.releases.objects[0] 103 | if not release.assets: 104 | continue 105 | asset = release.assets[0] 106 | for filename in valid_filenames: 107 | if filename == asset.name: 108 | self.data.file_name = filename 109 | self.content.path.remote = "release" 110 | break 111 | 112 | else: 113 | for filename in valid_filenames: 114 | if f"{location+'/' if location else ''}{filename}" in [ 115 | x.full_path for x in self.tree 116 | ]: 117 | self.data.file_name = filename.split("/")[-1] 118 | self.content.path.remote = location 119 | break 120 | -------------------------------------------------------------------------------- /custom_components/hacs/repositories/python_script.py: -------------------------------------------------------------------------------- 1 | """Class for python_scripts in HACS.""" 2 | from __future__ import annotations 3 | 4 | from typing import TYPE_CHECKING 5 | 6 | from ..enums import HacsCategory 7 | from ..exceptions import HacsException 8 | from ..utils.decorator import concurrent 9 | from .base import HacsRepository 10 | 11 | if TYPE_CHECKING: 12 | from ..base import HacsBase 13 | 14 | 15 | class HacsPythonScriptRepository(HacsRepository): 16 | """python_scripts in HACS.""" 17 | 18 | category = "python_script" 19 | 20 | def __init__(self, hacs: HacsBase, full_name: str): 21 | """Initialize.""" 22 | super().__init__(hacs=hacs) 23 | self.data.full_name = full_name 24 | self.data.full_name_lower = full_name.lower() 25 | self.data.category = HacsCategory.PYTHON_SCRIPT 26 | self.content.path.remote = "python_scripts" 27 | self.content.path.local = self.localpath 28 | self.content.single = True 29 | 30 | @property 31 | def localpath(self): 32 | """Return localpath.""" 33 | return f"{self.hacs.core.config_path}/python_scripts" 34 | 35 | async def validate_repository(self): 36 | """Validate.""" 37 | # Run common validation steps. 38 | await self.common_validate() 39 | 40 | # Custom step 1: Validate content. 41 | if self.data.content_in_root: 42 | self.content.path.remote = "" 43 | 44 | compliant = False 45 | for treefile in self.treefiles: 46 | if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(".py"): 47 | compliant = True 48 | break 49 | if not compliant: 50 | raise HacsException( 51 | f"Repository structure for {self.ref.replace('tags/','')} is not compliant" 52 | ) 53 | 54 | # Handle potential errors 55 | if self.validate.errors: 56 | for error in self.validate.errors: 57 | if not self.hacs.status.startup: 58 | self.logger.error("%s %s", self, error) 59 | return self.validate.success 60 | 61 | async def async_post_registration(self): 62 | """Registration.""" 63 | # Set name 64 | self.update_filenames() 65 | 66 | @concurrent(concurrenttasks=10, backoff_time=5) 67 | async def update_repository(self, ignore_issues=False, force=False): 68 | """Update.""" 69 | if not await self.common_update(ignore_issues, force) and not force: 70 | return 71 | 72 | # Get python_script objects. 73 | if self.data.content_in_root: 74 | self.content.path.remote = "" 75 | 76 | compliant = False 77 | for treefile in self.treefiles: 78 | if treefile.startswith(f"{self.content.path.remote}") and treefile.endswith(".py"): 79 | compliant = True 80 | break 81 | if not compliant: 82 | raise HacsException( 83 | f"Repository structure for {self.ref.replace('tags/','')} is not compliant" 84 | ) 85 | 86 | # Update name 87 | self.update_filenames() 88 | 89 | def update_filenames(self) -> None: 90 | """Get the filename to target.""" 91 | for treefile in self.tree: 92 | if treefile.full_path.startswith( 93 | self.content.path.remote 94 | ) and treefile.full_path.endswith(".py"): 95 | self.data.file_name = treefile.filename 96 | -------------------------------------------------------------------------------- /custom_components/hacs/repositories/theme.py: -------------------------------------------------------------------------------- 1 | """Class for themes in HACS.""" 2 | from __future__ import annotations 3 | 4 | from typing import TYPE_CHECKING 5 | 6 | from ..enums import HacsCategory 7 | from ..exceptions import HacsException 8 | from ..utils.decorator import concurrent 9 | from .base import HacsRepository 10 | 11 | if TYPE_CHECKING: 12 | from ..base import HacsBase 13 | 14 | 15 | class HacsThemeRepository(HacsRepository): 16 | """Themes in HACS.""" 17 | 18 | def __init__(self, hacs: HacsBase, full_name: str): 19 | """Initialize.""" 20 | super().__init__(hacs=hacs) 21 | self.data.full_name = full_name 22 | self.data.full_name_lower = full_name.lower() 23 | self.data.category = HacsCategory.THEME 24 | self.content.path.remote = "themes" 25 | self.content.path.local = self.localpath 26 | self.content.single = False 27 | 28 | @property 29 | def localpath(self): 30 | """Return localpath.""" 31 | return f"{self.hacs.core.config_path}/themes/{self.data.file_name.replace('.yaml', '')}" 32 | 33 | async def async_post_installation(self): 34 | """Run post installation steps.""" 35 | try: 36 | await self.hacs.hass.services.async_call("frontend", "reload_themes", {}) 37 | except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except 38 | pass 39 | 40 | async def validate_repository(self): 41 | """Validate.""" 42 | # Run common validation steps. 43 | await self.common_validate() 44 | 45 | # Custom step 1: Validate content. 46 | compliant = False 47 | for treefile in self.treefiles: 48 | if treefile.startswith("themes/") and treefile.endswith(".yaml"): 49 | compliant = True 50 | break 51 | if not compliant: 52 | raise HacsException( 53 | f"Repository structure for {self.ref.replace('tags/','')} is not compliant" 54 | ) 55 | 56 | if self.data.content_in_root: 57 | self.content.path.remote = "" 58 | 59 | # Handle potential errors 60 | if self.validate.errors: 61 | for error in self.validate.errors: 62 | if not self.hacs.status.startup: 63 | self.logger.error("%s %s", self, error) 64 | return self.validate.success 65 | 66 | async def async_post_registration(self): 67 | """Registration.""" 68 | # Set name 69 | self.update_filenames() 70 | self.content.path.local = self.localpath 71 | 72 | @concurrent(concurrenttasks=10, backoff_time=5) 73 | async def update_repository(self, ignore_issues=False, force=False): 74 | """Update.""" 75 | if not await self.common_update(ignore_issues, force) and not force: 76 | return 77 | 78 | # Get theme objects. 79 | if self.data.content_in_root: 80 | self.content.path.remote = "" 81 | 82 | # Update name 83 | self.update_filenames() 84 | self.content.path.local = self.localpath 85 | 86 | def update_filenames(self) -> None: 87 | """Get the filename to target.""" 88 | for treefile in self.tree: 89 | if treefile.full_path.startswith( 90 | self.content.path.remote 91 | ) and treefile.full_path.endswith(".yaml"): 92 | self.data.file_name = treefile.filename 93 | -------------------------------------------------------------------------------- /custom_components/hacs/sensor.py: -------------------------------------------------------------------------------- 1 | """Sensor platform for HACS.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.components.sensor import SensorEntity 5 | from homeassistant.core import callback 6 | 7 | from .base import HacsBase 8 | from .const import DOMAIN, NAME_SHORT 9 | 10 | 11 | async def async_setup_platform(hass, _config, async_add_entities, _discovery_info=None): 12 | """Setup sensor platform.""" 13 | async_add_entities([HACSSensor(hacs=hass.data.get(DOMAIN))]) 14 | 15 | 16 | async def async_setup_entry(hass, _config_entry, async_add_devices): 17 | """Setup sensor platform.""" 18 | async_add_devices([HACSSensor(hacs=hass.data.get(DOMAIN))]) 19 | 20 | 21 | class HACSSensor(SensorEntity): 22 | """HACS Sensor class.""" 23 | 24 | _attr_should_poll = False 25 | _attr_unique_id = "0717a0cd-745c-48fd-9b16-c8534c9704f9-bc944b0f-fd42-4a58-a072-ade38d1444cd" 26 | _attr_name = "hacs" 27 | _attr_icon = "hacs:hacs" 28 | _attr_unit_of_measurement = "pending update(s)" 29 | 30 | def __init__(self, hacs: HacsBase) -> None: 31 | """Initialize.""" 32 | self.hacs = hacs 33 | self._attr_native_value = None 34 | 35 | async def async_update(self) -> None: 36 | """Manual updates of the sensor.""" 37 | self._update() 38 | 39 | @callback 40 | def _update_and_write_state(self, *_) -> None: 41 | """Update the sensor and write state.""" 42 | self._update() 43 | self.async_write_ha_state() 44 | 45 | @property 46 | def device_info(self) -> dict[str, any]: 47 | """Return device information about HACS.""" 48 | info = { 49 | "identifiers": {(DOMAIN, self.unique_id)}, 50 | "name": NAME_SHORT, 51 | "manufacturer": "hacs.xyz", 52 | "model": "", 53 | "sw_version": str(self.hacs.version), 54 | "configuration_url": "homeassistant://hacs", 55 | } 56 | # LEGACY can be removed when min HA version is 2021.12 57 | if self.hacs.core.ha_version >= "2021.12.0b0": 58 | # pylint: disable=import-outside-toplevel 59 | from homeassistant.helpers.device_registry import DeviceEntryType 60 | 61 | info["entry_type"] = DeviceEntryType.SERVICE 62 | else: 63 | info["entry_type"] = "service" 64 | return info 65 | 66 | @callback 67 | def _update(self) -> None: 68 | """Update the sensor.""" 69 | 70 | repositories = [ 71 | repository 72 | for repository in self.hacs.repositories.list_all 73 | if repository.pending_update 74 | ] 75 | self._attr_native_value = len(repositories) 76 | self._attr_extra_state_attributes = { 77 | "repositories": [ 78 | { 79 | "name": repository.data.full_name, 80 | "display_name": repository.display_name, 81 | "installed_version": repository.display_installed_version, 82 | "available_version": repository.display_available_version, 83 | } 84 | for repository in repositories 85 | ] 86 | } 87 | 88 | async def async_added_to_hass(self) -> None: 89 | """Register for status events.""" 90 | self.async_on_remove( 91 | self.hass.bus.async_listen("hacs/repository", self._update_and_write_state) 92 | ) 93 | -------------------------------------------------------------------------------- /custom_components/moon_here/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/custom_components/moon_here/__init__.py -------------------------------------------------------------------------------- /custom_components/personalcapital/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /custom_components/personalcapital/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "personalcapital", 3 | "name": "Personal Capital", 4 | "documentation": "https://github.com/custom-components/sensor.personalcapital/blob/master/README.md", 5 | "dependencies": [], 6 | "codeowners": ["@iantrich"], 7 | "requirements": ["git+https://github.com/sanghviharshit/personalcapital.git@master#sanghviharshit"], 8 | "version": "0.1.3" 9 | } 10 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/__init__.py: -------------------------------------------------------------------------------- 1 | """The samsungtv component.""" 2 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Remote control Samsung televisions via TCP/IP connection""" 4 | 5 | 6 | __title__ = "samsungctl" 7 | __version__ = "0.8.65b" 8 | __url__ = "https://github.com/kdschlosser/samsungctl" 9 | __author__ = "Lauri Niskanen, Kevin Schlosser" 10 | __author_email__ = "kevin.g.schlosser@gmail.com" 11 | __license__ = "MIT" 12 | 13 | from . import utils # NOQA 14 | from .config import Config # NOQA 15 | from .remote import Remote # NOQA 16 | 17 | 18 | def discover(timeout=8): 19 | from .upnp.discover import discover as _discover 20 | res = list(_discover(timeout=timeout)) 21 | 22 | return res 23 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/cec_control/dispatcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Dispatcher(object): 5 | __callbacks = {} 6 | 7 | def __init__(self): 8 | import sys 9 | mod = sys.modules[__name__] 10 | self.__dict__ = mod.__dict__ 11 | sys.modules[__name__] = self 12 | 13 | def connect(self, callback, signal): 14 | if signal not in self.__callbacks: 15 | self.__callbacks[signal] = set() 16 | self.__callbacks[signal].add(callback) 17 | 18 | def disconnect(self, callback, signal): 19 | if signal in self.__callbacks: 20 | self.__callbacks[signal].discard(callback) 21 | if not len(self.__callbacks[signal]): 22 | del self.__callbacks[signal] 23 | 24 | def send(self, signal, sender, *args, **kwargs): 25 | if signal in self.__callbacks: 26 | for callback in self.__callbacks[signal]: 27 | callback(signal=signal, sender=sender, *args, **kwargs) 28 | 29 | 30 | dispatcher = Dispatcher() 31 | connect = dispatcher.connect 32 | disconnect = dispatcher.disconnect 33 | send = dispatcher.send 34 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/exceptions.py: -------------------------------------------------------------------------------- 1 | class SamsungTVError(Exception): 2 | """Samsung TV Exception Base Class.""" 3 | 4 | def __init__(self, *args): 5 | self._args = args 6 | 7 | def __str__(self): 8 | 9 | if self._args: 10 | return self.__class__.__doc__ % self._args 11 | 12 | elif b'%s' in self.__class__.__doc__: 13 | args = tuple([''] * self.__class__.__doc__.count(b'%s')) 14 | return self.__class__.__doc__ % args 15 | 16 | return self.__class__.__doc__ 17 | 18 | 19 | class AccessDenied(SamsungTVError): 20 | """Connection was denied.""" 21 | 22 | 23 | class ConnectionClosed(SamsungTVError): 24 | """Connection was closed.""" 25 | 26 | 27 | class UnhandledResponse(SamsungTVError): 28 | """Received unknown response.""" 29 | 30 | 31 | class NoTVFound(SamsungTVError): 32 | """Unable to locate a TV.""" 33 | 34 | 35 | class ConfigError(SamsungTVError): 36 | """Base class for config exceptions.""" 37 | 38 | 39 | class ConfigPortError(ConfigError): 40 | """Unknown connection port %s.""" 41 | 42 | 43 | class ConfigHostError(ConfigError): 44 | """Host (IP) not specified.""" 45 | 46 | 47 | class ConfigUnknownMethod(ConfigError): 48 | """Unknown connection method %s.""" 49 | 50 | 51 | class ConfigParseError(ConfigError): 52 | """Config data is not json formatted or is not a formatted flat file.""" 53 | 54 | 55 | class ConfigLoadError(ConfigError): 56 | """Config path specified cannot be located.""" 57 | 58 | 59 | class ConfigSavePathError(ConfigError): 60 | """Config save path %s is not valid.""" 61 | 62 | 63 | class ConfigSaveError(ConfigError): 64 | """Error saving config.""" 65 | 66 | 67 | class ConfigSavePathNotSpecified(ConfigError): 68 | """Config save path was not specified.""" 69 | 70 | 71 | class ConfigParameterError(ConfigError): 72 | """Parameter %s is not a config parameter.""" 73 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/power_handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import threading 4 | from . import wake_on_lan 5 | 6 | 7 | CEC_POWER_STATUS_ON = 0 8 | CEC_POWER_STATUS_STANDBY = 1 9 | CEC_POWER_STATUS_IN_TRANSITION_STANDBY_TO_ON = 2 10 | CEC_POWER_STATUS_IN_TRANSITION_ON_TO_STANDBY = 3 11 | CEC_POWER_STATUS_UNKNOWN = 153 12 | 13 | 14 | class PowerHandler(object): 15 | def __init__(self, parent): 16 | self._parent = parent 17 | self._event = threading.Event() 18 | self._thread = None 19 | self._power_on = False 20 | self._power_off = False 21 | self._is_powering_off = False 22 | self._is_powering_on = False 23 | 24 | @property 25 | def is_powering_off(self): 26 | return self._is_powering_off 27 | 28 | @is_powering_off.setter 29 | def is_powering_off(self, value): 30 | self._is_powering_off = value 31 | self._event.set() 32 | 33 | @property 34 | def is_powering_on(self): 35 | return self._is_powering_on 36 | 37 | @is_powering_on.setter 38 | def is_powering_on(self, value): 39 | self._is_powering_on = value 40 | self._event.set() 41 | 42 | def __on(self): 43 | self.is_powering_on = True 44 | 45 | if self._parent._cec is not None: 46 | if self._parent._cec.tv.power not in ( 47 | CEC_POWER_STATUS_ON, 48 | CEC_POWER_STATUS_IN_TRANSITION_STANDBY_TO_ON 49 | ): 50 | self._parent._cec.tv.power = True 51 | self._event.wait(120.0) 52 | else: 53 | self._parent.open() 54 | 55 | elif self._parent.mac_address: 56 | count = 0 57 | while not self._event.isSet(): 58 | if count == 60: 59 | break 60 | 61 | wake_on_lan.send_wol(self._parent.mac_address) 62 | self._event.wait(2.0) 63 | count += 1 64 | 65 | self._power_on = False 66 | 67 | if self._power_off: 68 | self.__off() 69 | 70 | def __off(self): 71 | self.is_powering_off = True 72 | 73 | if self._parent._cec is not None: 74 | if self._parent._cec.tv.power not in ( 75 | CEC_POWER_STATUS_STANDBY, 76 | CEC_POWER_STATUS_IN_TRANSITION_ON_TO_STANDBY 77 | ): 78 | self._parent._cec.tv.power = False 79 | 80 | else: 81 | if self._parent.config.method == 'websocket': 82 | power_key = 'KEY_POWER' 83 | else: 84 | power_key = 'KEY_POWEROFF' 85 | 86 | self._parent._send_key(power_key) 87 | 88 | self._event.wait(120.0) 89 | 90 | self._power_off = False 91 | 92 | if self._power_on: 93 | self.__on() 94 | 95 | def power_off(self): 96 | if self.is_powering_on: 97 | self._power_off = True 98 | elif not self.is_powering_off: 99 | self._thread = threading.Thread(target=self.__off) 100 | self._thread.daemon = True 101 | self._thread.start() 102 | 103 | def power_on(self): 104 | if self.is_powering_off: 105 | self._power_on = True 106 | elif not self.is_powering_on: 107 | self._thread = threading.Thread(target=self.__on) 108 | self._thread.daemon = True 109 | self._thread.start() 110 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/remote.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import six 4 | # noinspection PyCompatibility 5 | from . import exceptions 6 | from .remote_legacy import RemoteLegacy 7 | from .remote_websocket import RemoteWebsocket 8 | from .remote_encrypted import RemoteEncrypted 9 | from .config import Config 10 | from .key_mappings import KEYS 11 | 12 | 13 | class KeyWrapper(object): 14 | def __init__(self, remote, key): 15 | self.remote = remote 16 | self.key = key 17 | 18 | def __call__(self): 19 | self.key(self.remote) 20 | 21 | 22 | class RemoteMeta(type): 23 | 24 | def __call__(cls, config): 25 | 26 | if isinstance(config, dict): 27 | config = Config(**config) 28 | 29 | if config.method == "legacy": 30 | remote = RemoteLegacy(config) 31 | elif config.method == "websocket": 32 | remote = RemoteWebsocket(config) 33 | elif config.method == "encrypted": 34 | remote = RemoteEncrypted(config) 35 | else: 36 | raise exceptions.ConfigUnknownMethod(config.method) 37 | 38 | for name, key in KEYS.items(): 39 | remote.__dict__[name] = KeyWrapper(remote, key) 40 | 41 | return remote 42 | 43 | 44 | @six.add_metaclass(RemoteMeta) 45 | class Remote(object): 46 | 47 | def __init__(self, config): 48 | self.config = config 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/remote_encrypted/aes.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES as _AES 2 | import binascii 3 | from .keys import wbKey 4 | 5 | # Padding for the input string --not 6 | # related to encryption itself. 7 | BLOCK_SIZE = 16 # Bytes 8 | IV = b'\x00' * BLOCK_SIZE 9 | 10 | 11 | MODE_CBC = _AES.MODE_CBC 12 | 13 | 14 | def pad(s): 15 | return ( 16 | s + 17 | (BLOCK_SIZE - len(s) % BLOCK_SIZE) * 18 | chr(BLOCK_SIZE - len(s) % BLOCK_SIZE) 19 | ) 20 | 21 | 22 | def unpad(s): 23 | return s[:-ord(s[len(s) - 1:])] 24 | 25 | 26 | class AES: 27 | """ 28 | Usage: 29 | c = AESCipher('password').encrypt('message') 30 | m = AESCipher('password').decrypt(c) 31 | Tested under Python 3 and PyCrypto 2.6.1. 32 | """ 33 | 34 | def __init__(self, key, mode=_AES.MODE_ECB, *args): 35 | try: 36 | self.key = binascii.unhexlify(key) 37 | except (TypeError, binascii.Error): 38 | self.key = key 39 | 40 | self._mode = mode 41 | self._args = args 42 | 43 | @property 44 | def _cipher(self): 45 | if self._mode == MODE_CBC: 46 | cipher = _AES.new(self.key, self._mode, IV) 47 | else: 48 | cipher = _AES.new(self.key, self._mode, *self._args) 49 | 50 | return cipher 51 | 52 | def decrypt(self, enc, remove_padding=True, unhexlify=binascii.unhexlify): 53 | if unhexlify is not None: 54 | data = unhexlify(enc) 55 | else: 56 | data = enc 57 | 58 | data = self._cipher.decrypt(data) 59 | 60 | if remove_padding: 61 | return unpad(data) 62 | else: 63 | return data 64 | 65 | def encrypt(self, raw, add_padding=True, encoding="utf8"): 66 | if add_padding: 67 | data = pad(raw) 68 | else: 69 | data = raw 70 | 71 | if encoding is not None: 72 | data = data.encode(encoding) 73 | 74 | return self._cipher.encrypt(data) 75 | 76 | 77 | AES_CIPHER = AES(wbKey, MODE_CBC) 78 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/remote_encrypted/keys.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | 3 | publicKey = ( 4 | '2cb12bb2cbf7cec713c0fff7b59ae68a96784ae517f41d259a45d20556177c0ffe951ca60' 5 | 'ec03a990c9412619d1bee30adc7773088c5721664cffcedacf6d251cb4b76e2fd7aef09b3' 6 | 'ae9f9496ac8d94ed2b262eee37291c8b237e880cc7c021fb1be0881f3d0bffa4234d3b8e6' 7 | 'a61530c00473ce169c025f47fcc001d9b8051' 8 | ) 9 | privateKey = ( 10 | '2fd6334713816fae018cdee4656c5033a8d6b00e8eaea07b3624999242e96247112dcd019' 11 | 'c4191f4643c3ce1605002b2e506e7f1d1ef8d9b8044e46d37c0d5263216a87cd783aa1854' 12 | '90436c4a0cb2c524e15bc1bfeae703bcbc4b74a0540202e8d79cadaae85c6f9c218bc1107' 13 | 'd1f5b4b9bd87160e782f4e436eeb17485ab4d' 14 | ) 15 | transKey = '6c9474469ddf7578f3e5ad8a4c703d99' 16 | wbKey = 'abbb120c09e7114243d1fa0102163b27' 17 | prime = ( 18 | 'b361eb0ab01c3439f2c16ffda7b05e3e320701ebee3e249123c3586765fd5bf6c1dfa88bb' 19 | '6bb5da3fde74737cd88b6a26c5ca31d81d18e3515533d08df619317063224cf0943a2f29a' 20 | '5fe60c1c31ddf28334ed76a6478a1122fb24c4a94c8711617ddfe90cf02e643cd82d4748d' 21 | '6d4a7ca2f47d88563aa2baf6482e124acd7dd' 22 | ) 23 | 24 | 25 | BN_PRIME = int(prime, 16) 26 | BN_PRIVATE_KEY = int(privateKey, 16) 27 | PUBLIC_KEY = binascii.unhexlify(publicKey) 28 | TRANS_KEY = binascii.unhexlify(transKey) 29 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/remote_encrypted/rijndael/__init__.py: -------------------------------------------------------------------------------- 1 | from .rijndael import Rijndael 2 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/remote_encrypted/rijndael/constants.py: -------------------------------------------------------------------------------- 1 | 2 | shifts = [[[0, 0], [1, 3], [2, 2], [3, 1]], 3 | [[0, 0], [1, 5], [2, 4], [3, 3]], 4 | [[0, 0], [1, 7], [3, 5], [4, 4]]] 5 | 6 | # [key_size][block_size] 7 | num_rounds = { 8 | 16: {16: 3} 9 | } 10 | 11 | A = [[1, 1, 1, 1, 1, 0, 0, 0], 12 | [0, 1, 1, 1, 1, 1, 0, 0], 13 | [0, 0, 1, 1, 1, 1, 1, 0], 14 | [0, 0, 0, 1, 1, 1, 1, 1], 15 | [1, 0, 0, 0, 1, 1, 1, 1], 16 | [1, 1, 0, 0, 0, 1, 1, 1], 17 | [1, 1, 1, 0, 0, 0, 1, 1], 18 | [1, 1, 1, 1, 0, 0, 0, 1]] 19 | 20 | # produce log and a_log tables, needed for multiplying in the 21 | # field GF(2^m) (generator = 3) 22 | a_log = [1] 23 | for i in range(255): 24 | j = (a_log[-1] << 1) ^ a_log[-1] 25 | if j & 0x100 != 0: 26 | j ^= 0x11B 27 | a_log.append(j) 28 | 29 | log = [0] * 256 30 | for i in range(1, 255): 31 | log[a_log[i]] = i 32 | 33 | 34 | # multiply two elements of GF(2^m) 35 | def mul(a, b): 36 | if a == 0 or b == 0: 37 | return 0 38 | return a_log[(log[a & 0xFF] + log[b & 0xFF]) % 255] 39 | 40 | 41 | # substitution box based on F^{-1}(x) 42 | box = [[0] * 8 for i in range(256)] 43 | box[1][7] = 1 44 | for i in range(2, 256): 45 | j = a_log[255 - log[i]] 46 | for t in range(8): 47 | box[i][t] = (j >> (7 - t)) & 0x01 48 | 49 | B = [0, 1, 1, 0, 0, 0, 1, 1] 50 | 51 | # affine transform: box[i] <- B + A*box[i] 52 | cox = [[0] * 8 for i in range(256)] 53 | for i in range(256): 54 | for t in range(8): 55 | cox[i][t] = B[t] 56 | for j in range(8): 57 | cox[i][t] ^= A[t][j] * box[i][j] 58 | 59 | # S-boxes and inverse S-boxes 60 | S = [0] * 256 61 | Si = [0] * 256 62 | for i in range(256): 63 | S[i] = cox[i][0] << 7 64 | for t in range(1, 8): 65 | S[i] ^= cox[i][t] << (7-t) 66 | Si[S[i] & 0xFF] = i 67 | 68 | # T-boxes 69 | G = [ 70 | [2, 1, 1, 3], 71 | [3, 2, 1, 1], 72 | [1, 3, 2, 1], 73 | [1, 1, 3, 2] 74 | ] 75 | 76 | AA = [[0] * 8 for i in range(4)] 77 | 78 | for i in range(4): 79 | for j in range(4): 80 | AA[i][j] = G[i][j] 81 | AA[i][i+4] = 1 82 | 83 | for i in range(4): 84 | pivot = AA[i][i] 85 | for j in range(8): 86 | if AA[i][j] != 0: 87 | AA[i][j] = a_log[ 88 | (255 + log[AA[i][j] & 0xFF] - log[pivot & 0xFF]) % 255 89 | ] 90 | for t in range(4): 91 | if i != t: 92 | for j in range(i+1, 8): 93 | AA[t][j] ^= mul(AA[i][j], AA[t][i]) 94 | AA[t][i] = 0 95 | 96 | iG = [[0] * 4 for i in range(4)] 97 | 98 | for i in range(4): 99 | for j in range(4): 100 | iG[i][j] = AA[i][j + 4] 101 | 102 | 103 | def mul4(a, bs): 104 | if a == 0: 105 | return 0 106 | rr = 0 107 | for b in bs: 108 | rr <<= 8 109 | if b != 0: 110 | rr = rr | mul(a, b) 111 | return rr 112 | 113 | 114 | T1 = [] 115 | T2 = [] 116 | T3 = [] 117 | T4 = [] 118 | T5 = [] 119 | T6 = [] 120 | T7 = [] 121 | T8 = [] 122 | U1 = [] 123 | U2 = [] 124 | U3 = [] 125 | U4 = [] 126 | 127 | for t in range(256): 128 | s = S[t] 129 | T1.append(mul4(s, G[0])) 130 | T2.append(mul4(s, G[1])) 131 | T3.append(mul4(s, G[2])) 132 | T4.append(mul4(s, G[3])) 133 | 134 | s = Si[t] 135 | T5.append(mul4(s, iG[0])) 136 | T6.append(mul4(s, iG[1])) 137 | T7.append(mul4(s, iG[2])) 138 | T8.append(mul4(s, iG[3])) 139 | 140 | U1.append(mul4(t, iG[0])) 141 | U2.append(mul4(t, iG[1])) 142 | U3.append(mul4(t, iG[2])) 143 | U4.append(mul4(t, iG[3])) 144 | 145 | # round constants 146 | r_con = [1] 147 | r = 1 148 | for t in range(1, 30): 149 | r = mul(2, r) 150 | r_con.append(r) 151 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/remote_encrypted/rijndael/paddings.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class PaddingBase: 4 | def __init__(self, block_size): 5 | self.block_size = block_size 6 | 7 | def encode(self, source): 8 | raise NotImplementedError 9 | 10 | def decode(self, source): 11 | raise NotImplementedError 12 | 13 | 14 | class ZeroPadding(PaddingBase): 15 | """ 16 | Specified for hashes and MACs as Padding Method 1 in ISO/IEC 10118-1 and 17 | ISO/IEC 9797-1. 18 | """ 19 | 20 | def encode(self, source): 21 | pad_size = ( 22 | self.block_size - 23 | ( 24 | (len(source) + self.block_size - 1) % self.block_size + 1 25 | ) 26 | ) 27 | return source + b'\0' * pad_size 28 | 29 | def decode(self, source): 30 | assert len(source) % self.block_size == 0 31 | offset = len(source) 32 | if offset == 0: 33 | return b'' 34 | end = offset - self.block_size + 1 35 | 36 | while offset > end: 37 | offset -= 1 38 | if source[offset:offset+1] != b'\0': 39 | return source[:offset + 1] 40 | 41 | 42 | class Pkcs7Padding(PaddingBase): 43 | """ 44 | Technique for padding a string as defined in RFC 2315, section 10.3, 45 | note #2 46 | """ 47 | 48 | def encode(self, source): 49 | amount_to_pad = self.block_size - (len(source) % self.block_size) 50 | amount_to_pad = ( 51 | self.block_size if amount_to_pad == 0 else amount_to_pad 52 | ) 53 | pad = chr(amount_to_pad).encode() 54 | return source + pad * amount_to_pad 55 | 56 | def decode(self, source): 57 | return source[:-source[-1]] 58 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/upnp/UPNP_Device/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """UPNP device mapping tool""" 3 | 4 | 5 | import logging 6 | from logging import NullHandler 7 | 8 | logger = logging.getLogger(__name__) 9 | logger.addHandler(NullHandler()) 10 | logging.basicConfig(format="%(message)s", level=None) 11 | 12 | 13 | from .discover import discover as _discover # NOQA 14 | from .listen import listen # NOQA 15 | from .upnp_class import UPNPObject # NOQA 16 | 17 | 18 | def discover(timeout=5, log_level=None, ips=(), dump=''): 19 | for addr, locations in _discover(timeout, log_level, ips, dump): 20 | yield UPNPObject(addr, locations, dump) 21 | 22 | 23 | __title__ = "UPNP_Device" 24 | __version__ = "0.1.0b" 25 | __url__ = "https://github.com/kdschlosser/UPNP_Device" 26 | __author__ = "Kevin Schlosser" 27 | __author_email__ = "kevin.g.schlosser@gmail.com" 28 | __all__ = ( 29 | '__title__', '__version__', '__url__', '__author__', '__author_email__', 30 | 'discover' 31 | ) 32 | 33 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/upnp/UPNP_Device/icon.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import requests 4 | import logging 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class Icon(object): 9 | 10 | def __init__(self, parent, url, node): 11 | self.__parent = parent 12 | self.mime_type = None 13 | self.width = None 14 | self.height = None 15 | self.depth = None 16 | self.url = None 17 | 18 | for item in node: 19 | tag = item.tag 20 | try: 21 | text = int(item.text) 22 | except ValueError: 23 | text = item.text 24 | 25 | if tag == 'url': 26 | name = text.split('/')[-1] 27 | name = name.replace('.', '_') 28 | self.__name__ = name 29 | text = url + text 30 | 31 | setattr(self, tag, text) 32 | 33 | @property 34 | def data(self): 35 | content = requests.get(self.url).content 36 | return "".join(map(chr, list(content))) 37 | 38 | @property 39 | def access_point(self): 40 | return self.__parent.access_point + '.' + self.__name__ 41 | 42 | def __str__(self, indent=''): 43 | output = TEMPLATE.format( 44 | indent=indent, 45 | access_point=self.access_point, 46 | name=self.__name__, 47 | mime_type=self.mime_type, 48 | width=self.width, 49 | height=self.height, 50 | depth=self.depth, 51 | url=self.url, 52 | ) 53 | 54 | return output 55 | 56 | @property 57 | def as_dict(self): 58 | res = dict( 59 | name=self.__name__, 60 | mime_type=self.mime_type, 61 | width=self.width, 62 | height=self.height, 63 | depth=self.depth, 64 | url=self.url, 65 | ) 66 | return res 67 | 68 | 69 | TEMPLATE = ''' 70 | {indent}Icon name: {name} 71 | {indent}Access point: {access_point} 72 | {indent}---------------------------------------------- 73 | {indent} Mime Type: {mime_type} 74 | {indent} Width: {width} 75 | {indent} Height: {height} 76 | {indent} Color Depth: {depth} 77 | {indent} URL: {url} 78 | ''' 79 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/upnp/UPNP_Device/instance_singleton.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class InstanceSingleton(type): 5 | _objects = {} 6 | 7 | def __call__(cls, id, *args, **kwargs): 8 | 9 | if id not in InstanceSingleton._objects: 10 | InstanceSingleton._objects[id] = ( 11 | super(InstanceSingleton, cls).__call__(id, *args, **kwargs) 12 | ) 13 | else: 14 | try: 15 | InstanceSingleton._objects[id](id, *args, **kwargs) 16 | except TypeError: 17 | pass 18 | 19 | return InstanceSingleton._objects[id] 20 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_080b/upnp/UPNP_Device/xmlns.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ENVELOPE_XMLNS = 'http://schemas.xmlsoap.org/soap/envelope/' 4 | 5 | 6 | def strip_xmlns(root): 7 | def iter_node(n): 8 | nsmap = n.nsmap 9 | for child in n: 10 | nsmap.update(iter_node(child)) 11 | return nsmap 12 | 13 | xmlns = list('{' + item + '}' for item in iter_node(root).values()) 14 | 15 | def strip_node(n): 16 | for item in xmlns: 17 | n.tag = n.tag.replace(item, '') 18 | 19 | for child in n[:]: 20 | try: 21 | strip_node(child) 22 | except AttributeError: 23 | n.remove(child) 24 | strip_node(root) 25 | 26 | return root 27 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_qled/__init__.py: -------------------------------------------------------------------------------- 1 | """Remote control Samsung televisions via TCP/IP connection""" 2 | 3 | from .remote import Remote 4 | from .application import Application 5 | from .upnp import Upnp 6 | 7 | __title__ = "samsungctl" 8 | __version__ = "0.8" 9 | __url__ = "https://github.com/giefca/samsungctl" 10 | __author__ = "Lauri Niskanen" 11 | __author_email__ = "" 12 | __license__ = "MIT" 13 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_qled/application.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import requests 4 | 5 | APP_URL_FORMAT = "http://{}:8001/api/v2/applications/{}" 6 | 7 | APPS = {'YouTube': '111299001912', 8 | 'Plex': '3201512006963', 9 | 'Prime Video': '3201512006785', 10 | 'Universal Guide': '3201710015067', 11 | 'Netflix': '11101200001', 12 | 'Apple TV': '3201807016597', 13 | 'Steam Link': '3201702011851', 14 | 'MyCANAL': '3201606009910', 15 | # 'Browser': 'org.tizen.browser', 16 | 'Spotify': '3201606009684', 17 | 'Molotov': '3201611011210', 18 | 'SmartThings': '3201710015016', 19 | 'e-Manual': '20182100010', 20 | 'Google Play': '3201601007250', 21 | 'Gallery': '3201710015037', 22 | 'Rakuten TV': '3201511006428', 23 | 'RMC Sport': '3201704012212', 24 | 'MYTF1 VOD': '3201905018355', 25 | 'Blacknut': '3201811017333', 26 | 'Facebook Watch': '11091000000', 27 | 'McAfee Security for TV': '3201612011418', 28 | 'OCS': '3201703012029', 29 | 'Playzer': '3201810017091' 30 | } 31 | 32 | 33 | class Application: 34 | """ Handle applications.""" 35 | def __init__(self, config): 36 | self._ip = config['host'] 37 | 38 | def state(self, app): 39 | """ Get the state of the app.""" 40 | try: 41 | response = requests.get(APP_URL_FORMAT.format(self._ip, APPS[app]), timeout=0.2) 42 | return response.content.decode('utf-8') 43 | except: 44 | return """{"id":"","name":"","running":false,"version":"","visible":false}""" 45 | 46 | def is_running(self, app): 47 | """ Is the app running.""" 48 | app_state = json.loads(self.state(app)) 49 | return app_state['running'] 50 | 51 | def is_visible(self, app): 52 | """ Is the app visible.""" 53 | app_state = json.loads(self.state(app)) 54 | 55 | if 'visible' in app_state: 56 | return app_state['visible'] 57 | 58 | return False 59 | 60 | def start(self, app): 61 | """ Start an application.""" 62 | return os.system("curl -X POST " + APP_URL_FORMAT.format(self._ip, APPS[app])) 63 | 64 | def stop(self, app): 65 | """ Stop an application.""" 66 | return os.system("curl -X DELETE " + APP_URL_FORMAT.format(self._ip, APPS[app])) 67 | 68 | def current_app(self): 69 | """ Get the current visible app.""" 70 | current_app = None 71 | for app in APPS: 72 | if (self.is_visible(app) is True): 73 | current_app = app 74 | return current_app 75 | 76 | def app_list(self): 77 | """ List running apps.""" 78 | apps = [] 79 | for app in APPS: 80 | if (self.is_running(app) is True): 81 | apps.append(app) 82 | return apps -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_qled/exceptions.py: -------------------------------------------------------------------------------- 1 | class AccessDenied(Exception): 2 | """Connection was denied.""" 3 | pass 4 | 5 | 6 | class ConnectionClosed(Exception): 7 | """Connection was closed.""" 8 | pass 9 | 10 | 11 | class UnhandledResponse(Exception): 12 | """Received unknown response.""" 13 | pass 14 | 15 | 16 | class UnknownMethod(Exception): 17 | """Unknown method.""" 18 | pass 19 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_qled/interactive.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | 4 | _mappings = [ 5 | ["p", "KEY_POWEROFF", "P", "Power off"], 6 | ["KEY_UP", "KEY_UP", "Up", "Up"], 7 | ["KEY_DOWN", "KEY_DOWN", "Down", "Down"], 8 | ["KEY_LEFT", "KEY_LEFT", "Left", "Left"], 9 | ["KEY_RIGHT", "KEY_RIGHT", "Right", "Right"], 10 | ["KEY_PPAGE", "KEY_CHUP", "Page Up", "P Up"], 11 | ["KEY_NPAGE", "KEY_CHDOWN", "Page Down", "P Down"], 12 | ["\n", "KEY_ENTER", "Enter", "Enter"], 13 | ["KEY_BACKSPACE", "KEY_RETURN", "Backspace", "Return"], 14 | ["e", "KEY_EXIT", "E", "Exit"], 15 | ["h", "KEY_CONTENTS", "H", "Smart Hub"], 16 | ["l", "KEY_CH_LIST", "L", "Channel List"], 17 | ["m", "KEY_MENU", "M", "Menu"], 18 | ["s", "KEY_SOURCE", "S", "Source"], 19 | ["g", "KEY_GUIDE", "G", "Guide"], 20 | ["t", "KEY_TOOLS", "T", "Tools"], 21 | ["i", "KEY_INFO", "I", "Info"], 22 | ["z", "KEY_RED", "Z", "A / Red"], 23 | ["x", "KEY_GREEN", "X", "B / Green"], 24 | ["c", "KEY_YELLOW", "C", "C / Yellow"], 25 | ["v", "KEY_BLUE", "V", "D / Blue"], 26 | ["d", "KEY_PANNEL_CHDOWN", "D", "3D"], 27 | ["+", "KEY_VOLUP", "+", "Volume Up"], 28 | ["-", "KEY_VOLDOWN", "-", "Volume Down"], 29 | ["*", "KEY_MUTE", "*", "Mute"], 30 | ["0", "KEY_0", "0", "0"], 31 | ["1", "KEY_1", "1", "1"], 32 | ["2", "KEY_2", "2", "2"], 33 | ["3", "KEY_3", "3", "3"], 34 | ["4", "KEY_4", "4", "4"], 35 | ["5", "KEY_5", "5", "5"], 36 | ["6", "KEY_6", "6", "6"], 37 | ["7", "KEY_7", "7", "7"], 38 | ["8", "KEY_8", "8", "8"], 39 | ["9", "KEY_9", "9", "9"], 40 | ["KEY_F(1)", "KEY_DTV", "F1", "TV Source"], 41 | ["KEY_F(2)", "KEY_HDMI", "F2", "HDMI Source"], 42 | ] 43 | 44 | 45 | def run(remote): 46 | """Run interactive remote control application.""" 47 | curses.wrapper(_control, remote) 48 | 49 | 50 | def _control(stdscr, remote): 51 | height, width = stdscr.getmaxyx() 52 | 53 | stdscr.addstr("Interactive mode, press 'Q' to exit.\n") 54 | stdscr.addstr("Key mappings:\n") 55 | 56 | column_len = max(len(mapping[2]) for mapping in _mappings) + 1 57 | mappings_dict = {} 58 | for mapping in _mappings: 59 | mappings_dict[mapping[0]] = mapping[1] 60 | 61 | row = stdscr.getyx()[0] + 2 62 | if row < height: 63 | line = " {}= {} ({})\n".format(mapping[2].ljust(column_len), 64 | mapping[3], mapping[1]) 65 | stdscr.addstr(line) 66 | elif row == height: 67 | stdscr.addstr("[Terminal is too small to show all keys]\n") 68 | 69 | running = True 70 | while running: 71 | key = stdscr.getkey() 72 | 73 | if key == "q": 74 | running = False 75 | 76 | if key in mappings_dict: 77 | remote.control(mappings_dict[key]) 78 | 79 | try: 80 | stdscr.addstr(".") 81 | except curses.error: 82 | stdscr.deleteln() 83 | stdscr.move(stdscr.getyx()[0], 0) 84 | stdscr.addstr(".") 85 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_qled/remote.py: -------------------------------------------------------------------------------- 1 | from . import exceptions 2 | from .remote_legacy import RemoteLegacy 3 | from .remote_websocket import RemoteWebsocket 4 | from .application import Application 5 | 6 | 7 | class Remote: 8 | def __init__(self, config): 9 | if config["method"] == "legacy": 10 | self.remote = RemoteLegacy(config) 11 | elif config["method"] == "websocket": 12 | self.remote = RemoteWebsocket(config) 13 | else: 14 | raise exceptions.UnknownMethod() 15 | 16 | def __enter__(self): 17 | return self.remote.__enter__() 18 | 19 | def __exit__(self, type, value, traceback): 20 | self.remote.__exit__(type, value, traceback) 21 | 22 | def close(self): 23 | return self.remote.close() 24 | 25 | def control(self, key): 26 | return self.remote.control(key) 27 | 28 | def get_installed_apps(self): 29 | return self.remote.get_installed_apps() 30 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_qled/remote_legacy.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import logging 3 | import socket 4 | import time 5 | 6 | from . import exceptions 7 | 8 | 9 | class RemoteLegacy(): 10 | """Object for remote control connection.""" 11 | 12 | def __init__(self, config): 13 | if not config["port"]: 14 | config["port"] = 55000 15 | 16 | """Make a new connection.""" 17 | self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 18 | 19 | if config["timeout"]: 20 | self.connection.settimeout(config["timeout"]) 21 | 22 | self.connection.connect((config["host"], config["port"])) 23 | 24 | payload = b"\x64\x00" \ 25 | + self._serialize_string(config["description"]) \ 26 | + self._serialize_string(config["id"]) \ 27 | + self._serialize_string(config["name"]) 28 | packet = b"\x00\x00\x00" + self._serialize_string(payload, True) 29 | 30 | logging.info("Sending handshake.") 31 | self.connection.send(packet) 32 | self._read_response(True) 33 | 34 | def __enter__(self): 35 | return self 36 | 37 | def __exit__(self, type, value, traceback): 38 | self.close() 39 | 40 | def close(self): 41 | """Close the connection.""" 42 | if self.connection: 43 | self.connection.close() 44 | self.connection = None 45 | logging.debug("Connection closed.") 46 | 47 | def control(self, key): 48 | """Send a control command.""" 49 | if not self.connection: 50 | raise exceptions.ConnectionClosed() 51 | 52 | payload = b"\x00\x00\x00" + self._serialize_string(key) 53 | packet = b"\x00\x00\x00" + self._serialize_string(payload, True) 54 | 55 | logging.info("Sending control command: %s", key) 56 | self.connection.send(packet) 57 | self._read_response() 58 | time.sleep(self._key_interval) 59 | 60 | _key_interval = 0.2 61 | 62 | def _read_response(self, first_time=False): 63 | header = self.connection.recv(3) 64 | tv_name_len = int.from_bytes(header[1:3], 65 | byteorder="little") 66 | tv_name = self.connection.recv(tv_name_len) 67 | 68 | if first_time: 69 | logging.debug("Connected to '%s'.", tv_name.decode()) 70 | 71 | response_len = int.from_bytes(self.connection.recv(2), 72 | byteorder="little") 73 | response = self.connection.recv(response_len) 74 | 75 | if len(response) == 0: 76 | self.close() 77 | raise exceptions.ConnectionClosed() 78 | 79 | if response == b"\x64\x00\x01\x00": 80 | logging.debug("Access granted.") 81 | return 82 | elif response == b"\x64\x00\x00\x00": 83 | raise exceptions.AccessDenied() 84 | elif response[0:1] == b"\x0a": 85 | if first_time: 86 | logging.warning("Waiting for authorization...") 87 | return self._read_response() 88 | elif response[0:1] == b"\x65": 89 | logging.warning("Authorization cancelled.") 90 | raise exceptions.AccessDenied() 91 | elif response == b"\x00\x00\x00\x00": 92 | logging.debug("Control accepted.") 93 | return 94 | 95 | raise exceptions.UnhandledResponse(response) 96 | 97 | @staticmethod 98 | def _serialize_string(string, raw=False): 99 | if isinstance(string, str): 100 | string = str.encode(string) 101 | 102 | if not raw: 103 | string = base64.b64encode(string) 104 | 105 | return bytes([len(string)]) + b"\x00" + string 106 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_qled/remote_websocket.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | import socket 5 | import time 6 | import os 7 | import ssl 8 | 9 | from . import exceptions 10 | 11 | 12 | URL_FORMAT = "ws://{}:{}/api/v2/channels/samsung.remote.control?name={}" 13 | SSL_URL_FORMAT = "wss://{}:{}/api/v2/channels/samsung.remote.control?name={}" 14 | 15 | 16 | class RemoteWebsocket(): 17 | """Object for remote control connection.""" 18 | 19 | def __init__(self, config): 20 | import websocket 21 | self.token_file = os.path.dirname(os.path.realpath(__file__)) + "/token.txt" 22 | 23 | if not config["port"]: 24 | config["port"] = 8001 25 | 26 | if config["timeout"] == 0: 27 | config["timeout"] = None 28 | 29 | if config["port"] == 8001: 30 | url = URL_FORMAT.format(config["host"], config["port"], 31 | self._serialize_string(config["name"])) 32 | 33 | self.connection = websocket.create_connection(url, config["timeout"]) 34 | elif config["port"] == 8002: 35 | url = SSL_URL_FORMAT.format(config["host"], config["port"], 36 | self._serialize_string(config["name"])) 37 | if os.path.isfile(self.token_file): 38 | with open(self.token_file, "r") as token_file: 39 | url += "&token=" + token_file.readline() 40 | self.connection = websocket.create_connection(url, config["timeout"], sslopt={"cert_reqs": ssl.CERT_NONE}) 41 | 42 | self._read_response() 43 | 44 | def __enter__(self): 45 | return self 46 | 47 | def __exit__(self, type, value, traceback): 48 | self.close() 49 | 50 | def close(self): 51 | """Close the connection.""" 52 | if self.connection: 53 | self.connection.close() 54 | self.connection = None 55 | logging.debug("Connection closed.") 56 | 57 | def control(self, key): 58 | """Send a control command.""" 59 | if not self.connection: 60 | raise exceptions.ConnectionClosed() 61 | 62 | payload = json.dumps({ 63 | "method": "ms.remote.control", 64 | "params": { 65 | "Cmd": "Click", 66 | "DataOfCmd": key, 67 | "Option": "false", 68 | "TypeOfRemote": "SendRemoteKey" 69 | } 70 | }) 71 | 72 | logging.info("Sending control command: %s", key) 73 | self.connection.send(payload) 74 | time.sleep(self._key_interval) 75 | 76 | _key_interval = 0.2 77 | 78 | def _read_response(self): 79 | response = self.connection.recv() 80 | response = json.loads(response) 81 | 82 | if 'data' in response and 'token' in response["data"]: 83 | with open(self.token_file, "w") as token_file: 84 | token_file.write(response['data']["token"]) 85 | 86 | if response["event"] != "ms.channel.connect": 87 | self.close() 88 | raise exceptions.UnhandledResponse(response) 89 | 90 | logging.debug("Access granted.") 91 | 92 | @staticmethod 93 | def _serialize_string(string): 94 | if isinstance(string, str): 95 | string = str.encode(string) 96 | 97 | return base64.b64encode(string).decode("utf-8") 98 | 99 | def get_installed_apps(self): 100 | """Send a control command.""" 101 | if not self.connection: 102 | raise exceptions.ConnectionClosed() 103 | 104 | payload = json.dumps({ 105 | "method": "ms.channel.emit", 106 | "params": { 107 | "data": "", 108 | "event": "ed.installedApp.get", 109 | # "event": "ed.edenApp.get", 110 | "to": "host" 111 | } 112 | }) 113 | 114 | logging.info("Asking for installed apps.") 115 | self.connection.send(payload) 116 | data = json.loads(self.connection.recv()) 117 | installed_apps = [] 118 | for app in data['data']['data']: 119 | installed_apps.append([app['name'], app['appId']]) 120 | return installed_apps 121 | -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungctl_qled/upnp.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import requests 3 | 4 | class Upnp: 5 | def __init__(self, config): 6 | self._host = config['host'] 7 | self.mute = False 8 | self.volume = 0 9 | 10 | def SOAPrequest(self, action, arguments, protocole): 11 | headers = {'SOAPAction': '"urn:schemas-upnp-org:service:{protocole}:1#{action}"'.format(action=action, protocole=protocole), 'content-type': 'text/xml'} 12 | body = """ 13 | 14 | 15 | 16 | 0 17 | {arguments} 18 | 19 | 20 | """.format(action=action, arguments=arguments, protocole=protocole) 21 | response = None 22 | try: 23 | response = requests.post("http://{host}:9197/upnp/control/{protocole}1".format(host=self._host, protocole=protocole), data=body, headers=headers, timeout=0.2) 24 | response = response.content 25 | except: 26 | pass 27 | return response 28 | 29 | def get_volume(self): 30 | self.volume = 0 31 | response = self.SOAPrequest('GetVolume', "Master", 'RenderingControl') 32 | if (response is not None): 33 | volume_xml = response.decode('utf8') 34 | tree = ET.fromstring(volume_xml) 35 | for elem in tree.iter(tag='CurrentVolume'): 36 | self.volume = elem.text 37 | return self.volume 38 | 39 | def set_volume(self, volume): 40 | self.SOAPrequest('SetVolume', "Master{}".format(volume), 'RenderingControl') 41 | 42 | def get_mute(self): 43 | self.mute = False 44 | response = self.SOAPrequest('GetMute', "Master", 'RenderingControl') 45 | if (response is not None): 46 | mute_xml = response.decode('utf8') 47 | tree = ET.fromstring(mute_xml) 48 | for elem in tree.iter(tag='CurrentMute'): 49 | mute = elem.text 50 | if (int(mute) == 0): 51 | self.mute = False 52 | else: 53 | self.mute = True 54 | return self.mute 55 | 56 | def set_current_media(self, url): 57 | """ Set media to playback.""" 58 | self.SOAPrequest('SetAVTransportURI', "{url}".format(url=url), 'AVTransport') 59 | 60 | def play(self): 61 | """ Play media that was already set as current.""" 62 | self.SOAPrequest('Play', "1", 'AVTransport') -------------------------------------------------------------------------------- /custom_components/samsungtv_custom/samsungtvws/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SamsungTVWS - Samsung Smart TV WS API wrapper 3 | 4 | Copyright (C) 2019 Xchwarze 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) any later version. 10 | 11 | This library is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public 17 | License along with this library; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, 19 | Boston, MA 02110-1335 USA 20 | 21 | """ 22 | from .remote import SamsungTVWS 23 | -------------------------------------------------------------------------------- /custom_components/samsungtv_smart/api/shortcuts.py: -------------------------------------------------------------------------------- 1 | """ 2 | SamsungTVWS - Samsung Smart TV WS API wrapper 3 | 4 | Copyright (C) 2019 Xchwarze 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) any later version. 10 | 11 | This library is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public 17 | License along with this library; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, 19 | Boston, MA 02110-1335 USA 20 | 21 | """ 22 | 23 | 24 | class SamsungTVShortcuts: 25 | def __init__(self, remote): 26 | self.remote = remote 27 | 28 | # power 29 | def power(self): 30 | self.remote.send_key('KEY_POWER') 31 | 32 | # menu 33 | def home(self): 34 | self.remote.send_key('KEY_HOME') 35 | 36 | def menu(self): 37 | self.remote.send_key('KEY_MENU') 38 | 39 | def source(self): 40 | self.remote.send_key('KEY_SOURCE') 41 | 42 | def guide(self): 43 | self.remote.send_key('KEY_GUIDE') 44 | 45 | def tools(self): 46 | self.remote.send_key('KEY_TOOLS') 47 | 48 | def info(self): 49 | self.remote.send_key('KEY_INFO') 50 | 51 | # navigation 52 | def up(self): 53 | self.remote.send_key('KEY_UP') 54 | 55 | def down(self): 56 | self.remote.send_key('KEY_DOWN') 57 | 58 | def left(self): 59 | self.remote.send_key('KEY_LEFT') 60 | 61 | def right(self): 62 | self.remote.send_key('KEY_RIGHT') 63 | 64 | def enter(self, count=1): 65 | self.remote.send_key('KEY_ENTER') 66 | 67 | def back(self): 68 | self.remote.send_key('KEY_RETURN') 69 | 70 | # channel 71 | def channel_list(self): 72 | self.remote.send_key('KEY_CH_LIST') 73 | 74 | def channel(self, ch): 75 | for c in str(ch): 76 | self.digit(c) 77 | 78 | self.enter() 79 | 80 | def digit(self, d): 81 | self.remote.send_key('KEY_' + d) 82 | 83 | def channel_up(self): 84 | self.remote.send_key('KEY_CHUP') 85 | 86 | def channel_down(self): 87 | self.remote.send_key('KEY_CHDOWN') 88 | 89 | # volume 90 | def volume_up(self): 91 | self.remote.send_key('KEY_VOLUP') 92 | 93 | def volume_down(self): 94 | self.remote.send_key('KEY_VOLDOWN') 95 | 96 | def mute(self): 97 | self.remote.send_key('KEY_MUTE') 98 | 99 | # extra 100 | def red(self): 101 | self.remote.send_key('KEY_RED') 102 | 103 | def green(self): 104 | self.remote.send_key('KEY_GREEN') 105 | 106 | def yellow(self): 107 | self.remote.send_key('KEY_YELLOW') 108 | 109 | def blue(self): 110 | self.remote.send_key('KEY_BLUE') 111 | -------------------------------------------------------------------------------- /custom_components/samsungtv_smart/api/upnp.py: -------------------------------------------------------------------------------- 1 | # Smartthings TV integration# 2 | from aiohttp import ClientSession 3 | import async_timeout 4 | from typing import Optional 5 | import xml.etree.ElementTree as ET 6 | 7 | DEFAULT_TIMEOUT = 0.2 8 | 9 | 10 | class upnp: 11 | def __init__(self, host, session: Optional[ClientSession] = None): 12 | self._host = host 13 | self._mute = False 14 | self._volume = 0 15 | self._connected = False 16 | if session: 17 | self._session = session 18 | self._managed_session = False 19 | else: 20 | self._session = ClientSession() 21 | self._managed_session = True 22 | 23 | def __enter__(self): 24 | return self 25 | 26 | async def _SOAPrequest(self, action, arguments, protocole): 27 | headers = { 28 | "SOAPAction": f'"urn:schemas-upnp-org:service:{protocole}:1#{action}"', 29 | "content-type": "text/xml", 30 | } 31 | body = f""" 32 | 33 | 34 | 35 | 0 36 | {arguments} 37 | 38 | 39 | """ 40 | response = None 41 | try: 42 | async with async_timeout.timeout(DEFAULT_TIMEOUT): 43 | async with self._session.post( 44 | f"http://{self._host}:9197/upnp/control/{protocole}1", 45 | headers=headers, 46 | data=body, 47 | raise_for_status=True, 48 | ) as resp: 49 | response = await resp.content.read() 50 | self._connected = True 51 | except: 52 | self._connected = False 53 | 54 | return response 55 | 56 | @property 57 | def connected(self): 58 | return self._connected 59 | 60 | async def async_get_volume(self): 61 | response = await self._SOAPrequest( 62 | "GetVolume", "Master", "RenderingControl" 63 | ) 64 | if response is not None: 65 | volume_xml = response.decode("utf8") 66 | tree = ET.fromstring(volume_xml) 67 | for elem in tree.iter(tag="CurrentVolume"): 68 | self._volume = elem.text 69 | return self._volume 70 | 71 | async def async_set_volume(self, volume): 72 | await self._SOAPrequest( 73 | "SetVolume", 74 | f"Master{volume}", 75 | "RenderingControl", 76 | ) 77 | 78 | async def async_get_mute(self): 79 | response = await self._SOAPrequest( 80 | "GetMute", "Master", "RenderingControl" 81 | ) 82 | if response is not None: 83 | # mute_xml = response.decode('utf8') 84 | tree = ET.fromstring(response.decode("utf8")) 85 | mute = 0 86 | for elem in tree.iter(tag="CurrentMute"): 87 | mute = elem.text 88 | if int(mute) == 0: 89 | self._mute = False 90 | else: 91 | self._mute = True 92 | 93 | return self._mute 94 | 95 | async def async_set_current_media(self, url): 96 | """ Set media to playback and play it.""" 97 | try: 98 | await self._SOAPrequest( 99 | "SetAVTransportURI", 100 | f"{url}", 101 | "AVTransport", 102 | ) 103 | await self._SOAPrequest("Play", "1", "AVTransport") 104 | except Exception: 105 | pass 106 | 107 | async def async_play(self): 108 | """ Play media that was already set as current.""" 109 | try: 110 | await self._SOAPrequest("Play", "1", "AVTransport") 111 | except Exception: 112 | pass 113 | -------------------------------------------------------------------------------- /custom_components/samsungtv_smart/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the samsungtv_smart integration.""" 2 | from enum import Enum 3 | 4 | 5 | class AppLoadMethod(Enum): 6 | All = 1 7 | Default = 2 8 | NotLoad = 3 9 | 10 | 11 | class AppLaunchMethod(Enum): 12 | Standard = 1 13 | Remote = 2 14 | Rest = 3 15 | 16 | 17 | class PowerOnMethod(Enum): 18 | WOL = 1 19 | SmartThings = 2 20 | 21 | 22 | DOMAIN = "samsungtv_smart" 23 | 24 | MIN_HA_MAJ_VER = 2021 25 | MIN_HA_MIN_VER = 7 26 | __min_ha_version__ = f"{MIN_HA_MAJ_VER}.{MIN_HA_MIN_VER}.0" 27 | 28 | DATA_OPTIONS = "options" 29 | WS_PREFIX = "[Home Assistant]" 30 | 31 | ATTR_DEVICE_MAC = "device_mac" 32 | ATTR_DEVICE_MODEL = "device_model" 33 | ATTR_DEVICE_NAME = "device_name" 34 | ATTR_DEVICE_OS = "device_os" 35 | 36 | CONF_APP_LAUNCH_METHOD = "app_launch_method" 37 | CONF_APP_LIST = "app_list" 38 | CONF_APP_LOAD_METHOD = "app_load_method" 39 | CONF_CHANNEL_LIST = "channel_list" 40 | CONF_DEVICE_MODEL = "device_model" 41 | CONF_DEVICE_NAME = "device_name" 42 | CONF_DEVICE_OS = "device_os" 43 | CONF_DUMP_APPS = "dump_apps" 44 | CONF_EXT_POWER_ENTITY = "ext_power_entity" 45 | CONF_LOAD_ALL_APPS = "load_all_apps" 46 | CONF_LOGO_OPTION = "logo_option" 47 | CONF_PING_PORT = "ping_port" 48 | CONF_POWER_ON_DELAY = "power_on_delay" 49 | CONF_POWER_ON_METHOD = "power_on_method" 50 | CONF_SHOW_CHANNEL_NR = "show_channel_number" 51 | CONF_SOURCE_LIST = "source_list" 52 | CONF_SYNC_TURN_OFF = "sync_turn_off" 53 | CONF_SYNC_TURN_ON = "sync_turn_on" 54 | CONF_USE_MUTE_CHECK = "use_mute_check" 55 | CONF_USE_ST_CHANNEL_INFO = "use_st_channel_info" 56 | CONF_USE_ST_STATUS_INFO = "use_st_status_info" 57 | CONF_WOL_REPEAT = "wol_repeat" 58 | CONF_WS_NAME = "ws_name" 59 | 60 | # obsolete 61 | CONF_UPDATE_METHOD = "update_method" 62 | CONF_UPDATE_CUSTOM_PING_URL = "update_custom_ping_url" 63 | CONF_SCAN_APP_HTTP = "scan_app_http" 64 | 65 | DEFAULT_APP = "TV/HDMI" 66 | DEFAULT_PORT = 8001 67 | DEFAULT_POWER_ON_DELAY = 30 68 | DEFAULT_SOURCE_LIST = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} 69 | DEFAULT_TIMEOUT = 6 70 | 71 | MAX_WOL_REPEAT = 5 72 | 73 | RESULT_NOT_SUCCESSFUL = "not_successful" 74 | RESULT_NOT_SUPPORTED = "not_supported" 75 | RESULT_ST_DEVICE_USED = "st_device_used" 76 | RESULT_ST_DEVICE_NOT_FOUND = "st_device_not_found" 77 | RESULT_ST_MULTI_DEVICES = "st_multiple_device" 78 | RESULT_SUCCESS = "success" 79 | RESULT_WRONG_APIKEY = "wrong_api_key" 80 | 81 | SERVICE_SELECT_PICTURE_MODE = "select_picture_mode" 82 | SERVICE_SET_ART_MODE = "set_art_mode" 83 | 84 | SERVICE_TURN_OFF = "turn_off" 85 | SERVICE_TURN_ON = "turn_on" 86 | 87 | STD_APP_LIST = { 88 | # app_id: smartthings app id (if different and available) 89 | "org.tizen.browser": "", # Internet 90 | "11101200001": "RN1MCdNq8t.Netflix", # Netflix 91 | "3201907018807": "org.tizen.netflix-app", # Netflix (New) 92 | "111299001912": "9Ur5IzDKqV.TizenYouTube", # YouTube 93 | "3201512006785": "org.tizen.ignition", # Prime Video 94 | # "3201512006785": "evKhCgZelL.AmazonIgnitionLauncher2", # Prime Video 95 | "3201901017640": "MCmYXNxgcu.DisneyPlus", # Disney+ 96 | "11091000000": "4ovn894vo9.Facebook", # Facebook 97 | "3201601007250": "QizQxC7CUf.PlayMovies", # Google Play 98 | "3201606009684": "rJeHak5zRg.Spotify", # Spotify 99 | } 100 | -------------------------------------------------------------------------------- /custom_components/samsungtv_smart/diagnostics.py: -------------------------------------------------------------------------------- 1 | """Diagnostics support for Samsung TV Smart.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.components.diagnostics import REDACTED, async_redact_data 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_MAC 7 | from homeassistant.core import HomeAssistant, callback 8 | from homeassistant.helpers import device_registry as dr, entity_registry as er 9 | 10 | from .const import DOMAIN 11 | 12 | 13 | TO_REDACT = {CONF_API_KEY, CONF_MAC} 14 | 15 | 16 | async def async_get_config_entry_diagnostics( 17 | hass: HomeAssistant, entry: ConfigEntry 18 | ) -> dict: 19 | """Return diagnostics for a config entry.""" 20 | diag_data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)} 21 | 22 | yaml_data = hass.data[DOMAIN].get(entry.unique_id, {}) 23 | if yaml_data: 24 | diag_data["config_data"] = async_redact_data(yaml_data, TO_REDACT) 25 | 26 | device_id = entry.data.get(CONF_ID, entry.entry_id) 27 | hass_data = _async_device_ha_info(hass, device_id) 28 | if hass_data: 29 | diag_data["device"] = hass_data 30 | 31 | return diag_data 32 | 33 | 34 | @callback 35 | def _async_device_ha_info( 36 | hass: HomeAssistant, device_id: str 37 | ) -> dict | None: 38 | """Gather information how this TV device is represented in Home Assistant.""" 39 | 40 | device_registry = dr.async_get(hass) 41 | entity_registry = er.async_get(hass) 42 | hass_device = device_registry.async_get_device( 43 | identifiers={(DOMAIN, device_id)} 44 | ) 45 | if not hass_device: 46 | return None 47 | 48 | data = { 49 | "name": hass_device.name, 50 | "name_by_user": hass_device.name_by_user, 51 | "model": hass_device.model, 52 | "manufacturer": hass_device.manufacturer, 53 | "sw_version": hass_device.sw_version, 54 | "disabled": hass_device.disabled, 55 | "disabled_by": hass_device.disabled_by, 56 | "entities": {}, 57 | } 58 | 59 | hass_entities = er.async_entries_for_device( 60 | entity_registry, 61 | device_id=hass_device.id, 62 | include_disabled_entities=True, 63 | ) 64 | 65 | for entity_entry in hass_entities: 66 | if entity_entry.platform != DOMAIN: 67 | continue 68 | state = hass.states.get(entity_entry.entity_id) 69 | state_dict = None 70 | if state: 71 | state_dict = dict(state.as_dict()) 72 | # The entity_id is already provided at root level. 73 | state_dict.pop("entity_id", None) 74 | # The context doesn't provide useful information in this case. 75 | state_dict.pop("context", None) 76 | # Redact the `entity_picture` attribute as it contains a token. 77 | if "entity_picture" in state_dict["attributes"]: 78 | state_dict["attributes"] = { 79 | **state_dict["attributes"], 80 | "entity_picture": REDACTED, 81 | } 82 | 83 | data["entities"][entity_entry.entity_id] = { 84 | "name": entity_entry.name, 85 | "original_name": entity_entry.original_name, 86 | "disabled": entity_entry.disabled, 87 | "disabled_by": entity_entry.disabled_by, 88 | "entity_category": entity_entry.entity_category, 89 | "device_class": entity_entry.device_class, 90 | "original_device_class": entity_entry.original_device_class, 91 | "icon": entity_entry.icon, 92 | "original_icon": entity_entry.original_icon, 93 | "unit_of_measurement": entity_entry.unit_of_measurement, 94 | "state": state_dict, 95 | } 96 | 97 | return data 98 | -------------------------------------------------------------------------------- /custom_components/samsungtv_smart/services.yaml: -------------------------------------------------------------------------------- 1 | select_picture_mode: 2 | description: Send to samsung TV the command to change picture mode. 3 | fields: 4 | entity_id: 5 | name: Entity Name 6 | description: Name of the target entity 7 | required: true 8 | example: "media_player.tv" 9 | selector: 10 | entity: 11 | integration: samsungtv_smart 12 | picture_mode: 13 | name: Picture Mode 14 | description: 15 | Name of the picture mode to switch to. Possible options 16 | can be found in the picture_mode_list state attribute. 17 | required: true 18 | example: "Standard" 19 | selector: 20 | text: 21 | 22 | set_art_mode: 23 | description: Send to samsung TV the command to set art mode. 24 | fields: 25 | entity_id: 26 | name: Entity Name 27 | description: Name of the target entity 28 | required: true 29 | example: "media_player.tv" 30 | selector: 31 | entity: 32 | integration: samsungtv_smart 33 | -------------------------------------------------------------------------------- /custom_components/samsungtv_tizen/__init__.py: -------------------------------------------------------------------------------- 1 | """The samsungtv component.""" 2 | -------------------------------------------------------------------------------- /custom_components/samsungtv_tizen/exceptions.py: -------------------------------------------------------------------------------- 1 | class ConnectionFailure(Exception): 2 | """Error during connection.""" 3 | pass 4 | 5 | 6 | class ResponseError(Exception): 7 | """Error in response.""" 8 | pass 9 | 10 | 11 | class HttpApiError(Exception): 12 | """Error using HTTP API.""" 13 | pass 14 | -------------------------------------------------------------------------------- /custom_components/samsungtv_tizen/shortcuts.py: -------------------------------------------------------------------------------- 1 | """ 2 | SamsungTVWS - Samsung Smart TV WS API wrapper 3 | 4 | Copyright (C) 2019 Xchwarze 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) any later version. 10 | 11 | This library is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public 17 | License along with this library; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, 19 | Boston, MA 02110-1335 USA 20 | 21 | """ 22 | 23 | 24 | class SamsungTVShortcuts: 25 | def __init__(self, remote): 26 | self.remote = remote 27 | 28 | # power 29 | def power(self): 30 | self.remote.send_key('KEY_POWER') 31 | 32 | # menu 33 | def home(self): 34 | self.remote.send_key('KEY_HOME') 35 | 36 | def menu(self): 37 | self.remote.send_key('KEY_MENU') 38 | 39 | def source(self): 40 | self.remote.send_key('KEY_SOURCE') 41 | 42 | def guide(self): 43 | self.remote.send_key('KEY_GUIDE') 44 | 45 | def tools(self): 46 | self.remote.send_key('KEY_TOOLS') 47 | 48 | def info(self): 49 | self.remote.send_key('KEY_INFO') 50 | 51 | # navigation 52 | def up(self): 53 | self.remote.send_key('KEY_UP') 54 | 55 | def down(self): 56 | self.remote.send_key('KEY_DOWN') 57 | 58 | def left(self): 59 | self.remote.send_key('KEY_LEFT') 60 | 61 | def right(self): 62 | self.remote.send_key('KEY_RIGHT') 63 | 64 | def enter(self, count=1): 65 | self.remote.send_key('KEY_ENTER') 66 | 67 | def back(self): 68 | self.remote.send_key('KEY_RETURN') 69 | 70 | # channel 71 | def channel_list(self): 72 | self.remote.send_key('KEY_CH_LIST') 73 | 74 | def channel(self, ch): 75 | for c in str(ch): 76 | self.digit(c) 77 | 78 | self.enter() 79 | 80 | def digit(self, d): 81 | self.remote.send_key('KEY_' + d) 82 | 83 | def channel_up(self): 84 | self.remote.send_key('KEY_CHUP') 85 | 86 | def channel_down(self): 87 | self.remote.send_key('KEY_CHDOWN') 88 | 89 | # volume 90 | def volume_up(self): 91 | self.remote.send_key('KEY_VOLUP') 92 | 93 | def volume_down(self): 94 | self.remote.send_key('KEY_VOLDOWN') 95 | 96 | def mute(self): 97 | self.remote.send_key('KEY_MUTE') 98 | 99 | # extra 100 | def red(self): 101 | self.remote.send_key('KEY_RED') 102 | 103 | def green(self): 104 | self.remote.send_key('KEY_GREEN') 105 | 106 | def yellow(self): 107 | self.remote.send_key('KEY_YELLOW') 108 | 109 | def blue(self): 110 | self.remote.send_key('KEY_BLUE') 111 | -------------------------------------------------------------------------------- /custom_components/samsungtv_tizen/upnp.py: -------------------------------------------------------------------------------- 1 | #Smartthings TV integration# 2 | import requests 3 | import xml.etree.ElementTree as ET 4 | 5 | class upnp: 6 | 7 | def __init__(self, host): 8 | self.host = host 9 | self.mute = False 10 | self.volume = 0 11 | 12 | def __enter__(self): 13 | return self 14 | 15 | def SOAPrequest(self, action, arguments, protocole): 16 | headers = {'SOAPAction': '"urn:schemas-upnp-org:service:{protocole}:1#{action}"'.format(action=action, protocole=protocole), 'content-type': 'text/xml'} 17 | body = """ 18 | 19 | 20 | 21 | 0 22 | {arguments} 23 | 24 | 25 | """.format(action=action, arguments=arguments, protocole=protocole) 26 | response = None 27 | try: 28 | response = requests.post("http://{host}:9197/upnp/control/{protocole}1".format(host=self.host, protocole=protocole), data=body, headers=headers, timeout=0.1) 29 | response = response.content 30 | except: 31 | pass 32 | return response 33 | 34 | def get_volume(self): 35 | response = self.SOAPrequest('GetVolume', "Master", 'RenderingControl') 36 | if response is not None: 37 | volume_xml = response.decode('utf8') 38 | tree = ET.fromstring(volume_xml) 39 | for elem in tree.iter(tag='CurrentVolume'): 40 | self.volume = elem.text 41 | return self.volume 42 | 43 | def set_volume(self, volume): 44 | self.SOAPrequest('SetVolume', "Master{}".format(volume), 'RenderingControl') 45 | 46 | def get_mute(self): 47 | response = self.SOAPrequest('GetMute', "Master", 'RenderingControl') 48 | if response is not None: 49 | # mute_xml = response.decode('utf8') 50 | tree = ET.fromstring(response.decode('utf8')) 51 | mute = 0 52 | for elem in tree.iter(tag='CurrentMute'): 53 | mute = elem.text 54 | if (int(mute) == 0): 55 | self.mute = False 56 | else: 57 | self.mute = True 58 | return self.mute 59 | 60 | def set_current_media(self, url): 61 | """ Set media to playback and play it.""" 62 | try: 63 | self.SOAPrequest('SetAVTransportURI', "{url}".format(url=url), 'AVTransport') 64 | self.SOAPrequest('Play', "1", 'AVTransport') 65 | except Exception: 66 | pass 67 | 68 | def play(self): 69 | """ Play media that was already set as current.""" 70 | try: 71 | self.SOAPrequest('Play', "1", 'AVTransport') 72 | except Exception: 73 | pass 74 | 75 | -------------------------------------------------------------------------------- /custom_components/sectorperformance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/custom_components/sectorperformance/__init__.py -------------------------------------------------------------------------------- /custom_components/tuya_v2/aes_cbc.py: -------------------------------------------------------------------------------- 1 | """AES-CBC encryption and decryption for account info.""" 2 | 3 | import base64 as b64 4 | import json 5 | import random 6 | from binascii import a2b_hex, b2a_hex 7 | 8 | from Crypto.Cipher import AES 9 | 10 | AES_ACCOUNT_KEY = "o0o0o0" 11 | XOR_KEY = "00oo00" 12 | KEY_KEY = "oo00oo" 13 | 14 | 15 | class AesCBC: 16 | 17 | # random_16 18 | def random_16(self): 19 | str = "" 20 | return str.join( 21 | random.choice("abcdefghijklmnopqrstuvwxyz!@#$%^&*1234567890") 22 | for i in range(16) 23 | ) 24 | 25 | # add_to_16 26 | def add_to_16(self, text): 27 | if len(text.encode("utf-8")) % 16: 28 | add = 16 - (len(text.encode("utf-8")) % 16) 29 | else: 30 | add = 0 31 | text = text + ("\0" * add) 32 | return text.encode("utf-8") 33 | 34 | # cbc_encryption 35 | def cbc_encrypt(self, key, iv, text): 36 | key = key.encode("utf-8") 37 | mode = AES.MODE_CBC 38 | iv = bytes(iv, encoding="utf8") 39 | text = self.add_to_16(text) 40 | cryptos = AES.new(key, mode, iv) 41 | cipher_text = cryptos.encrypt(text) 42 | return str(b2a_hex(cipher_text), encoding="utf-8") 43 | 44 | # cbc_decryption 45 | def cbc_decrypt(self, key, iv, text): 46 | key = key.encode("utf-8") 47 | iv = bytes(iv, encoding="utf8") 48 | mode = AES.MODE_CBC 49 | cryptos = AES.new(key, mode, iv) 50 | plain_text = cryptos.decrypt(a2b_hex(text)) 51 | return bytes.decode(plain_text).rstrip("\0") 52 | 53 | # xor_encrypt 54 | def xor_encrypt(self, data, key): 55 | lkey = len(key) 56 | secret = [] 57 | num = 0 58 | for each in data: 59 | if num >= lkey: 60 | num = num % lkey 61 | secret.append(chr(ord(each) ^ ord(key[num]))) 62 | num += 1 63 | return b64.b64encode("".join(secret).encode()).decode() 64 | 65 | # xor_decrypt 66 | def xor_decrypt(self, secret, key): 67 | tips = b64.b64decode(secret.encode()).decode() 68 | lkey = len(key) 69 | secret = [] 70 | num = 0 71 | for each in tips: 72 | if num >= lkey: 73 | num = num % lkey 74 | secret.append(chr(ord(each) ^ ord(key[num]))) 75 | num += 1 76 | return "".join(secret) 77 | 78 | # json to dict 79 | def json_to_dict(self, json_str): 80 | return json.loads(json_str) 81 | 82 | # confuse str 83 | def b64_encrypt(self, text): 84 | return b64.b64encode(text.encode()).decode() 85 | 86 | # unconfuse str 87 | def b64_decrypt(self, text): 88 | return b64.b64decode(text).decode() 89 | -------------------------------------------------------------------------------- /custom_components/tuya_v2/base.py: -------------------------------------------------------------------------------- 1 | """Tuya Home Assistant Base Device Model.""" 2 | from __future__ import annotations 3 | 4 | from typing import Any 5 | 6 | from tuya_iot import TuyaDevice, TuyaDeviceManager 7 | 8 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 9 | from homeassistant.helpers.entity import Entity 10 | 11 | from .const import DOMAIN, TUYA_HA_SIGNAL_UPDATE_ENTITY 12 | 13 | 14 | class TuyaHaEntity(Entity): 15 | """Tuya base device.""" 16 | 17 | def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: 18 | """Init TuyaHaEntity.""" 19 | super().__init__() 20 | 21 | self.tuya_device = device 22 | self.tuya_device_manager = device_manager 23 | 24 | @staticmethod 25 | def remap(old_value, old_min, old_max, new_min, new_max): 26 | """Remap old_value to new_value.""" 27 | new_value = ((old_value - old_min) / (old_max - old_min)) * ( 28 | new_max - new_min 29 | ) + new_min 30 | return new_value 31 | 32 | @property 33 | def should_poll(self) -> bool: 34 | """Hass should not poll.""" 35 | return False 36 | 37 | @property 38 | def unique_id(self) -> str | None: 39 | """Return a unique ID.""" 40 | return f"ty{self.tuya_device.id}" 41 | 42 | @property 43 | def name(self) -> str | None: 44 | """Return Tuya device name.""" 45 | return self.tuya_device.name 46 | 47 | @property 48 | def device_info(self): 49 | """Return a device description for device registry.""" 50 | _device_info = { 51 | "identifiers": {(DOMAIN, f"{self.tuya_device.id}")}, 52 | "manufacturer": "tuya", 53 | "name": self.tuya_device.name, 54 | "model": self.tuya_device.product_name, 55 | } 56 | return _device_info 57 | 58 | @property 59 | def available(self) -> bool: 60 | """Return if the device is available.""" 61 | return self.tuya_device.online 62 | 63 | async def async_added_to_hass(self): 64 | """Call when entity is added to hass.""" 65 | self.async_on_remove( 66 | async_dispatcher_connect( 67 | self.hass, 68 | f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{self.tuya_device.id}", 69 | self.async_write_ha_state, 70 | ) 71 | ) 72 | 73 | def _send_command(self, commands: list[dict[str, Any]]) -> None: 74 | """Send command to the device.""" 75 | self.tuya_device_manager.send_commands(self.tuya_device.id, commands) 76 | -------------------------------------------------------------------------------- /custom_components/tuya_v2/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Tuya integration.""" 2 | 3 | DOMAIN = "tuya_v2" 4 | 5 | CONF_PROJECT_TYPE = "tuya_project_type" 6 | CONF_ENDPOINT = "endpoint" 7 | CONF_ACCESS_ID = "access_id" 8 | CONF_ACCESS_SECRET = "access_secret" 9 | CONF_USERNAME = "username" 10 | CONF_PASSWORD = "password" 11 | CONF_COUNTRY_CODE = "country_code" 12 | CONF_APP_TYPE = "tuya_app_type" 13 | 14 | TUYA_DISCOVERY_NEW = "tuya_v2_discovery_new_{}" 15 | TUYA_DEVICE_MANAGER = "tuya_device_manager" 16 | TUYA_HOME_MANAGER = "tuya_home_manager" 17 | TUYA_MQTT_LISTENER = "tuya_mqtt_listener" 18 | TUYA_HA_TUYA_MAP = "tuya_ha_tuya_map" 19 | TUYA_HA_DEVICES = "tuya_ha_devices" 20 | 21 | TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" 22 | 23 | TUYA_PROJECT_TYPE_INDUSTY_SOLUTIONS = "Custom Development" 24 | TUYA_PROJECT_TYPE_SMART_HOME = "Smart Home PaaS" 25 | 26 | TUYA_PROJECT_TYPES = { 27 | TUYA_PROJECT_TYPE_SMART_HOME: 0, 28 | TUYA_PROJECT_TYPE_INDUSTY_SOLUTIONS: 1, 29 | } 30 | 31 | TUYA_ENDPOINTS = { 32 | "America": "https://openapi.tuyaus.com", 33 | "China": "https://openapi.tuyacn.com", 34 | "Europe": "https://openapi.tuyaeu.com", 35 | "India": "https://openapi.tuyain.com", 36 | "Eastern America": "https://openapi-ueaz.tuyaus.com", 37 | "Western Europe": "https://openapi-weaz.tuyaeu.com", 38 | } 39 | 40 | TUYA_APP_TYPES = {"TuyaSmart": "tuyaSmart", "Smart Life": "smartlife"} 41 | 42 | PLATFORMS = [ 43 | "binary_sensor", 44 | "climate", 45 | "cover", 46 | "fan", 47 | "humidifier", 48 | "light", 49 | "number", 50 | "scene", 51 | "select", 52 | "sensor", 53 | "switch", 54 | "vacuum", 55 | ] 56 | -------------------------------------------------------------------------------- /custom_components/tuya_v2/scene.py: -------------------------------------------------------------------------------- 1 | """Support for Tuya scenes.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from typing import Any 6 | 7 | from tuya_iot import TuyaHomeManager, TuyaScene 8 | 9 | from homeassistant.components.scene import Scene 10 | from homeassistant.config_entries import ConfigEntry 11 | from homeassistant.core import HomeAssistant 12 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 13 | 14 | from .const import DOMAIN, TUYA_HOME_MANAGER 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | async def async_setup_entry( 20 | hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback 21 | ) -> None: 22 | """Set up tuya scenes.""" 23 | home_manager = hass.data[DOMAIN][entry.entry_id][TUYA_HOME_MANAGER] 24 | scenes = await hass.async_add_executor_job(home_manager.query_scenes) 25 | async_add_entities(TuyaHAScene(home_manager, scene) for scene in scenes) 26 | 27 | 28 | class TuyaHAScene(Scene): 29 | """Tuya Scene Remote.""" 30 | 31 | def __init__(self, home_manager: TuyaHomeManager, scene: TuyaScene) -> None: 32 | """Init Tuya Scene.""" 33 | super().__init__() 34 | self.home_manager = home_manager 35 | self.scene = scene 36 | 37 | @property 38 | def should_poll(self) -> bool: 39 | """Hass should not poll.""" 40 | return False 41 | 42 | @property 43 | def unique_id(self) -> str | None: 44 | """Return a unique ID.""" 45 | return f"tys{self.scene.scene_id}" 46 | 47 | @property 48 | def name(self) -> str | None: 49 | """Return Tuya scene name.""" 50 | return self.scene.name 51 | 52 | @property 53 | def device_info(self): 54 | """Return a device description for device registry.""" 55 | return { 56 | "identifiers": {(DOMAIN, f"{self.unique_id}")}, 57 | "manufacturer": "tuya", 58 | "name": self.scene.name, 59 | "model": "Tuya Scene", 60 | } 61 | 62 | @property 63 | def available(self) -> bool: 64 | """Return if the scene is enabled.""" 65 | return self.scene.enabled 66 | 67 | def activate(self, **kwargs: Any) -> None: 68 | """Activate the scene.""" 69 | self.home_manager.trigger_scene(self.scene.home_id, self.scene.scene_id) 70 | -------------------------------------------------------------------------------- /custom_components/tuya_v2/select.py: -------------------------------------------------------------------------------- 1 | """Support for Tuya Select entities.""" 2 | from __future__ import annotations 3 | 4 | import json 5 | import logging 6 | 7 | from homeassistant.components.select import DOMAIN as DEVICE_DOMAIN 8 | from homeassistant.components.select import SelectEntity 9 | from homeassistant.config_entries import ConfigEntry 10 | from homeassistant.core import HomeAssistant, callback 11 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 12 | from homeassistant.helpers.entity import Entity 13 | from tuya_iot import TuyaDevice, TuyaDeviceManager 14 | 15 | from .base import TuyaHaEntity 16 | from .const import ( 17 | DOMAIN, 18 | TUYA_DEVICE_MANAGER, 19 | TUYA_DISCOVERY_NEW, 20 | TUYA_HA_DEVICES, 21 | TUYA_HA_TUYA_MAP, 22 | ) 23 | 24 | _LOGGER = logging.getLogger(__name__) 25 | 26 | TUYA_SUPPORT_TYPE = { 27 | "xxj", # Diffuser 28 | "kfj", # Coffee Maker 29 | "sd", # Vacuum Robot 30 | } 31 | 32 | DPCODE_MODE = "mode" 33 | DPCODE_COUNTDOWN = "countdown" 34 | DPCODE_WORK_MODE = "work_mode" 35 | DPCODE_DIRECTIONCONTROL = "direction_control" 36 | 37 | # Coffee Maker 38 | # https://developer.tuya.com/en/docs/iot/f?id=K9gf4701ox167 39 | DPCODE_MATERIAL = "material" 40 | DPCODE_CONCENTRATIONSET = "concentration_set" 41 | DPCODE_CUPNUMBER = "cup_number" 42 | 43 | 44 | AUTO_GENERATE_DP_LIST = [ 45 | DPCODE_MODE, 46 | DPCODE_COUNTDOWN, 47 | DPCODE_WORK_MODE, 48 | DPCODE_MATERIAL, 49 | DPCODE_CONCENTRATIONSET, 50 | DPCODE_CUPNUMBER, 51 | DPCODE_DIRECTIONCONTROL 52 | ] 53 | 54 | 55 | async def async_setup_entry( 56 | hass: HomeAssistant, entry: ConfigEntry, async_add_entities 57 | ) -> None: 58 | """Set up tuya select dynamically through tuya discovery.""" 59 | _LOGGER.info("select init") 60 | 61 | hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ 62 | DEVICE_DOMAIN 63 | ] = TUYA_SUPPORT_TYPE 64 | 65 | @callback 66 | def async_discover_device(dev_ids): 67 | _LOGGER.info(f"select add-> {dev_ids}") 68 | if not dev_ids: 69 | return 70 | entities = _setup_entities(hass, entry, dev_ids) 71 | async_add_entities(entities) 72 | 73 | entry.async_on_unload( 74 | async_dispatcher_connect( 75 | hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device 76 | ) 77 | ) 78 | 79 | device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] 80 | device_ids = [] 81 | for (device_id, device) in device_manager.device_map.items(): 82 | if device.category in TUYA_SUPPORT_TYPE: 83 | device_ids.append(device_id) 84 | async_discover_device(device_ids) 85 | 86 | 87 | def get_auto_generate_data_points(status) -> list: 88 | dps = [] 89 | for data_point in AUTO_GENERATE_DP_LIST: 90 | if data_point in status: 91 | dps.append(data_point) 92 | 93 | return dps 94 | 95 | 96 | def _setup_entities( 97 | hass, entry: ConfigEntry, device_ids: list[str] 98 | ) -> list[Entity]: 99 | """Set up Tuya Select.""" 100 | device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] 101 | entities:list[Entity] = [] 102 | for device_id in device_ids: 103 | device = device_manager.device_map[device_id] 104 | if device is None: 105 | continue 106 | 107 | for data_point in get_auto_generate_data_points(device.status): 108 | entities.append(TuyaHaSelect(device, device_manager, data_point)) 109 | hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) 110 | return entities 111 | 112 | 113 | class TuyaHaSelect(TuyaHaEntity, SelectEntity): 114 | """Tuya Select Device.""" 115 | 116 | def __init__( 117 | self, device: TuyaDevice, device_manager: TuyaDeviceManager, code: str = "" 118 | ): 119 | self._code = code 120 | self._attr_current_option = None 121 | super().__init__(device, device_manager) 122 | 123 | @property 124 | def unique_id(self) -> str | None: 125 | return f"{super().unique_id}{self._code}" 126 | 127 | @property 128 | def name(self) -> str | None: 129 | """Return Tuya device name.""" 130 | return self.tuya_device.name + self._code 131 | 132 | @property 133 | def current_option(self) -> str: 134 | return self.tuya_device.status.get(self._code, None) 135 | 136 | def select_option(self, option: str) -> None: 137 | self._send_command([{"code": self._code, "value": option}]) 138 | 139 | @property 140 | def options(self) -> list: 141 | dp_range = json.loads(self.tuya_device.function.get(self._code).values) 142 | return dp_range.get("range", []) 143 | -------------------------------------------------------------------------------- /custom_components/wyzeapi/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Wyze Home Assistant Integration integration.""" 2 | 3 | DOMAIN = "wyzeapi" 4 | CONF_CLIENT = "wyzeapi_client" 5 | 6 | ACCESS_TOKEN = "access_token" 7 | REFRESH_TOKEN = "refresh_token" 8 | REFRESH_TIME = "refresh_time" 9 | 10 | WYZE_NOTIFICATION_TOGGLE = f"{DOMAIN}.wyze.notification.toggle" 11 | 12 | LOCK_UPDATED = f"{DOMAIN}.lock_updated" 13 | CAMERA_UPDATED = f"{DOMAIN}.camera_updated" 14 | # EVENT NAMES 15 | WYZE_CAMERA_EVENT = "wyze_camera_event" 16 | -------------------------------------------------------------------------------- /custom_components/wyzeapi/token_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from inspect import iscoroutinefunction 3 | 4 | from homeassistant.config_entries import ConfigEntry 5 | from homeassistant.const import CONF_USERNAME, CONF_PASSWORD 6 | from homeassistant.core import HomeAssistant 7 | from homeassistant.exceptions import ConfigEntryAuthFailed 8 | from wyzeapy.exceptions import AccessTokenError, LoginError 9 | from wyzeapy.wyze_auth_lib import Token 10 | 11 | from .const import DOMAIN, ACCESS_TOKEN, REFRESH_TOKEN, REFRESH_TIME 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | class TokenManager: 17 | hass: HomeAssistant = None 18 | config_entry: ConfigEntry = None 19 | 20 | def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): 21 | TokenManager.hass = hass 22 | TokenManager.config_entry = config_entry 23 | 24 | @staticmethod 25 | async def token_callback(token: Token = None): 26 | _LOGGER.debug("TokenManager: Received new token, updating config entry.") 27 | if TokenManager.hass.config_entries.async_entries(DOMAIN): 28 | for entry in TokenManager.hass.config_entries.async_entries(DOMAIN): 29 | TokenManager.hass.config_entries.async_update_entry( 30 | entry, 31 | data={ 32 | CONF_USERNAME: entry.data.get(CONF_USERNAME), 33 | CONF_PASSWORD: entry.data.get(CONF_PASSWORD), 34 | ACCESS_TOKEN: token.access_token, 35 | REFRESH_TOKEN: token.refresh_token, 36 | REFRESH_TIME: str(token.refresh_time), 37 | }, 38 | ) 39 | 40 | 41 | def token_exception_handler(func): 42 | async def inner_function(*args, **kwargs): 43 | try: 44 | if iscoroutinefunction(func): 45 | await func(*args, **kwargs) 46 | else: 47 | func(*args, **kwargs) 48 | except (AccessTokenError, LoginError) as err: 49 | _LOGGER.error("TokenManager detected a login issue please re-login.") 50 | raise ConfigEntryAuthFailed("Unable to login, please re-login.") from err 51 | 52 | return inner_function 53 | -------------------------------------------------------------------------------- /custom_components/wyzesense/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/custom_components/wyzesense/__init__.py -------------------------------------------------------------------------------- /custom_components/wyzesense/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "wyzesense", 3 | "name": "Wyze Sense Component", 4 | "documentation": "https://github.com/kevinvincent/wyzesense", 5 | "requirements": ["wyzesense==0.0.4","retry==0.9.2"], 6 | "dependencies": [], 7 | "codeowners": ["@kevinvincent"] 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/wyzesense/services.yaml: -------------------------------------------------------------------------------- 1 | # Describes the format for available wyzesense services 2 | 3 | scan: 4 | description: Allow new devices to join for the next 30s 5 | 6 | remove: 7 | description: Remove a device 8 | fields: 9 | mac: 10 | description: MAC address of the node to remove 11 | example: "777A4656" -------------------------------------------------------------------------------- /custom_components/yahoo_earnings/__init__.py: -------------------------------------------------------------------------------- 1 | """The yahoo_earnings sensor component.""" 2 | -------------------------------------------------------------------------------- /custom_components/yahoo_earnings/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "yahoo_earnings", 3 | "name": "yahoo_earnings", 4 | "documentation": "https://www.home-assistant.io/components/sensor", 5 | "requirements": ["random_user_agent==1.0.1", "lxml==4.4.2"], 6 | "dependencies": [], 7 | "version": "0.0.1", 8 | "codeowners": ["@arsaboo"] 9 | } 10 | -------------------------------------------------------------------------------- /customize_glob.yaml: -------------------------------------------------------------------------------- 1 | input_boolean.*: 2 | templates: 3 | icon_color: > 4 | if (state === 'on') return 'rgb(251, 210, 41)'; return 'rgb(54, 95, 140)'; 5 | sensor.*_futures_change: 6 | templates: 7 | icon_color: > 8 | if (state > 0) return 'rgb(50,205,50)'; else return 'rgb(178,34,34)'; 9 | sensor.*_futures_change_pct: 10 | templates: 11 | icon_color: > 12 | if (state < -5) return 'rgb(151, 26, 30)'; 13 | if (state < -4) return 'rgb(194, 39, 45)'; 14 | if (state < -3) return 'rgb(236, 27, 33)'; 15 | if (state < -2) return 'rgb(244, 101, 35)'; 16 | if (state < -1) return 'rgb(248, 147, 29)'; 17 | if (state < 0) return 'rgb(255, 194, 15)'; 18 | if (state < 1) return 'rgb(202, 219, 43)'; 19 | if (state < 2) return 'rgb(142, 198, 65)'; 20 | if (state < 3) return 'rgb(106, 158, 47)'; 21 | if (state < 4) return 'rgb(37, 145, 60)'; 22 | return 'rgb(0, 111, 58)'; 23 | -------------------------------------------------------------------------------- /emulated_hue_ids.json: -------------------------------------------------------------------------------- 1 | {"6": "switch.wemoporch", "3": "group.living_room_lights", "9": "light.lifx5", "1": "input_boolean.startrecording", "8": "light.gateway_light_34ce00813670", "7": "switch.driveway", "2": "light.kitchen_lights", "5": "switch.garage_relay_switch", "4": "input_boolean.abodeupdate"} -------------------------------------------------------------------------------- /frontend.yaml: -------------------------------------------------------------------------------- 1 | themes: !include_dir_merge_named themes 2 | -------------------------------------------------------------------------------- /gitupdate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git add . 4 | git status 5 | echo -n "Enter the Description for the Change: " [Minor Update] 6 | read CHANGE_MSG 7 | git commit -m "${CHANGE_MSG}" 8 | git push origin master 9 | 10 | exit 11 | -------------------------------------------------------------------------------- /groups.yaml: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | ## Groups 3 | ################################################################# 4 | 5 | Thermostats: 6 | - switch.downstairs_away 7 | - switch.bedroom_away 8 | - switch.upstairs_away 9 | 10 | Household: 11 | - device_tracker.meta_rashmi 12 | - device_tracker.meta_alok 13 | - device_tracker.meta_arnav 14 | -------------------------------------------------------------------------------- /ha_ss_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/ha_ss_1.png -------------------------------------------------------------------------------- /ha_ss_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/ha_ss_2.png -------------------------------------------------------------------------------- /ha_ss_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/ha_ss_3.png -------------------------------------------------------------------------------- /ha_ss_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/ha_ss_4.png -------------------------------------------------------------------------------- /ha_ss_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/ha_ss_5.png -------------------------------------------------------------------------------- /history.yaml: -------------------------------------------------------------------------------- 1 | use_include_order: True 2 | include: 3 | entities: 4 | - binary_sensor.garage_door 5 | - device_tracker.meta_rashmi 6 | - device_tracker.test_rashmi 7 | - person.rashmi 8 | - device_tracker.meta_alok 9 | - device_tracker.test_alok 10 | - person.alok 11 | - sensor.temperature_158d0001a35ba5 12 | - sensor.humidity_158d0001a35ba5 13 | - sensor.pressure_158d0001a35ba5 14 | -------------------------------------------------------------------------------- /input_boolean.yaml: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | ## Input_boolean 3 | ################################################################# 4 | 5 | dashbounty: 6 | name: Dash Bounty 7 | initial: off 8 | icon: mdi:security 9 | abodeupdate: 10 | name: Abode Updates 11 | icon: mdi:security 12 | homeautomation: 13 | name: Home Automation 14 | icon: mdi:home-automation 15 | startrecording: 16 | name: Camera Recording 17 | initial: off 18 | icon: mdi:record-rec 19 | tv: 20 | name: TV 21 | initial: off 22 | icon: mdi:television 23 | tvtime: 24 | name: TV Time 25 | initial: on 26 | icon: mdi:television-guide 27 | leeoalarm: 28 | name: Leeo Alarm 29 | initial: off 30 | icon: mdi:fire 31 | devmode: 32 | name: Developer Mode 33 | initial: off 34 | icon: mdi:tune 35 | sleeping: 36 | name: Sleeping 37 | initial: off 38 | icon: mdi:sleep 39 | lifxeffects: 40 | name: Lifx Colorloop 41 | initial: off 42 | icon: mdi:theme-light-dark 43 | master_occupied: 44 | name: Master Occupied 45 | initial: off 46 | icon: mdi:motion-sensor 47 | -------------------------------------------------------------------------------- /input_button.yaml: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | ## Input_button 3 | ################################################################# 4 | restart_pihole: 5 | name: Restart Pihole 6 | icon: mdi:restart 7 | -------------------------------------------------------------------------------- /input_datetime.yaml: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | ## Input_datetime 3 | ################################################################# 4 | 5 | master_time: 6 | name: Master timestamp 7 | has_time: true 8 | has_date: true 9 | upstairs_time: 10 | name: Upstairs timestamp 11 | has_time: true 12 | has_date: true 13 | downstairs_time: 14 | name: Downstairs timestamp 15 | has_time: true 16 | has_date: true 17 | -------------------------------------------------------------------------------- /input_number.yaml: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | ## Input_number 3 | ################################################################# 4 | 5 | portfoliovalue: 6 | icon: mdi:currency-usd 7 | name: Portfolio Value 8 | min: 1 9 | max: 5000000 10 | step: 0.01 11 | mode: box 12 | portfolioreturn: 13 | icon: mdi:chart-line-variant 14 | name: Portfolio Return 15 | min: 1 16 | max: 25 17 | portfoliochange: 18 | icon: mdi:currency-usd 19 | name: Portfolio Change 20 | min: -50000 21 | max: 50000 22 | step: 0.01 23 | mode: box 24 | harmonyvolume: 25 | icon: mdi:volume-high 26 | name: Volume 27 | initial: 30 28 | min: 1 29 | max: 100 30 | step: 1 31 | lifxbrightness: 32 | icon: mdi:brightness-6 33 | name: Brightness 34 | initial: 255 35 | min: 0 36 | max: 255 37 | step: 1 38 | lifxtransition: 39 | icon: mdi:timer 40 | name: Transition 41 | initial: 15 42 | min: 0 43 | max: 60 44 | step: 1 45 | lifxchange: 46 | icon: mdi:delta 47 | name: Change 48 | initial: 35 49 | min: 0 50 | max: 359 51 | step: 1 52 | lifxspread: 53 | icon: mdi:arrow-split-vertical 54 | name: Spread 55 | initial: 359 56 | min: 0 57 | max: 359 58 | step: 1 59 | master_prev_temp: 60 | icon: mdi:chart-line-variant 61 | name: Master Temperature 62 | min: 50 63 | max: 100 64 | upstairs_prev_temp: 65 | icon: mdi:chart-line-variant 66 | name: Upstairs Temperature 67 | min: 50 68 | max: 100 69 | downstairs_prev_temp: 70 | icon: mdi:chart-line-variant 71 | name: Downstairs Temperature 72 | min: 50 73 | max: 100 74 | master_prev_eff: 75 | icon: mdi:chart-line-variant 76 | name: Master Prev Efficiency 77 | min: 0 78 | max: 100 79 | upstairs_prev_eff: 80 | icon: mdi:chart-line-variant 81 | name: Upstairs Prev Efficiency 82 | min: 0 83 | max: 100 84 | downstairs_prev_eff: 85 | icon: mdi:chart-line-variant 86 | name: Downstairs Prev Efficiency 87 | min: 0 88 | max: 100 89 | -------------------------------------------------------------------------------- /input_select.yaml: -------------------------------------------------------------------------------- 1 | abodestatus: 2 | name: Abode status 3 | options: 4 | - disarmed 5 | - armed_home 6 | - armed_away 7 | initial: disarmed 8 | icon: mdi:security 9 | livingroomharmony: 10 | name: Harmony Activity 11 | options: 12 | - PowerOff 13 | - Watch Fire TV 14 | - Youtube 15 | - SATV 16 | - Watch Apple TV 17 | initial: PowerOff 18 | icon: mdi:remote 19 | hdmiswitcher: 20 | name: HDMI Switcher 21 | options: 22 | - AppleTV 23 | - FireTV 24 | - Shield 25 | initial: Shield 26 | icon: mdi:remote 27 | hdmiinput: 28 | name: HDMI Input 29 | options: 30 | - InputHdmi1 31 | - InputHdmi2 32 | - InputHDMI3 33 | - InputHdmi4 34 | initial: InputHdmi4 35 | icon: mdi:remote 36 | 37 | current_theme: 38 | name: 'Current Theme' 39 | options: 40 | - 'default' 41 | - 'oxfordblue' 42 | - 'teal' 43 | - 'darkorange' 44 | - 'darkred' 45 | - 'darkcyan' 46 | - 'Google - Dark' 47 | - 'Google - Light' 48 | - 'sweetpink' 49 | initial: 'default' 50 | icon: 'mdi:palette' 51 | -------------------------------------------------------------------------------- /input_text.yaml: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | ## Input_text 3 | ################################################################# 4 | 5 | porch: 6 | name: Porch Camera Tags 7 | initial: Tags 8 | patio: 9 | name: Patio Camera Tags 10 | initial: Tags 11 | driveway: 12 | name: Driveway Camera Tags 13 | initial: Tags 14 | backyard: 15 | name: Backyard Camera Tags 16 | initial: Tags 17 | porchfaces: 18 | name: Porch Camera Faces 19 | initial: 0 20 | patiofaces: 21 | name: Patio Camera Faces 22 | initial: 0 23 | drivewayfaces: 24 | name: Driveway Camera Faces 25 | initial: 0 26 | backyardfaces: 27 | name: Backyard Camera Faces 28 | initial: 0 29 | -------------------------------------------------------------------------------- /ios.yaml: -------------------------------------------------------------------------------- 1 | actions: 2 | - name: Skip Track 3 | background_color: "#000000" 4 | label: 5 | text: Skip Track 6 | color: "#ff0000" 7 | icon: 8 | icon: skip-next-circle-outline 9 | color: "#ffffff" 10 | - name: Temperature report 11 | background_color: "#000000" 12 | label: 13 | text: Report 14 | color: "#ff0000" 15 | icon: 16 | icon: temperature-fahrenheit 17 | color: "#ffffff" 18 | - name: Set lights 19 | background_color: "#000000" 20 | label: 21 | text: Set lights 22 | color: "#ff0000" 23 | icon: 24 | icon: lamp 25 | color: "#ffffff" 26 | - name: Set network 27 | background_color: "#000000" 28 | label: 29 | text: Set network 30 | color: "#ff0000" 31 | icon: 32 | icon: router-wireless-settings 33 | color: "#ffffff" 34 | push: 35 | categories: 36 | - name: Abode Alarm 37 | identifier: 'abode_alarm' 38 | actions: 39 | - identifier: 'SOUND_ALARM' 40 | title: 'Sound Alarm' 41 | activationMode: 'background' 42 | authenticationRequired: yes 43 | destructive: yes 44 | behavior: 'default' 45 | - identifier: 'DISARM_ABODE' 46 | title: 'Disarm Abode' 47 | activationMode: 'background' 48 | authenticationRequired: yes 49 | destructive: no 50 | behavior: 'default' 51 | - name: Abode Updates 52 | identifier: 'abode_updates' 53 | actions: 54 | - identifier: 'ENABLE_ABODE_UPDATES' 55 | title: 'Enable Abode Updates' 56 | activationMode: 'background' 57 | authenticationRequired: no 58 | destructive: yes 59 | behavior: 'default' 60 | - name: Home Automation 61 | identifier: 'home_automation' 62 | actions: 63 | - identifier: 'ENABLE_HOME_AUTOMATION' 64 | title: 'Enable Home Automation' 65 | activationMode: 'background' 66 | authenticationRequired: no 67 | destructive: yes 68 | behavior: 'default' 69 | # Apple Watch 70 | - name: Set lights 71 | identifier: set_lights 72 | actions: 73 | - identifier: MASTER 74 | title: Master 75 | - identifier: LIVING_ROOM 76 | title: Living room 77 | - identifier: KITCHEN 78 | title: Kitchen 79 | - name: Light options 80 | identifier: light_options 81 | actions: 82 | - identifier: "OFF" 83 | title: "Off" 84 | - identifier: "ON" 85 | title: "On" 86 | 87 | - name: Set network 88 | identifier: set_network 89 | actions: 90 | - identifier: RISHAANFIREHD 91 | title: Rishaan FireHD 92 | - identifier: ARNAV_IPAD 93 | title: Arnav iPad 94 | - identifier: LENOVO 95 | title: Lenovo 96 | - identifier: TOSHIBA_AIO 97 | title: ToshibaAIO 98 | - identifier: SAMMY_TV 99 | title: Sammy TV 100 | - identifier: PIHOLE_MAIN 101 | title: PiHole Main 102 | - identifier: PIHOLE_KIDS 103 | title: PiHole Kids 104 | - name: Device options 105 | identifier: device_options 106 | actions: 107 | - identifier: "OFF" 108 | title: "Off" 109 | - identifier: "ON" 110 | title: "On" 111 | -------------------------------------------------------------------------------- /logbook.yaml: -------------------------------------------------------------------------------- 1 | include: 2 | domains: 3 | - alarm_control_panel 4 | - climate 5 | - cover 6 | - input_boolean 7 | - input_select 8 | - light 9 | - media_player 10 | - switch 11 | - person 12 | - logbook 13 | entities: 14 | - binary_sensor.alok_home 15 | - binary_sensor.back_door 16 | - binary_sensor.cube_158d0001035aa7 17 | - binary_sensor.door_window_sensor_158d0001bf26df 18 | - binary_sensor.front_door 19 | - binary_sensor.garage_entry_door 20 | - binary_sensor.key_fob 21 | - binary_sensor.motion_sensor_158d0001a1f2ab 22 | - binary_sensor.rashmi_home 23 | - binary_sensor.water_leak_sensor_158d0001d77800 24 | - device_tracker.meta_rashmi 25 | - device_tracker.meta_alok 26 | - device_tracker.rashmiphone_rashmiphone_2 27 | - device_tracker.rashmiappiphone_2 28 | - device_tracker.rashmiiphone 29 | - device_tracker.life360_sonu 30 | - device_tracker.275f08b3_dd14_459f_81b9_6c5b02c9b54e 31 | - device_tracker.alokphone_alokphone_2 32 | - device_tracker.alokiphone_3 33 | - device_tracker.alokiphone 34 | - device_tracker.life360_alok_saboo 35 | - device_tracker.alok_geofency 36 | - device_tracker.alok_composite 37 | - sensor.living_room 38 | - sensor.plex 39 | - sensor.season 40 | - sensor.speedtest_download 41 | - sensor.ssl_certificate_expiry 42 | - sensor.steps_alok 43 | - sensor.steps_rashmi 44 | - sensor.sonos_stereo 45 | - sensor.cube_last_action 46 | - switch.shelly_garage 47 | - automation.turn_off_master_lights 48 | - automation.turn_on_master_lights_on_motion 49 | - image_processing.opencv_porch 50 | - image_processing.opencv_patio 51 | - image_processing.opencv_driveway 52 | - image_processing.opencv_backyard 53 | - image_processing.tensorflow_porch 54 | - image_processing.tensorflow_patio 55 | - image_processing.tensorflow_driveway 56 | - image_processing.tensorflow_backyard 57 | exclude: 58 | domains: 59 | - sun 60 | - binary_sensor 61 | - camera 62 | - device_tracker 63 | - group 64 | - input_number 65 | - input_text 66 | - script 67 | - sensor 68 | - weather 69 | - zone 70 | - automation 71 | entities: 72 | - sensor.last_boot 73 | - input_boolean.master_occupied 74 | - light.master_1 75 | - light.master_2 76 | - light.master_3 77 | - sensor.date 78 | - input_number.harmonyvolume 79 | - light.lifx3 80 | - light.lifxnrguest 81 | - light.lifxnrkitchen 82 | - switch.auto_away 83 | - switch.auto_home 84 | - switch.backyardmotion 85 | - switch.drivewaymotion 86 | - switch.patiomotion 87 | - switch.porchmotion 88 | - switch.study_fan 89 | -------------------------------------------------------------------------------- /lovelace.yaml: -------------------------------------------------------------------------------- 1 | mode: yaml 2 | resources: 3 | - url: /hacsfiles/weather-card/weather-card.js 4 | type: module 5 | - url: /hacsfiles/pc-card/pc-card.js 6 | type: module 7 | - url: /local/custom_ui/circle-sensor-card.js 8 | type: module 9 | - url: /hacsfiles/lovelace-thermostat-card/main.js 10 | type: module 11 | - url: /hacsfiles/button-card/button-card.js 12 | type: module 13 | - url: /hacsfiles/restriction-card/restriction-card.js 14 | type: module 15 | - url: https://unpkg.com/moment@2.22.2/moment.js 16 | type: js 17 | - url: /hacsfiles/lovelace-slider-entity-row/slider-entity-row.js 18 | type: js 19 | - url: /hacsfiles/mini-media-player/mini-media-player-bundle.js 20 | type: module 21 | - url: /hacsfiles/mini-graph-card/mini-graph-card-bundle.js 22 | type: module 23 | - url: /hacsfiles/lovelace-gap-card/gap-card.js 24 | type: module 25 | - url: /hacsfiles/lovelace-layout-card/layout-card.js 26 | type: module 27 | - url: /local/custom_ui/card-modder.js 28 | type: js 29 | - url: /hacsfiles/lovelace-card-mod/card-mod.js 30 | type: module 31 | - url: /hacsfiles/firetv-card/firetv-card.js 32 | type: module 33 | - url: /hacsfiles/tv-card/tv-card.js 34 | type: module 35 | - url: /hacsfiles/lovelace-auto-entities/auto-entities.js 36 | type: module 37 | - url: /hacsfiles/config-template-card/config-template-card.js 38 | type: module 39 | - url: /hacsfiles/secondaryinfo-entity-row/secondaryinfo-entity-row.js 40 | type: module 41 | - url: /hacsfiles/simple-thermostat/simple-thermostat.js 42 | type: module 43 | - url: /hacsfiles/lovelace-hass-aarlo/hass-aarlo.js 44 | type: module 45 | - url: /hacsfiles/list-card/list-card.js 46 | type: js 47 | - url: /hacsfiles/unused-card/unused-card.js 48 | type: module 49 | - url: /hacsfiles/lovelace-card-tools/card-tools.js 50 | type: module 51 | - url: /hacsfiles/lovelace-template-entity-row/template-entity-row.js 52 | type: module 53 | - url: /hacsfiles/harmony-card/harmony-card.js 54 | type: module 55 | - url: /hacsfiles/lovelace-multiple-entity-row/multiple-entity-row.js 56 | type: module 57 | - url: /hacsfiles/vertical-stack-in-card/vertical-stack-in-card.js 58 | type: module 59 | - url: /hacsfiles/banner-card/banner-card.js 60 | type: module 61 | - url: /hacsfiles/bar-card/bar-card.js 62 | type: module 63 | - url: /hacsfiles/uptime-card/uptime-card.js 64 | type: module 65 | -------------------------------------------------------------------------------- /packages/homebridge.yaml: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | ## Package / Homebridge 3 | ################################################################ 4 | 5 | ################################################ 6 | ## Customize 7 | ################################################ 8 | 9 | homeassistant: 10 | customize_domain: 11 | alarm_control_panel: 12 | homebridge_hidden: false 13 | 14 | binary_sensor: 15 | homebridge_hidden: true 16 | 17 | climate: 18 | homebridge_hidden: true 19 | 20 | cover: 21 | homebridge_hidden: false 22 | 23 | device_tracker: 24 | homebridge_hidden: true 25 | 26 | fan: 27 | homebridge_hidden: true 28 | 29 | group: 30 | homebridge_hidden: true 31 | 32 | input_boolean: 33 | homebridge_hidden: true 34 | 35 | light: 36 | homebridge_hidden: true 37 | 38 | lock: 39 | homebridge_hidden: false 40 | 41 | media_player: 42 | homebridge_hidden: true 43 | 44 | scene: 45 | homebridge_hidden: true 46 | 47 | remote: 48 | homebridge_hidden: true 49 | 50 | sensor: 51 | homebridge_hidden: true 52 | 53 | switch: 54 | homebridge_hidden: true 55 | -------------------------------------------------------------------------------- /packages/opencv.yaml: -------------------------------------------------------------------------------- 1 | homeassistant: 2 | customize: 3 | sensor.secrets: 4 | base_url: !secret base_url 5 | image_processing.opencv_backyard: 6 | camera: camera.backyard 7 | image_processing.opencv_patio: 8 | camera: camera.patio 9 | image_processing.opencv_driveway: 10 | camera: camera.driveway 11 | image_processing.opencv_porch: 12 | camera: camera.porch 13 | image_processing.face_detected_backyard: 14 | camera: camera.backyard 15 | image_processing.face_detected_patio: 16 | camera: camera.patio 17 | image_processing.face_detected_driveway: 18 | camera: camera.driveway 19 | image_processing.face_detected_porch: 20 | camera: camera.porch 21 | automation: 22 | - id: log_opencv 23 | alias: 'OpenCV detection' 24 | initial_state: 'off' 25 | trigger: 26 | - platform: state 27 | entity_id: 28 | - image_processing.opencv_backyard 29 | - image_processing.opencv_patio 30 | - image_processing.opencv_driveway 31 | - image_processing.opencv_porch 32 | condition: 33 | - condition: template 34 | value_template: > 35 | {{ trigger.to_state.attributes.total_matches > 0 }} 36 | action: 37 | - service: logbook.log 38 | data_template: 39 | name: "OpenCV activity: " 40 | message: >- 41 | {{ dict(trigger.to_state.attributes)|tojson }} 42 | - service: notify.telegram 43 | data_template: 44 | title: "OpenCV: " 45 | message: >- 46 | {{ dict(trigger.to_state.attributes)|tojson }} 47 | data: 48 | photo: 49 | - url: >- 50 | {% set camera = trigger.to_state.attributes.camera.split('.')[1] %} 51 | {{ states.sensor.secrets.attributes.base_url ~ states.camera[camera].attributes.entity_picture }} 52 | caption: >- 53 | {{ trigger.to_state.attributes.matches.Face }} 54 | # - id: dlib_person_detected 55 | # alias: Dlib person detected 56 | # initial_state: 'on' 57 | # trigger: 58 | # platform: event 59 | # event_type: 60 | # - image_processing.face_detected_backyard 61 | # - image_processing.face_detected_driveway 62 | # - image_processing.face_detected_patio 63 | # - image_processing.face_detected_porch 64 | # action: 65 | # - service: logbook.log 66 | # data_template: 67 | # name: "Person Detected: " 68 | # message: >- 69 | # {{ dict(trigger.to_state.attributes)|tojson }} 70 | # - service: notify.telegram 71 | # data_template: 72 | # title: "Person Detected: " 73 | # message: >- 74 | # {{ dict(trigger.to_state.attributes)|tojson }} 75 | # data: 76 | # photo: 77 | # - url: >- 78 | # {% set camera = trigger.to_state.attributes.camera.split('.')[1] %} 79 | # {{ states.sensor.secrets.attributes.base_url ~ states.camera[camera].attributes.entity_picture }} 80 | 81 | sensor: 82 | - platform: template 83 | sensors: 84 | secrets: 85 | value_template: "Secrets Container" 86 | -------------------------------------------------------------------------------- /packages/synology_surveillance.yaml: -------------------------------------------------------------------------------- 1 | #Create triggers in Action Rule and obtain the respective URLs. The format is: 2 | # ss_enable_home: 'http://IP_ADDRESS:5000/webapi/entry.cgi?api=SYNO.SurveillanceStation.ExternalEvent&method="Trigger"&version=1&eventId=1&eventName="This is external event1"&account="USER"&password="PASSWORD"' 3 | rest_command: 4 | ss_enable_home: 5 | url: !secret ss_enable_home 6 | method: get 7 | ss_disable_home: 8 | url: !secret ss_disable_home 9 | method: get 10 | -------------------------------------------------------------------------------- /python_scripts/dark_sky_friendly_names.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Author: Alok R. Saboo 3 | # Description: This script takes the dark_sky forecast sensors and updates 4 | # their friendly_name to include the date and day. 5 | # https://github.com/arsaboo/homeassistant-config/blob/master/packages/weather.yaml 6 | ############################################################################### 7 | dark_sky_entities = ["sensor.forecast_1", "sensor.forecast_2", "sensor.forecast_3", 8 | "sensor.forecast_4", "sensor.forecast_5", "sensor.forecast_6", 9 | "sensor.forecast_7"] 10 | days = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"] 11 | 12 | triggeredEntity = data.get('entity_id') 13 | # logger.warning("trigger is {}".format(triggeredEntity)) 14 | now = datetime.datetime.now() 15 | today = now.weekday() 16 | 17 | if triggeredEntity is None: 18 | for entity_id in dark_sky_entities: 19 | # copy it's state 20 | state = hass.states.get(entity_id) 21 | newState = state.state 22 | forecastdays = int(entity_id.split('_')[1]) 23 | day = datetime.timedelta(days = forecastdays) 24 | forecastdate = now + day 25 | newEntityPicture = state.attributes.get('entity_picture') 26 | if today + forecastdays > 6: 27 | newDay = days[today + forecastdays - 7] 28 | else: 29 | newDay = days[today + forecastdays] 30 | # Set states 31 | hass.states.set(entity_id, newState, { 32 | 'friendly_name': "{} ({}/{})".format(newDay, forecastdate.month, forecastdate.day), 33 | 'entity_picture': newEntityPicture, 34 | }) 35 | else: 36 | state = hass.states.get(triggeredEntity) 37 | newState = state.state 38 | forecastdays = int(triggeredEntity.split('_')[1]) 39 | day = datetime.timedelta(days = forecastdays) 40 | forecastdate = now + day 41 | newEntityPicture = state.attributes.get('entity_picture') 42 | if today + forecastdays > 6: 43 | newDay = days[today + forecastdays - 7] 44 | else: 45 | newDay = days[today + forecastdays] 46 | # Set states 47 | hass.states.set(triggeredEntity, newState, { 48 | 'friendly_name': "{} ({}/{})".format(newDay, forecastdate.month, forecastdate.day), 49 | 'entity_picture': newEntityPicture, 50 | }) 51 | -------------------------------------------------------------------------------- /python_scripts/fade_in_light.py: -------------------------------------------------------------------------------- 1 | # GE Dimmer Switch Connected to SmartThings via MQTT 2 | # Python script to turn it on over a period of X minutes 3 | entity_id = data.get('entity_id') 4 | sleep_delay = int(data.get('delay_in_sec')) 5 | start_level_pct = int(data.get('start_level_pct')) 6 | end_level_pct = int(data.get('end_level_pct')) 7 | step_pct = int(data.get('step_in_level_pct')) 8 | 9 | start_level = int(255*start_level_pct/100) 10 | end_level = int(255*end_level_pct/100) 11 | step = int(255*step_pct/100) 12 | 13 | new_level = start_level 14 | while new_level < end_level 15 | states = hass.states.get(entity_id) 16 | current_level = states.attributes.get('brightness') or 0 17 | if (current_level > new_level) : 18 | logger.info('Exiting Fade In') 19 | break; 20 | else : 21 | logger.info('Setting brightness of ' + str(entity_id) + ' from ' + current_level + ' to ' + new_level) 22 | data = { "entity_id" : entity_id, "brightness" : new_level } 23 | hass.services.call('light', 'turn_on', data) 24 | new_level = new_level + step 25 | time.sleep(sleep_delay) 26 | -------------------------------------------------------------------------------- /python_scripts/initialize_tracker.py: -------------------------------------------------------------------------------- 1 | AlokFriendlyName = 'Alok Test Tracker' 2 | AlokEntityPicture = '/local/icons/Alok.png' 3 | AloktrackerName = 'device_tracker.test_alok' 4 | 5 | RashmiFriendlyName = 'Rashmi Test Tracker' 6 | RashmiEntityPicture = '/local/icons/Rashmi.png' 7 | RashmitrackerName = 'device_tracker.test_rashmi' 8 | 9 | AlokcurrentState = hass.states.get(AloktrackerName) 10 | AloknewStatus = AlokcurrentState.state 11 | 12 | RashmicurrentState = hass.states.get(RashmitrackerName) 13 | RashminewStatus = RashmicurrentState.state 14 | 15 | # Create device_tracker.test_alok entity 16 | hass.states.set(AloktrackerName, AloknewStatus, { 17 | 'friendly_name': AlokFriendlyName, 18 | 'entity_picture': AlokEntityPicture, 19 | 'show_last_changed': 'true' 20 | }) 21 | 22 | # Create device_tracker.test_rashmi entity 23 | hass.states.set(RashmitrackerName, RashminewStatus, { 24 | 'friendly_name': RashmiFriendlyName, 25 | 'entity_picture': RashmiEntityPicture, 26 | 'show_last_changed': 'true' 27 | }) 28 | -------------------------------------------------------------------------------- /python_scripts/light_counter.py: -------------------------------------------------------------------------------- 1 | on = 0 2 | for entity_id in hass.states.entity_ids('light'): 3 | state = hass.states.get(entity_id) 4 | if state.state == 'on': 5 | on = on + 1 6 | hass.states.set('sensor.lights_on', on, { 7 | 'unit_of_measurement': 'lights', 8 | 'friendly_name': 'Lights On' 9 | }) 10 | -------------------------------------------------------------------------------- /python_scripts/meta_device_tracker.py: -------------------------------------------------------------------------------- 1 | # Combine multiple device trackers into one entity 2 | # You can call the script using the following: 3 | # - service: python_script.meta_device_tracker 4 | # data_template: 5 | # entity_id: '{{trigger.entity_id}}' 6 | 7 | # OPTIONS 8 | # List the trackers for each individual 9 | RashmiTrackers = ['device_tracker.rashmiiphone', 'device_tracker.rashmiphone_rashmiphone_2', 10 | 'device_tracker.rashmiappiphone_2', 'device_tracker.life360_sonu', 11 | 'device_tracker.275f08b3_dd14_459f_81b9_6c5b02c9b54e'] 12 | AlokTrackers = ['device_tracker.alokiphone', 'device_tracker.alokphone_alokphone_2', 13 | 'device_tracker.alokiphone_3', 'device_tracker.life360_alok_saboo', 14 | 'device_tracker.elantrase_2', 'device_tracker.alok_geofency'] 15 | ArnavTrackers = ['device_tracker.arnav_iphone_12', 16 | 'device_tracker.life360_arnav_saboo', 17 | 'device_tracker.arnav_geofency', 18 | 'bdevice_tracker.arnavphone_arnavphone', 19 | 'device_tracker.arnaviphone12pro'] 20 | # Get the entity that triggered the automation 21 | triggeredEntity = data.get('entity_id') 22 | 23 | # Set friendly name and the metatracker name based on the entity that triggered 24 | if triggeredEntity in AlokTrackers: 25 | newFriendlyName = 'Alok Tracker' 26 | newEntityPicture = '/local/icons/Alok.png' 27 | metatrackerName = 'device_tracker.meta_alok' 28 | elif triggeredEntity in RashmiTrackers: 29 | newFriendlyName = 'Rashmi Tracker' 30 | newEntityPicture = '/local/icons/Rashmi.png' 31 | metatrackerName = 'device_tracker.meta_rashmi' 32 | elif triggeredEntity in ArnavTrackers: 33 | newFriendlyName = 'Arnav Tracker' 34 | newEntityPicture = '/local/icons/Arnav.png' 35 | metatrackerName = 'device_tracker.meta_arnav' 36 | else: 37 | newFriendlyName = None 38 | metatrackerName = None 39 | 40 | # Get current & new state 41 | newState = hass.states.get(triggeredEntity) 42 | currentState = hass.states.get(metatrackerName) 43 | # Get New data 44 | newSource = newState.attributes.get('source_type') 45 | newFriendlyName_temp = newState.attributes.get('friendly_name') 46 | 47 | # If GPS source, set new coordinates 48 | if newSource == 'gps': 49 | newLatitude = newState.attributes.get('latitude') 50 | newLongitude = newState.attributes.get('longitude') 51 | newgpsAccuracy = newState.attributes.get('gps_accuracy') 52 | # If not, keep last known coordinates 53 | elif newSource is not None and currentState.attributes.get('latitude') is not None: 54 | newLatitude = currentState.attributes.get('latitude') 55 | newLongitude = currentState.attributes.get('longitude') 56 | newgpsAccuracy = currentState.attributes.get('gps_accuracy') 57 | # Otherwise return null 58 | else: 59 | newLatitude = None 60 | newLongitude = None 61 | newgpsAccuracy = None 62 | 63 | # Get Battery 64 | if newState.attributes.get('battery') is not None: 65 | newBattery = newState.attributes.get('battery') 66 | elif currentState is not None and currentState.attributes.get('battery') is not None: 67 | newBattery = currentState.attributes.get('battery') 68 | else: 69 | newBattery = None 70 | 71 | # Get velocity 72 | if newState.attributes.get('velocity') is not None: 73 | newVelocity = newState.attributes.get('velocity') 74 | elif currentState is not None and currentState.attributes.get('velocity') is not None: 75 | newVelocity = currentState.attributes.get('velocity') 76 | else: 77 | newVelocity = None 78 | 79 | if newState.state is not None: 80 | newStatus = newState.state 81 | else: 82 | newStatus = currentState.state 83 | 84 | # Create device_tracker.meta entity 85 | hass.states.set(metatrackerName, newStatus, { 86 | 'friendly_name': newFriendlyName, 87 | 'entity_picture': newEntityPicture, 88 | 'source_type': newSource, 89 | 'battery': newBattery, 90 | 'gps_accuracy': newgpsAccuracy, 91 | 'latitude': newLatitude, 92 | 'longitude': newLongitude, 93 | 'velocity': newVelocity, 94 | 'update_source': triggeredEntity, 95 | 'show_last_changed': 'true' 96 | }) 97 | -------------------------------------------------------------------------------- /python_scripts/ring_download.py: -------------------------------------------------------------------------------- 1 | # obtain ring doorbell camera object 2 | # replace the camera.front_door by your camera entity 3 | ring_cam = hass.states.get('camera.front_door') 4 | 5 | subdir_name = 'ring_{}'.format(ring_cam.attributes.get('friendly_name').lower().replace(' ', '_')) 6 | 7 | # get video URL 8 | data = { 9 | 'url': ring_cam.attributes.get('video_url'), 10 | 'subdir': subdir_name, 11 | 'overwrite': True, 12 | 'filename': ring_cam.attributes.get('friendly_name').lower().replace(' ', '_') 13 | } 14 | 15 | # call downloader component to save the video 16 | hass.services.call('downloader', 'download_file', data) 17 | -------------------------------------------------------------------------------- /python_scripts/toggle_state.py: -------------------------------------------------------------------------------- 1 | # Manually toggle state of a device_tracker entity 2 | # You can call the script using the following: 3 | # - service: python_script.toggle_state 4 | # data_template: 5 | # entity_id: '{{trigger.entity_id}}' 6 | 7 | # Get the entity that triggered the automation 8 | triggeredEntity = data.get('entity_id') 9 | currentState = hass.states.get(triggeredEntity) 10 | 11 | if currentState == 'home': 12 | newStatus = 'not_home' 13 | else: 14 | newStatus = 'home' 15 | 16 | hass.states.set(triggeredEntity, newStatus) 17 | logger.warning("Set %s to %s",triggeredEntity, newStatus) 18 | -------------------------------------------------------------------------------- /recorder.yaml: -------------------------------------------------------------------------------- 1 | purge_keep_days: 5 2 | db_url: !secret recorder_db_url 3 | exclude: 4 | domains: 5 | - sun 6 | entities: 7 | - binary_sensor.backyard_motion 8 | - binary_sensor.driveway_motion 9 | - binary_sensor.patio_motion 10 | - binary_sensor.porch_motion 11 | - group.camera_sensors 12 | - input_select.livingroomharmony 13 | - sensor.living_room 14 | - sensor.dark_sky_summary 15 | - sensor.total_tv_time 16 | - zwave.garage_door_tilt_sensor 17 | - automation.change_leeo_color 18 | - automation.change_leeo_upstairs_color 19 | - automation.parse_life360_data 20 | - automation.update_dark_sky_friendly_names 21 | - automation.update_friendly_names_at_night 22 | - group.ring_doorbell 23 | - sensor.rpi_cpu_thermal_1_temperature 24 | - sensor.cold_flu_risk 25 | - sensor.curr_future 26 | - sensor.pollen_level 27 | - sensor.uptime 28 | - sensor.sonos_stereo 29 | - sensor.sonos_audio_in 30 | - sensor.ssl_certificate_expiry 31 | - switch.pihole 32 | - sensor.downstairs_hvac_runtime 33 | - sensor.upstairs_hvac_runtime 34 | - sensor.master_hvac_runtime 35 | - sensor.downstairs_temp_change 36 | - sensor.master_temp_change 37 | - sensor.upstairs_temp_change 38 | - sensor.pws_weather 39 | - sensor.dark_sky_hourly_summary 40 | - sensor.dark_sky_daily_summary 41 | - sensor.pws_wind_string 42 | - sensor.emulated_hue_names 43 | - sensor.usdinr 44 | - automation.populate_owntracks_data_from_life360 45 | - automation.update_device_meta_tracker 46 | - image_processing.opencv_backyard 47 | - image_processing.opencv_patio 48 | - image_processing.opencv_driveway 49 | - image_processing.opencv_porch 50 | - sensor.pihole_dns_queries_today 51 | - sensor.pihole_domains_blocked 52 | - sensor.pihole_ads_percentage_blocked_today 53 | - sensor.pihole_dns_unique_domains 54 | - sensor.10_year_treasury 55 | - input_boolean.frontdoor 56 | - input_boolean.backdoor 57 | - binary_sensor.garageentrydoor 58 | - input_text.patio 59 | - input_text.porch 60 | - input_text.driveway 61 | - input_text.backyard 62 | - sensor.new_cases_us 63 | - sensor.new_cases_india 64 | - sensor.new_cases_georgia 65 | -------------------------------------------------------------------------------- /scenes.yaml: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | ## Scenes 3 | ################################################################# 4 | 5 | - name: Colorloop 6 | entities: 7 | light.living_room_lights: on 8 | lifx_effect_colorloop 9 | - platform: lifx_cloud 10 | token: !secret lifx_key 11 | -------------------------------------------------------------------------------- /shell_scripts/alexa_wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Amazon Alexa TTS Home Assistant Wrapper 4 | # 5 | # 2018-06-18: v0.1 initial release 6 | # 7 | # This script is intended to allow the Alexa Remote Control script 8 | # from Alex Lotzimmer to be used as a command line notify platform 9 | # in Home Assistant. 10 | # 11 | # Usage: 12 | # ./alexa_wrapper.sh -d "My Dot Name" 13 | # 14 | # Home Assistant will pass the message to the script via STDIN. The 15 | # Alexa Remote control script requires that spaces be replaced with 16 | # underscores. 17 | # 18 | # Installation: 19 | # Place alexa_wrapper.sh and alexa_remote_control.sh in your Home Assistant 20 | # config directory. In a shell type echo $PATH and replace the below PATH 21 | # variable with your values. 22 | # 23 | # Edit alexa_remote_control.sh with your credentials and 24 | # your location. Test that you can pull a list of devices with 25 | # ./alexa_remote_control.sh -a 26 | # 27 | # Add a command line notify component for each Alexa device 28 | # to Home Assistant as follows: 29 | # 30 | # notify: 31 | # - platform: command_line 32 | # name: 'My Dot Name' 33 | # command: "/config/alexa_wrapper -d 'My Dot Name'" 34 | # 35 | # You should then be able to call notify.my_dot_name from automations 36 | # 37 | 38 | PATH=/home/arsaboo/bin:/home/arsaboo/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin 39 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 40 | ALEXA_REMOTE="$DIR/alexa_remote_control.sh" 41 | 42 | usage() 43 | { 44 | echo "$0 -d |ALL" 45 | } 46 | 47 | case "$1" in 48 | -d) 49 | if [ "${2#-}" != "${2}" -o -z "$2" ] ; then 50 | echo "ERROR: missing argument for ${1}" 51 | usage 52 | exit 1 53 | fi 54 | DEVICE=$2 55 | shift 56 | ;; 57 | *) 58 | echo "ERROR: unknown option ${1}" 59 | usage 60 | exit 1 61 | ;; 62 | esac 63 | shift 64 | 65 | read message 66 | 67 | formatted=${message// /_} 68 | 69 | $ALEXA_REMOTE -d "$DEVICE" -e speak:$formatted >> /dev/null 70 | exit 0 71 | -------------------------------------------------------------------------------- /shell_scripts/facebox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Set variables 4 | facebox_endpoint=$(awk '/facebox_endpoint/ {print $2}' /config/secrets.yaml) 5 | password=$(awk '/http_password/ {print $2}' /config/secrets.yaml) 6 | cameraURL=$(grep "$1_camera_image_url" /config/secrets.yaml | awk '{print $2}') 7 | sensor_name=$(grep "$1_faces_sensor" /config/secrets.yaml | awk '{print $2}') 8 | 9 | tagbox=$(curl -s -H 'Content-Type: application/json' -d '{"url": "'"$cameraURL"'"}' $facebox_endpoint) 10 | tags=$(echo $tagbox | jq --raw-output '.facesCount') 11 | 12 | curl -X POST -H "x-ha-access: $password" \ 13 | -H "Content-Type: application/json" \ 14 | -d '{"state":"'"$tags"'" }' \ 15 | "$sensor_name" 16 | -------------------------------------------------------------------------------- /shell_scripts/image_classification.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Set variables 4 | tagbox_endpoint=$(awk '/tagbox_endpoint/ {print $2}' /config/secrets.yaml) 5 | password=$(awk '/http_password/ {print $2}' /config/secrets.yaml) 6 | cameraURL=$(grep "$1_camera_image_url" /config/secrets.yaml | awk '{print $2}') 7 | sensor_name=$(grep "$1_tag_sensor" /config/secrets.yaml | awk '{print $2}') 8 | 9 | tagbox=$(curl -s -H 'Content-Type: application/json' -d '{"url": "'"$cameraURL"'"}' $tagbox_endpoint) 10 | tags=$(echo $tagbox | jq --raw-output '.tags | map(.tag) | join(", ")') 11 | 12 | curl -X POST -H "x-ha-access: $password" \ 13 | -H "Content-Type: application/json" \ 14 | -d '{"state":"'"$tags"'" }' \ 15 | "$sensor_name" 16 | -------------------------------------------------------------------------------- /themes/oxfordblue/oxfordblue.yaml: -------------------------------------------------------------------------------- 1 | oxfordblue: 2 | # Main colors 3 | ha-card-border-radius: '8px' 4 | primary-color: '#324d67' # Header 5 | accent-color: '#148e99' # Accent color 6 | dark-primary-color: '#243a52' # Hyperlinks 7 | light-primary-color: '#617d98' # Horizontal line in about 8 | # Text colors 9 | primary-text-color: '#102a42' # Primary text color, here is referencing dark-primary-color 10 | text-primary-color: '#ffffff' # Primary text color 11 | secondary-text-color: '#829ab0' # For secondary titles in more info boxes etc. 12 | disabled-text-color: '#dae2ec' # Disabled text color 13 | # Background colors 14 | primary-background-color: '#f1f5f8' # Settings background 15 | secondary-background-color: 'var(--primary-background-color)' # Main card UI background 16 | sidebar-background-color: 'var(--primary-background-color)' 17 | divider-color: 'var(--secondary-text-color)' 18 | # Table rows 19 | table-row-background-color: 'var(--primary-background-color)' # Table row 20 | table-row-alternative-background-color: '#e4eaf1' # Table row alternative 21 | # Nav Menu 22 | paper-listbox-color: '#102a42' # Navigation menu selection hoover 23 | paper-listbox-background-color: 'var(--primary-background-color)' # Navigation menu background 24 | paper-grey-50: 'var(--primary-text-color)' 25 | paper-grey-200: '#414A59' # Navigation menu selection 26 | # Paper card 27 | paper-card-header-color: 'var(--accent-color)' # Card header text color 28 | paper-card-background-color: '#FFF' # Card background color 29 | paper-dialog-background-color: '#FFF' # Card dialog background color 30 | paper-item-icon-color: 'var(--accent-color)' # Icon color 31 | paper-item-icon-active-color: '#F9C536' # Icon color active 32 | paper-item-icon_-_color: 'green' 33 | paper-item-selected_-_background-color: '#434954'# Popup item select 34 | paper-tabs-selection-bar-color: 'green' 35 | # Labels 36 | label-badge-red: 'var(--google-red-500)' 37 | label-badge-blue: 'var(--accent-color)' # References the brand color label badge border 38 | ha-label-badge-color: 'var(--accent-color)' 39 | label-badge-text-color: 'var(--primary-text-color)' # Now same as label badge border but that's a matter of taste 40 | label-badge-background-color: 'var(--primary-background-color)' # Same, but can also be set to transparent here 41 | # Switches 42 | paper-toggle-button-checked-button-color: 'var(--accent-color)' 43 | paper-toggle-button-checked-bar-color: 'var(--accent-color)' 44 | paper-toggle-button-checked-ink-color: 'var(--accent-color)' 45 | paper-toggle-button-unchecked-button-color: 'var(--disabled-text-color)' 46 | paper-toggle-button-unchecked-bar-color: 'var(--disabled-text-color)' 47 | paper-toggle-button-unchecked-ink-color: 'var(--disabled-text-color)' 48 | switch-checked-color: 'var(--accent-color)' 49 | switch-unchecked-color: 'var(--disabled-text-color)' 50 | switch-unchecked-button-color: 'var(--disabled-text-color)' 51 | switch-unchecked-track-color: 'var(--disabled-text-color)' 52 | # Sliders 53 | paper-slider-knob-color: 'var(--accent-color)' 54 | paper-slider-knob-start-color: 'var(--accent-color)' 55 | paper-slider-pin-color: 'var(--accent-color)' 56 | paper-slider-active-color: 'var(--accent-color)' 57 | paper-slider-container-color: 'linear-gradient(var(--primary-background-color), var(--secondary-background-color)) no-repeat' 58 | paper-slider-secondary-color: 'var(--secondary-background-color)' 59 | paper-slider-disabled-active-color: 'var(--disabled-text-color)' 60 | paper-slider-disabled-secondary-color: 'var(--disabled-text-color)' 61 | # Google colors 62 | google-red-500: '#db3236' 63 | google-green-500: '#39E949' 64 | 65 | # CUSTOM STYLES NOT PART OF CORE HOME ASSISTANT 66 | # HACS https://hacs.xyz/docs/basic/theming 67 | hacs-badge-color: 'var(--paper-item-icon-color)' # Used for icon colors and new items in the store 68 | hacs-status-installed: 'var(--light-primary-color)' # Controls the icon color for installed, up-to-date repositories 69 | hacs-status-pending-restart: 'var(--accent-color)' # Controls the icon color for installed repositories that are awaiting a Home Assistant restart 70 | hacs-status-pending-update: 'var(--accent-color)' # Controls the icon color for installed repositories that have an update available 71 | hacs-status-not-loaded: 'var(--disabled-text-color)' # Controls the icon color for repositories that is not loaded 72 | link-text-color: 'var(--accent-color)' # Used for href links in HACS 73 | -------------------------------------------------------------------------------- /themes/sweetpink/sweetpink.yaml: -------------------------------------------------------------------------------- 1 | sweetpink: 2 | # used these colors 3 | light: "#dbebfb" 4 | dark: "#32334a" 5 | pink: "#e83e8c" 6 | gray: "#1d1e2f" 7 | gray-dark: "#11111d" 8 | indigo: "#6610f2" 9 | purple: "#6f42c1" 10 | teal: "#20c997" 11 | cyan: "#17a2b8" 12 | 13 | # could use these colors 14 | blue: "#007bff" 15 | red: "#dc3545" 16 | orange: "#fd7e14" 17 | yellow: "#ffc107" 18 | green: "#28a745" 19 | white: "#fff" 20 | 21 | primary-color: "var(--pink)" 22 | primary-background-color: "var(--dark)" 23 | primary-text-color: "var(--light)" 24 | light-primary-color: "var(--cyan)" 25 | accent-color: "var(--teal)" 26 | 27 | secondary-background-color: "var(--gray)" 28 | secondary-text-color: "var(--cyan)" 29 | 30 | disabled-text-color: "var(--gray)" 31 | 32 | divider-color: "rgba(255, 255, 255, 0.12)" 33 | 34 | paper-card-background-color: "var(--gray-dark)" 35 | paper-card-header-color: "#var(--paper-item-icon-color)" 36 | card-background-color: "var(--paper-card-background-color)" 37 | 38 | paper-item-icon-color: "var(--pink)" 39 | paper-listbox-background-color: "var(--indigo)" 40 | paper-listbox-color: "var(--light)" 41 | 42 | paper-grey-50: "var(--primary-text-color)" 43 | paper-grey-200: "var(-gray)" 44 | google-red-500: "var(--orange)" 45 | 46 | label-badge-background-color: "var(--secondary-background-color)" 47 | label-badge-text-color: "var(--text-primary-color)" 48 | 49 | paper-item-icon-active-color: "var(--cyan)" 50 | paper-item-icon_-_color: "var(--primary-text-color)" 51 | 52 | sidebar-icon-color: "var(--light)" 53 | sidebar-text-color: "var(--light)" 54 | sidebar-selected-background-color: "var(--cyan)" 55 | sidebar-selected-icon-color: "var(--light)" 56 | sidebar-selected-text-color: "var(--light)" 57 | sidebar-text_-_background: "var(--light)" 58 | 59 | paper-progress-active-color: "var(--pink)" 60 | paper-progress-container-color: "var(--purple)" 61 | paper-slider-knob-color: "var(--teal)" 62 | paper-slider-knob-start-color: "var(--purple)" 63 | 64 | switch-unchecked-color: "var(--teal)" 65 | switch-unchecked-button-color: "var(--teal)" 66 | 67 | # Themable CCS vars in HACS (https://hacs.xyz/docs/basic/theming) 68 | link-text-color: "var(--cyan)" 69 | hacs-status-installed: "var(--teal)" 70 | hacs-status-pending-restart: "var(--indigo)" 71 | hacs-status-pending-update: "var(--purple)" 72 | hacs-status-not-loaded: "var(--indigo)" 73 | 74 | paper-dialog-button-color: "var(--light)" 75 | iron-icon-fill-color: "var(--light)" 76 | update-heading: "var(--light)" 77 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function get_file { 4 | DOWNLOAD_PATH=${2}?raw=true 5 | FILE_NAME=$1 6 | if [ "${FILE_NAME:0:1}" = "/" ]; then 7 | SAVE_PATH=$FILE_NAME 8 | else 9 | SAVE_PATH=$3$FILE_NAME 10 | fi 11 | TMP_NAME=${1}.tmp 12 | echo "Getting $1" 13 | # wget $DOWNLOAD_PATH -q -O $TMP_NAME 14 | curl -s -q -L -o $TMP_NAME $DOWNLOAD_PATH 15 | rv=$? 16 | if [ $rv != 0 ]; then 17 | rm $TMP_NAME 18 | echo "Download failed with error $rv" 19 | exit 20 | fi 21 | diff ${SAVE_PATH} $TMP_NAME &>/dev/null 22 | if [ $? == 0 ]; then 23 | echo "File up to date." 24 | rm $TMP_NAME 25 | return 0 26 | else 27 | mv $TMP_NAME ${SAVE_PATH} 28 | if [ $1 == $0 ]; then 29 | chmod u+x $0 30 | echo "Restarting" 31 | $0 32 | exit $? 33 | else 34 | return 1 35 | fi 36 | fi 37 | } 38 | 39 | function get_file_and_gz { 40 | get_file $1 $2 $3 41 | r1=$? 42 | get_file ${1}.gz ${2}.gz $3 43 | r2=$? 44 | if (( $r1 != 0 || $r2 != 0 )); then 45 | return 1 46 | fi 47 | return 0 48 | } 49 | 50 | function check_dir { 51 | if [ ! -d $1 ]; then 52 | read -p "$1 dir not found. Create? (y/n): [n] " r 53 | r=${r:-n} 54 | if [[ $r == 'y' || $r == 'Y' ]]; then 55 | mkdir -p $1 56 | else 57 | exit 58 | fi 59 | fi 60 | } 61 | 62 | if [ ! -f configuration.yaml ]; then 63 | echo "There is no configuration.yaml in current dir. 'update.sh' should run from Homeassistant config dir" 64 | read -p "Are you sure you want to continue? (y/n): [n] " r 65 | r=${r:-n} 66 | if [[ $r == 'n' || $r == 'N' ]]; then 67 | exit 68 | fi 69 | fi 70 | 71 | get_file $0 https://github.com/andrey-git/home-assistant-custom-ui/blob/master/update.sh ./ 72 | 73 | 74 | check_dir "www/custom_ui" 75 | 76 | 77 | get_file scripts.js.map https://github.com/andrey-git/home-assistant-custom-ui/blob/master/scripts.js.map www/custom_ui/ 78 | get_file scripts.js.LICENSE https://github.com/andrey-git/home-assistant-custom-ui/blob/master/scripts.js.LICENSE www/custom_ui/ 79 | get_file scripts-es5.js.map https://github.com/andrey-git/home-assistant-custom-ui/blob/master/scripts-es5.js.map www/custom_ui/ 80 | get_file scripts-es5.js.LICENSE https://github.com/andrey-git/home-assistant-custom-ui/blob/master/scripts-es5.js.LICENSE www/custom_ui/ 81 | get_file_and_gz state-card-custom-ui-es5.html https://github.com/andrey-git/home-assistant-custom-ui/blob/master/state-card-custom-ui-es5.html www/custom_ui/ 82 | get_file_and_gz state-card-custom-ui.html https://github.com/andrey-git/home-assistant-custom-ui/blob/master/state-card-custom-ui.html www/custom_ui/ 83 | 84 | 85 | if [ $? != 0 ]; then 86 | echo "Updated to Custom UI `grep -o -e '"[0-9][0-9][0-9]*"' www/custom_ui/state-card-custom-ui.html`" 87 | fi 88 | 89 | 90 | check_dir "custom_components/customizer" 91 | 92 | get_file __init__.py https://github.com/andrey-git/home-assistant-customizer/blob/master/customizer/__init__.py custom_components/customizer/ 93 | get_file services.yaml https://github.com/andrey-git/home-assistant-customizer/blob/master/customizer/services.yaml custom_components/customizer/ 94 | -------------------------------------------------------------------------------- /update_files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | wget -O /config/www/custom_ui/card-modder.js https://raw.githubusercontent.com/thomasloven/lovelace-card-modder/master/card-modder.js 4 | sudo chown -R homeassistant:homeassistant /config/ 5 | -------------------------------------------------------------------------------- /www/assets/images/skullcanyon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/assets/images/skullcanyon.jpeg -------------------------------------------------------------------------------- /www/assets/images/vintage-colors-blur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/assets/images/vintage-colors-blur.jpg -------------------------------------------------------------------------------- /www/community/button-card/button-card.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/button-card/button-card.js.gz -------------------------------------------------------------------------------- /www/community/config-template-card/config-template-card.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/config-template-card/config-template-card.js.gz -------------------------------------------------------------------------------- /www/community/list-card/list-card.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/list-card/list-card.js.gz -------------------------------------------------------------------------------- /www/community/lovelace-auto-entities/auto-entities.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/lovelace-auto-entities/auto-entities.js.gz -------------------------------------------------------------------------------- /www/community/lovelace-card-mod/card-mod.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/lovelace-card-mod/card-mod.js.gz -------------------------------------------------------------------------------- /www/community/lovelace-card-tools/card-tools.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/lovelace-card-tools/card-tools.js.gz -------------------------------------------------------------------------------- /www/community/lovelace-hass-aarlo/hass-aarlo.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/lovelace-hass-aarlo/hass-aarlo.js.gz -------------------------------------------------------------------------------- /www/community/lovelace-layout-card/layout-card.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/lovelace-layout-card/layout-card.js.gz -------------------------------------------------------------------------------- /www/community/lovelace-slider-entity-row/slider-entity-row.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/lovelace-slider-entity-row/slider-entity-row.js.gz -------------------------------------------------------------------------------- /www/community/mini-graph-card/mini-graph-card-bundle.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/mini-graph-card/mini-graph-card-bundle.js.gz -------------------------------------------------------------------------------- /www/community/mini-media-player/mini-media-player-bundle.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/mini-media-player/mini-media-player-bundle.js.gz -------------------------------------------------------------------------------- /www/community/pc-card/pc-card.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/pc-card/pc-card.js.gz -------------------------------------------------------------------------------- /www/community/restriction-card/restriction-card.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/restriction-card/restriction-card.js.gz -------------------------------------------------------------------------------- /www/community/secondaryinfo-entity-row/secondaryinfo-entity-row.js: -------------------------------------------------------------------------------- 1 | customElements.whenDefined('card-tools').then(() => { 2 | 3 | class SecondaryInfoEntityRow extends cardTools.LitElement { 4 | version() { return "0.5"; } 5 | 6 | render() { 7 | return cardTools.LitHtml` 8 | ${this._wrappedElement} 9 | `; 10 | } 11 | 12 | setConfig(config) { 13 | cardTools.checkVersion(2.0); 14 | this._config = config; 15 | this._wrappedElement = this._createElement(config); 16 | this.requestUpdate(); 17 | } 18 | 19 | set hass(hass) { 20 | this._hass = hass; 21 | this._stateObj = this._config.entity in hass.states ? hass.states[this._config.entity] : null; 22 | this._updateElement(this._wrappedElement, hass); 23 | } 24 | 25 | _createElement(config) { 26 | // Override the custom row type in order to create the 'standard' row for this entity 27 | let defaultRowConfig = Object.assign({}, config); 28 | delete defaultRowConfig.type; 29 | const element = cardTools.createEntityRow(defaultRowConfig); 30 | return element; 31 | } 32 | 33 | async _updateElement(wrappedElement, hass) { 34 | if (!wrappedElement) return; 35 | 36 | this._wrappedElement.hass = hass; 37 | await this._wrappedElement.updateComplete; 38 | await this._wrappedElement.shadowRoot.querySelector("hui-generic-entity-row"); 39 | let secondaryInfoDiv = this._wrappedElement.shadowRoot.querySelector("hui-generic-entity-row").shadowRoot.querySelector(".secondary"); 40 | if (secondaryInfoDiv && this._config.secondary_info) { 41 | let text; 42 | if (this._config.secondary_info.includes('{{') || this._config.secondary_info.includes('{%')) { 43 | text = await window.cardTools.parseTemplate(hass, this._config.secondary_info, {entity: this._config.entity}) 44 | } else { 45 | text = window.cardTools.parseTemplate(this._config.secondary_info, {entity: this._config.entity}); 46 | } 47 | secondaryInfoDiv.innerHTML = text; 48 | } 49 | } 50 | } 51 | customElements.define('secondaryinfo-entity-row', SecondaryInfoEntityRow); 52 | 53 | }); 54 | 55 | setTimeout(() => { 56 | if (customElements.get('card-tools')) return; 57 | customElements.define('secondaryinfo-entity-row', class extends HTMLElement { 58 | setConfig() { 59 | throw new Error("Can't find card-tools. See https://github.com/thomasloven/lovelace-card-tools"); 60 | } 61 | }); 62 | }, 2000); 63 | -------------------------------------------------------------------------------- /www/community/secondaryinfo-entity-row/secondaryinfo-entity-row.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/secondaryinfo-entity-row/secondaryinfo-entity-row.js.gz -------------------------------------------------------------------------------- /www/community/simple-thermostat/simple-thermostat.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/simple-thermostat/simple-thermostat.js.gz -------------------------------------------------------------------------------- /www/community/unused-card/unused-card.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/www/community/unused-card/unused-card.js.gz -------------------------------------------------------------------------------- /www/custom_ui/ext-weblink.js: -------------------------------------------------------------------------------- 1 | class ExtWebLink extends HTMLElement { 2 | set hass(hass) { 3 | if (!this.config.icon) { 4 | this.config.icon = "mdi:home-assistant"; 5 | } 6 | if (!this.config.entity) { 7 | var state = ""; 8 | } else { 9 | if (hass.states[this.config.entity].attributes.unit_of_measurement) { 10 | var state = hass.states[this.config.entity].state+' '+hass.states[this.config.entity].attributes.unit_of_measurement; 11 | } else { 12 | var state = hass.states[this.config.entity].state 13 | } 14 | } 15 | if (this.config.name) { 16 | var name = this.config.name; 17 | } else { 18 | var name = hass.states[this.config.entity].attributes.friendly_name; 19 | } 20 | if (!this.config.url) { 21 | this.config.url = "#"; 22 | } 23 | this.innerHTML =` 24 | 58 |
59 | 60 | 61 |
${name}
62 | 63 |
${state}
64 | 65 |
66 | `; 67 | } 68 | setConfig(config) { 69 | this.config = config; 70 | } 71 | 72 | getCardSize() { 73 | return 1; 74 | } 75 | } 76 | customElements.define('ext-weblink', ExtWebLink); -------------------------------------------------------------------------------- /www/custom_ui/gap-card.js: -------------------------------------------------------------------------------- 1 | class GapCard extends HTMLElement { 2 | 3 | setConfig(config) { 4 | this.height = ('height' in config) ? config.height : 50; 5 | this.size = ('size' in config) ? config.size : Math.ceil(this.height/50); 6 | this.style.setProperty('height', this.height + 'px'); 7 | } 8 | 9 | getCardSize() { 10 | return this.size; 11 | } 12 | } 13 | 14 | customElements.define('gap-card', GapCard); 15 | -------------------------------------------------------------------------------- /z-wave_relay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsaboo/homeassistant-config/949a0ef28911f0bd3832e61365e5b58731915b4b/z-wave_relay.png --------------------------------------------------------------------------------