diff --git a/.github/workflows/HACSCheck.yaml b/.github/workflows/HACSCheck.yaml new file mode 100644 index 0000000..11df276 --- /dev/null +++ b/.github/workflows/HACSCheck.yaml @@ -0,0 +1,22 @@ +name: HACS + +on: + push: + pull_request: + +jobs: + validate-hassfest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Hassfest validation + uses: home-assistant/actions/hassfest@master + + validate-hacs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: HACS validation + uses: hacs/action@main + with: + category: integration diff --git a/README.md b/README.md index e727bf4..38ce2b4 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ logger: Please follow screenshots below. In summary; - You will first install HASS Add On assuming you are running on Hassos or Supervised. If not, please execute this command to run docker instance manually ```docker run -it -e USERNAME=email@address.com -e PASSWORD=password_goes_here -p 3000:3000 bropat/eufy-security-ws:X.Y.Z```. To find out correct values for X.Y.Z, please chek here /~https://github.com/fuatakgun/eufy_security_addon/blob/main/config.json#L3 - Later on, you should install RTSP Server Add On to have faster/more reliable p2p streaming. I will deprecate/not support file based streaming soon, so, please migrate in timely manner. If you are not using Hassos or Supervised installation please execute this command to run docker instance manually ```docker run --rm -it -e RTSP_PROTOCOLS=tcp -d -p 8554:8554 -p 1935:1935 aler9/rtsp-simple-server``` -- When you are done with HASS Add On, you will install integration via adding integration address to HACS, downloading files over UI, restarting home assistant and setting up integration. +- When you are done with HASS Add On, you will install integration via HACS, downloading files over UI, restarting home assistant and setting up integration. - Double check if your `configuration.yaml` includes `ffmpeg` integration. If not, please do like this; https://www.home-assistant.io/integrations/ffmpeg/#configuration . This integration relies on `ffmpeg` to be setup to live stream via P2P and capture images from live RTSP/P2P streams. ## 6.1 Installing Eufy Security Add On - Required @@ -105,40 +105,33 @@ Please follow screenshots below. In summary; ## 6.3 Installing Integration -1- Go to HACS and click on `Custom repositories` +1- Go to HACS and search for 'Eufy Security' -![6-hacs-custom-repositories](https://user-images.githubusercontent.com/11085566/126563932-e9fc2783-02a1-42d3-8f4a-bc0fa2edf386.PNG) - -2- Add custom integration repository URL -```/~https://github.com/fuatakgun/eufy_security/``` - -![7-hacs-add](https://user-images.githubusercontent.com/11085566/126563937-4ad08d92-b9c1-45e3-a205-be9b244bc3a7.PNG) - -3- Install the `Eufy Secucirty` repository +2- Install the `Eufy Security` repository ![8-hacs-install](https://user-images.githubusercontent.com/11085566/126563950-1c89c1e8-f77d-46ac-8910-77048500a07f.PNG) -4- You need to restart your HA instance to pick up new repository. +3- You need to restart your HA instance to pick up new repository. ![9-hacs-restart](https://user-images.githubusercontent.com/11085566/126563954-b801e4ea-b93e-4695-928d-a82221fe01f4.PNG) -5- After restart, go to `Configuration` - `Integrations` page and click on `Add Integration` +4- After restart, go to `Configuration` - `Integrations` page and click on `Add Integration` ![10-integrations](https://user-images.githubusercontent.com/11085566/126563961-a05c5e50-b006-4759-b55a-548f691a13d8.PNG) -6- Search for ```Eufy Security``` and click on it +5- Search for ```Eufy Security``` and click on it ![11-integration-search](https://user-images.githubusercontent.com/11085566/126563968-920a74de-ab93-456b-b4b2-dcf651a07f9f.PNG) -7- In next page, use ```localhost``` if you have used Add-on installation, otherwise put your Docker instance ip address and keep `3000` as port +6- In next page, use ```localhost``` if you have used Add-on installation, otherwise put your Docker instance ip address and keep `3000` as port ![12-integration-configure](https://user-images.githubusercontent.com/11085566/126563976-234005e7-2920-4ef0-a301-187d4d929f10.png) -8- You will be shown devices connected to your account. Or you can be alerted with captcha notification. +7- You will be shown devices connected to your account. Or you can be alerted with captcha notification. ![13-integration-done](https://user-images.githubusercontent.com/11085566/126563982-38b3a00a-ff6a-45aa-8dcc-b04e864a37f8.PNG) -9- Follow screenshots below for captcha +8- Follow screenshots below for captcha a. Get notified about captcha request ![image](https://user-images.githubusercontent.com/11085566/145604195-7b5499b3-9603-468b-aa03-84121047719b.png) @@ -153,11 +146,11 @@ d. If you put the wrong code, after couple of seconds, you will get a similar er e. After entering correct captcha code, your devices will be ready to use. -10- If your camera does not support RTSP based live streaming, you can use `Start Live Stream` and `Stop Live Stream` services rather than turn_on and turn_off because they tend to be using RTSP functions. They require camera entities as input, you can use UI for this. More importantly, please set your `Streaming Quality` settings to `Low` per each camera using Eufy Security app, otherwise WebRTC will fail to stream the video. Underlying issue is realted mid or high quality codecs are not supported by WebRTC. +9- If your camera does not support RTSP based live streaming, you can use `Start Live Stream` and `Stop Live Stream` services rather than turn_on and turn_off because they tend to be using RTSP functions. They require camera entities as input, you can use UI for this. More importantly, please set your `Streaming Quality` settings to `Low` per each camera using Eufy Security app, otherwise WebRTC will fail to stream the video. Underlying issue is realted mid or high quality codecs are not supported by WebRTC. ![14-services-live-stream](https://user-images.githubusercontent.com/11085566/126563991-5ef949c5-144c-4702-a9e3-577e2d37c0f8.PNG) -11- If you want faster P2P live streaming, go to Integration Configuration section and enable it. +10- If you want faster P2P live streaming, go to Integration Configuration section and enable it. ![image](https://user-images.githubusercontent.com/11085566/127866543-1345d56f-b4f3-4154-96c7-a278d747cf8d.png) ## 6.3 WebRTC - Required diff --git a/custom_components/eufy_security/binary_sensor.py b/custom_components/eufy_security/binary_sensor.py index 24a73bb..4685291 100644 --- a/custom_components/eufy_security/binary_sensor.py +++ b/custom_components/eufy_security/binary_sensor.py @@ -8,9 +8,11 @@ DEVICE_CLASS_POWER, DEVICE_CLASS_SOUND, ) + from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from .const import COORDINATOR, DOMAIN, Device, get_child_value from .coordinator import EufySecurityDataUpdateCoordinator @@ -29,8 +31,9 @@ async def async_setup_entry( "status_led_enabled", "Status Led Enabled", "state.statusLed", - "mdi: led-on", + "mdi:led-on", None, + EntityCategory.DIAGNOSTIC, ), ( "motion_sensor", @@ -38,6 +41,7 @@ async def async_setup_entry( "state.motionDetected", None, DEVICE_CLASS_MOTION, + None, ), ( "motion_detection_enabled", @@ -45,6 +49,7 @@ async def async_setup_entry( "state.motionDetection", None, DEVICE_CLASS_MOTION, + EntityCategory.DIAGNOSTIC, ), ( "person_detector_sensor", @@ -52,6 +57,7 @@ async def async_setup_entry( "state.personDetected", None, DEVICE_CLASS_MOTION, + EntityCategory.DIAGNOSTIC, ), ( "person_detection_enabled", @@ -59,6 +65,7 @@ async def async_setup_entry( "state.personDetection", None, DEVICE_CLASS_MOTION, + EntityCategory.DIAGNOSTIC, ), ( "pet_detector_sensor", @@ -66,6 +73,7 @@ async def async_setup_entry( "state.petDetected", None, DEVICE_CLASS_MOTION, + None, ), ( "pet_detection_enabled", @@ -73,6 +81,7 @@ async def async_setup_entry( "state.petDetection", "mdi: dog", None, + EntityCategory.DIAGNOSTIC, ), ( "sound_detector_sensor", @@ -80,6 +89,7 @@ async def async_setup_entry( "state.soundDetected", None, DEVICE_CLASS_SOUND, + None, ), ( "sound_detection_enabled", @@ -87,6 +97,7 @@ async def async_setup_entry( "state.soundDetection", "mdi: account-voice", None, + EntityCategory.DIAGNOSTIC, ), ( "crying_detector_sensor", @@ -94,16 +105,39 @@ async def async_setup_entry( "state.cryingDetected", None, DEVICE_CLASS_SOUND, + None, + ), + ( + "sensor_open", + "Sensor Open", + "state.sensorOpen", + None, + DEVICE_CLASS_DOOR, + None, + ), + ( + "battery_low", + "Battery Low", + "state.batteryLow", + None, + DEVICE_CLASS_BATTERY, + EntityCategory.DIAGNOSTIC, + ), + ( + "ringing_sensor", + "Ringing Sensor", + "state.ringing", + "mdi:bell-ring", + None, + None, ), - ("sensor_open", "Sensor Open", "state.sensorOpen", None, DEVICE_CLASS_DOOR), - ("battery_low", "Battery Low", "state.batteryLow", None, DEVICE_CLASS_BATTERY), - ("ringing_sensor", "Ringing Sensor", "state.ringing", "mdi:bell-ring", None), ( "motion_tracking_enabled", "Motion Tracking", "state.motionTracking", "mdi:go-kart-track", None, + EntityCategory.DIAGNOSTIC, ), ( "notification_person_enabled", @@ -111,6 +145,7 @@ async def async_setup_entry( "state.notificationPerson", "mdi:bell-ring", None, + EntityCategory.DIAGNOSTIC, ), ( "notification_pet_enabled", @@ -118,6 +153,7 @@ async def async_setup_entry( "state.notificationPet", "mdi:bell-ring", None, + EntityCategory.DIAGNOSTIC, ), ( "notification_all_other_motion_enabled", @@ -125,6 +161,7 @@ async def async_setup_entry( "state.notificationAllOtherMotion", "mdi:bell-ring", None, + EntityCategory.DIAGNOSTIC, ), ( "notification_crying_enabled", @@ -132,6 +169,7 @@ async def async_setup_entry( "state.notificationCrying", "mdi:bell-ring", None, + EntityCategory.DIAGNOSTIC, ), ( "notification_all_sound_enabled", @@ -139,14 +177,23 @@ async def async_setup_entry( "state.notificationAllSound", "mdi:bell-ring", None, + EntityCategory.DIAGNOSTIC, + ), + ( + "speaker_enabled", + "Speaker", + "state.speaker", + "mdi:bullhorn", + None, + EntityCategory.DIAGNOSTIC, ), - ("speaker_enabled", "Speaker", "state.speaker", "mdi:bullhorn", None), ( "microphone_enabled", "Microphone", "state.microphone", "mdi:microphone", None, + EntityCategory.DIAGNOSTIC, ), ( "auto_night_vision_enabled", @@ -154,6 +201,7 @@ async def async_setup_entry( "state.autoNightvision", "mdi:weather-night", None, + EntityCategory.DIAGNOSTIC, ), ( "audio_recording_enabled", @@ -161,6 +209,7 @@ async def async_setup_entry( "state.audioRecording", "mdi:record-rec", None, + EntityCategory.DIAGNOSTIC, ), ( "charging", @@ -168,21 +217,37 @@ async def async_setup_entry( "state.chargingStatus", None, DEVICE_CLASS_BATTERY_CHARGING, + EntityCategory.DIAGNOSTIC, + ), + ( + "enabled", + "Enabled", + "state.enabled", + None, + DEVICE_CLASS_POWER, + EntityCategory.DIAGNOSTIC, + ), + ( + "streaming", + "Streaming Sensor", + "is_streaming", + None, + DEVICE_CLASS_MOTION, + EntityCategory.DIAGNOSTIC, ), - ("enabled", "Enabled", "state.enabled", None, DEVICE_CLASS_POWER), - ("streaming", "Streaming Sensor", "is_streaming", None, DEVICE_CLASS_MOTION), ( "rtsp_stream_enabled", "RTSP Stream", "state.rtspStream", "mdi:cast-connected", None, + EntityCategory.DIAGNOSTIC, ), ] entities = [] for device in coordinator.devices.values(): - for id, description, key, icon, device_class in INSTRUMENTS: + for id, description, key, icon, device_class, entity_category in INSTRUMENTS: if not get_child_value(device.__dict__, key) is None: entities.append( EufySecurityBinarySensor( @@ -194,6 +259,7 @@ async def async_setup_entry( key, icon, device_class, + entity_category, ) ) @@ -211,6 +277,7 @@ def __init__( key: str, icon: str, device_class: str, + entity_category: str, ): super().__init__(coordinator, config_entry, device) self._id = id @@ -218,6 +285,7 @@ def __init__( self.key = key self._icon = icon self._device_class = device_class + self._attr_entity_category = entity_category if self.id == "motion_sensor" and device.is_motion_sensor() is True: self.key = "motionDetection" diff --git a/custom_components/eufy_security/manifest.json b/custom_components/eufy_security/manifest.json index e93bdad..33f385e 100644 --- a/custom_components/eufy_security/manifest.json +++ b/custom_components/eufy_security/manifest.json @@ -5,7 +5,8 @@ "issue_tracker": "/~https://github.com/fuatakgun/eufy_security/issues", "dependencies": ["ffmpeg", "http"], "config_flow": true, + "version": "1.1.0", "codeowners": ["@fuatakgun"], "requirements": ["websocket-client==1.1.0"], - "version": "0.0.1" + "iot_class": "cloud_push" } diff --git a/custom_components/eufy_security/select.py b/custom_components/eufy_security/select.py index 5e7e620..ea31bbd 100644 --- a/custom_components/eufy_security/select.py +++ b/custom_components/eufy_security/select.py @@ -3,6 +3,7 @@ from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from .const import COORDINATOR, DOMAIN, Device, get_child_value from .coordinator import EufySecurityDataUpdateCoordinator @@ -17,22 +18,48 @@ async def async_setup_entry( coordinator: EufySecurityDataUpdateCoordinator = hass.data[DOMAIN][COORDINATOR] INSTRUMENTS = [ - ("night_vision", "Night Vision", "nightvision"), - ("power_working_mode", "Power Working Mode", "powerWorkingMode"), - ("video_streaming_quality", "Video Streaming Quality", "videoStreamingQuality"), - ("video_recording_quality", "Video Recording Quality", "videoRecordingQuality"), - ("motion_detection_type", "Motion Detection Type", "motionDetectionType"), - ("rotation_speed", "Rotation Speed", "rotationSpeed"), + ("night_vision", "Night Vision", "nightvision", EntityCategory.CONFIG), + ( + "power_working_mode", + "Power Working Mode", + "powerWorkingMode", + EntityCategory.CONFIG, + ), + ( + "video_streaming_quality", + "Video Streaming Quality", + "videoStreamingQuality", + EntityCategory.CONFIG, + ), + ( + "video_recording_quality", + "Video Recording Quality", + "videoRecordingQuality", + EntityCategory.CONFIG, + ), + ( + "motion_detection_type", + "Motion Detection Type", + "motionDetectionType", + EntityCategory.CONFIG, + ), + ("rotation_speed", "Rotation Speed", "rotationSpeed", EntityCategory.CONFIG), ] entities = [] for device in coordinator.devices.values(): instruments = INSTRUMENTS - for id, description, key in instruments: + for id, description, key, entity_category in instruments: if not get_child_value(device.state, key) is None: entities.append( EufySelectEntity( - coordinator, config_entry, device, id, description, key + coordinator, + config_entry, + device, + id, + description, + key, + entity_category, ) ) @@ -48,6 +75,7 @@ def __init__( id: str, description: str, key: str, + entity_category: str, ): EufySecurityEntity.__init__(self, coordinator, config_entry, device) SelectEntity.__init__(self) @@ -61,6 +89,7 @@ def __init__( self.values_to_states = self.states.get("states", {}) self.states_to_values = {v: k for k, v in self.values_to_states.items()} self._attr_options: list[str] = list(self.values_to_states.values()) + self._attr_entity_category = entity_category _LOGGER.debug( f"{DOMAIN} - {self.device.name} - {self.id} - select init - {self.values_to_states} - {self.states_to_values}" diff --git a/custom_components/eufy_security/sensor.py b/custom_components/eufy_security/sensor.py index afe2d33..0f39f1c 100644 --- a/custom_components/eufy_security/sensor.py +++ b/custom_components/eufy_security/sensor.py @@ -7,6 +7,7 @@ PERCENTAGE, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from .const import COORDINATOR, DOMAIN, Device, get_child_value from .coordinator import EufySecurityDataUpdateCoordinator @@ -21,7 +22,15 @@ async def async_setup_entry( coordinator: EufySecurityDataUpdateCoordinator = hass.data[DOMAIN][COORDINATOR] INSTRUMENTS = [ - ("battery", "Battery", "state.battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY), + ( + "battery", + "Battery", + "state.battery", + PERCENTAGE, + None, + DEVICE_CLASS_BATTERY, + EntityCategory.DIAGNOSTIC, + ), ( "wifiRSSI", "Wifi RSSI", @@ -29,6 +38,7 @@ async def async_setup_entry( None, None, DEVICE_CLASS_SIGNAL_STRENGTH, + EntityCategory.DIAGNOSTIC, ), ( "detected_person_name", @@ -37,6 +47,7 @@ async def async_setup_entry( None, None, None, + None, ), ] @@ -48,6 +59,7 @@ async def async_setup_entry( None, None, None, + EntityCategory.DIAGNOSTIC, ), ( "stream_source_address", @@ -56,9 +68,18 @@ async def async_setup_entry( None, None, None, + EntityCategory.DIAGNOSTIC, + ), + ("codec", "Codec", "codec", None, None, None, EntityCategory.DIAGNOSTIC), + ( + "stream_queue_size", + "Stream Queue Size", + "queue", + None, + None, + None, + EntityCategory.DIAGNOSTIC, ), - ("codec", "Codec", "codec", None, None, None), - ("stream_queue_size", "Stream Queue Size", "queue", None, None, None), ] entities = [] @@ -66,7 +87,15 @@ async def async_setup_entry( instruments = INSTRUMENTS if device.is_camera() is True: instruments = instruments + CAMERA_INSTRUMENTS - for id, description, key, unit, icon, device_class in instruments: + for ( + id, + description, + key, + unit, + icon, + device_class, + entity_category, + ) in instruments: if not get_child_value(device.__dict__, key) is None: entities.append( EufySecuritySensor( @@ -79,6 +108,7 @@ async def async_setup_entry( unit, icon, device_class, + entity_category, ) ) @@ -97,6 +127,7 @@ def __init__( unit: str, icon: str, device_class: str, + entity_category: str, ): super().__init__(coordinator, config_entry, device) self._id = id @@ -105,6 +136,7 @@ def __init__( self.unit = unit self._icon = icon self._device_class = device_class + self._attr_entity_category = entity_category @property def state(self): diff --git a/custom_components/eufy_security/switch.py b/custom_components/eufy_security/switch.py index 7a89c7e..08dd75a 100644 --- a/custom_components/eufy_security/switch.py +++ b/custom_components/eufy_security/switch.py @@ -3,6 +3,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from .const import COORDINATOR, DOMAIN, Device, get_child_value from .coordinator import EufySecurityDataUpdateCoordinator @@ -17,28 +18,53 @@ async def async_setup_entry( coordinator: EufySecurityDataUpdateCoordinator = hass.data[DOMAIN][COORDINATOR] INSTRUMENTS = [ - ("enabled", "Enabled", "enabled"), - ("motion_detection", "Motion Detection", "motionDetection"), - ("motion_tracking", "Motion Tracking", "motionTracking"), - ("person_detection", "Person Detection", "personDetection"), - ("pet_detection", "Pet Detection", "petDetection"), - ("crying_detection", "Crying Detection", "cryingDetection"), - ("indoor_chime", "Indoor Chime", "chimeIndoor"), - ("status_led", "Status Led", "statusLed"), - ("anti_theft_detection", "Anti Theft Detection", "antitheftDetection"), - ("auto_night_vision", "Auto Night Vision", "autoNightvision"), - ("night_vision", "Night Vision", "nightvision"), - ("microphone", "Microphone", "microphone"), - ("speaker", "Speaker", "speaker"), - ("audio_recording", "Audio Recording", "audioRecording"), - ("light", "Light", "light"), - ("rtsp_stream", "RTSP Stream", "rtspStream"), + ("enabled", "Enabled", "enabled", EntityCategory.CONFIG), + ( + "motion_detection", + "Motion Detection", + "motionDetection", + EntityCategory.CONFIG, + ), + ("motion_tracking", "Motion Tracking", "motionTracking", EntityCategory.CONFIG), + ( + "person_detection", + "Person Detection", + "personDetection", + EntityCategory.CONFIG, + ), + ("pet_detection", "Pet Detection", "petDetection", EntityCategory.CONFIG), + ( + "crying_detection", + "Crying Detection", + "cryingDetection", + EntityCategory.CONFIG, + ), + ("indoor_chime", "Indoor Chime", "chimeIndoor", EntityCategory.CONFIG), + ("status_led", "Status Led", "statusLed", EntityCategory.CONFIG), + ( + "anti_theft_detection", + "Anti Theft Detection", + "antitheftDetection", + EntityCategory.CONFIG, + ), + ( + "auto_night_vision", + "Auto Night Vision", + "autoNightvision", + EntityCategory.CONFIG, + ), + ("night_vision", "Night Vision", "nightvision", EntityCategory.CONFIG), + ("microphone", "Microphone", "microphone", EntityCategory.CONFIG), + ("speaker", "Speaker", "speaker", EntityCategory.CONFIG), + ("audio_recording", "Audio Recording", "audioRecording", EntityCategory.CONFIG), + ("light", "Light", "light", EntityCategory.CONFIG), + ("rtsp_stream", "RTSP Stream", "rtspStream", EntityCategory.CONFIG), ] entities = [] for device in coordinator.devices.values(): instruments = INSTRUMENTS - for id, description, key in instruments: + for id, description, key, entity_category in instruments: if not get_child_value(device.state, key) is None: entities.append( EufySwitchEntity( @@ -50,6 +76,7 @@ async def async_setup_entry( key, "False", "True", + entity_category, ) ) @@ -67,6 +94,7 @@ def __init__( key: str, off_value: str, on_value: str, + entity_category: str, ): EufySecurityEntity.__init__(self, coordinator, config_entry, device) SwitchEntity.__init__(self) @@ -75,6 +103,7 @@ def __init__( self.key = key self.off_value = str(off_value) self.on_value = str(on_value) + self._attr_entity_category = entity_category @property def is_on(self):