├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── src ├── utils.ts ├── index.ts ├── settings.ts ├── api │ ├── Whisker.types.ts │ └── Whisker.ts ├── litterRobot.ts ├── accessories │ ├── drawerLevel.ts │ ├── occupancySensor.ts │ └── globeLight.ts └── platform.ts ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.md │ ├── support-request.md │ └── bug-report.md └── workflows │ └── build.yml ├── nodemon.json ├── tsconfig.json ├── config.schema.json ├── README.md ├── package.json ├── .eslintrc ├── .gitignore ├── .npmignore └── LICENSE /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function parseJwt (token) { 2 | return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); 3 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | }, 6 | "editor.rulers": [ 140 ], 7 | "eslint.enable": true 8 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # blank_issues_enabled: false 2 | # contact_links: 3 | # - name: Homebridge Discord Community 4 | # url: https://discord.gg/kqNCe2D 5 | # about: Ask your questions in the #YOUR_CHANNEL_HERE channel -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts", 6 | "ignore": [], 7 | "exec": "tsc && homebridge -I -D", 8 | "signal": "SIGTERM", 9 | "env": { 10 | "NODE_OPTIONS": "--trace-warnings" 11 | } 12 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { API } from 'homebridge'; 2 | 3 | import { PLATFORM_NAME } from './settings'; 4 | import { LitterRobotPlatform } from './platform'; 5 | 6 | /** 7 | * This method registers the platform with Homebridge 8 | */ 9 | export = (api: API) => { 10 | api.registerPlatform(PLATFORM_NAME, LitterRobotPlatform); 11 | }; 12 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the name of the platform that users will use to register the plugin in the Homebridge config.json 3 | */ 4 | export const PLATFORM_NAME = 'LitterRobot4'; 5 | 6 | /** 7 | * This must match the name of your plugin as defined the package.json 8 | */ 9 | export const PLUGIN_NAME = 'homebridge-litter-robot-4'; -------------------------------------------------------------------------------- /src/api/Whisker.types.ts: -------------------------------------------------------------------------------- 1 | // Axios Device Types 2 | 3 | export interface whiskerResponse { 4 | data: { 5 | query: Array; 6 | }; 7 | } 8 | 9 | export interface Robot { 10 | serial: string; 11 | name: string; 12 | isNightLightLEDOn: boolean; 13 | robotStatus: string; 14 | catDetect: string; 15 | DFILevelPercent: number; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", // ~node10 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2015", 7 | "es2016", 8 | "es2017", 9 | "es2018" 10 | ], 11 | "declaration": true, 12 | "declarationMap": true, 13 | "sourceMap": true, 14 | "outDir": "./dist", 15 | "rootDir": "./src", 16 | "strict": true, 17 | "esModuleInterop": true, 18 | "noImplicitAny": false 19 | }, 20 | "include": [ 21 | "src/" 22 | ], 23 | "exclude": [ 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/dist/index.js", 15 | "outFiles": [ 16 | "${workspaceFolder}/**/*.js" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe:** 11 | 12 | 13 | **Describe the solution you'd like:** 14 | 15 | 16 | **Describe alternatives you've considered:** 17 | 18 | 19 | **Additional context:** 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "LitterRobot4", 3 | "pluginType": "platform", 4 | "singular": true, 5 | "schema": { 6 | "type": "object", 7 | "properties": { 8 | "email": { 9 | "title": "Litter Robot App Email", 10 | "type": "string", 11 | "required": true, 12 | "default": "my_email" 13 | }, 14 | "password": { 15 | "title": "Litter Robot App Password", 16 | "type": "string", 17 | "required": true, 18 | "default": "my_password" 19 | }, 20 | "disableDrawerSensor": { 21 | "type": "boolean", 22 | "title": "Disable Drawer Level Sensor", 23 | "description": "Disables monitoring of the drawer level sensor.", 24 | "default": false, 25 | "required": false 26 | }, 27 | "debugMode": { 28 | "title": "Debug Mode", 29 | "type": "boolean", 30 | "required": false, 31 | "default": false 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | # the Node.js versions to build on 13 | node-version: [16.x, 18.x, 20.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Install dependencies 24 | run: npm install 25 | 26 | - name: Lint the project 27 | run: npm run lint 28 | 29 | - name: Build the project 30 | run: npm run build 31 | 32 | - name: List, audit, fix outdated dependencies and build again 33 | run: | 34 | npm list --outdated 35 | npm audit || true # ignore failures 36 | npm audit fix || true 37 | npm list --outdated 38 | npm run build 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support Request 3 | about: Need help? 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **Describe Your Problem:** 13 | 14 | 15 | **Logs:** 16 | 17 | ``` 18 | Show the Homebridge logs here, remove any sensitive information. 19 | ``` 20 | 21 | **Plugin Config:** 22 | 23 | ```json 24 | Show your Homebridge config.json here, remove any sensitive information. 25 | ``` 26 | 27 | **Screenshots:** 28 | 29 | 30 | **Environment:** 31 | 32 | * **Plugin Version**: 33 | * **Homebridge Version**: 34 | * **Node.js Version**: 35 | * **NPM Version**: 36 | * **Operating System**: 37 | 38 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **Describe The Bug:** 13 | 14 | 15 | **To Reproduce:** 16 | 17 | 18 | **Expected behavior:** 19 | 20 | 21 | **Logs:** 22 | 23 | ``` 24 | Show the Homebridge logs here, remove any sensitive information. 25 | ``` 26 | 27 | **Plugin Config:** 28 | 29 | ```json 30 | Show your Homebridge config.json here, remove any sensitive information. 31 | ``` 32 | 33 | **Screenshots:** 34 | 35 | 36 | **Environment:** 37 | 38 | * **Plugin Version**: 39 | * **Homebridge Version**: 40 | * **Node.js Version**: 41 | * **NPM Version**: 42 | * **Operating System**: 43 | 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Litter Robot 4 Homebridge Plugin 2 | 3 | ## Features 4 | - Supports Multiple Litter Robot 4 Devices 🤖🤖🤖🤖🤖🤖🤖 5 | - Toggle The Globle Light 💡 6 | - Cat Detect Sensor 📸 7 | - Waste Drawer Level Sensor 💰 8 | 9 | 10 | ## Supported Robot Versions 11 | - Litter Robot 4 12 | 13 | ## Installation 14 | 15 | #### For Homebridge Web UI Users 16 | Go to plugin page, search for `@rylee-s/Homebridge-Litter-Robot-4` and install it. 17 | 18 | #### For Homebridge Command Line Users 19 | 20 | Run the following command in the terminal: 21 | ``` 22 | npm install @rylee-s/Homebridge-Litter-Robot-4 23 | ``` 24 | 25 | 26 | ## Configuration 27 | 28 | Configuration should be very simple! 29 | 30 | ### Homebridge Web UI 31 | Just enter the Username and Password you use for your Whisker App 32 | 33 | ### Homebridge Command Line Users 34 | 35 | ```json 36 | { 37 | "platform": "LitterRobot4", 38 | "email": "whisker.app@email.com", 39 | "password": "WhiskerAppPassword", 40 | "disableDrawerSensor": false 41 | } 42 | ``` 43 | 44 | ## Limitations 45 | 46 | ## FAQ 47 | 48 | 49 | ## Contributing 50 | PRs and issues are welcome. 51 | 52 | # 53 | Thanks for taking a look at my project! If it helps you, please give it a star ⭐️ 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "Litter Robot 4", 3 | "name": "homebridge-litter-robot-4", 4 | "version": "3.1.0", 5 | "description": "Litter Robot 4 Plugin for Apple HomeKit via Homebridge", 6 | "license": "Apache-2.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/rylee-s/Homebridge-Litter-Robot-4.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/rylee-s/Homebridge-Litter-Robot-4/issues" 13 | }, 14 | "engines": { 15 | "node": "^18.17.0", 16 | "homebridge": "^1.6.0" 17 | }, 18 | "main": "dist/index.js", 19 | "scripts": { 20 | "lint": "eslint src/**.ts --max-warnings=0", 21 | "watch": "npm run build && npm link && nodemon", 22 | "build": "rimraf ./dist && tsc", 23 | "prepublishOnly": "npm run lint && npm run build" 24 | }, 25 | "keywords": [ 26 | "homebridge-plugin" 27 | ], 28 | "dependencies": { 29 | "axios": "^1.5.0" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^18.16.20", 33 | "@typescript-eslint/eslint-plugin": "^5.62.0", 34 | "@typescript-eslint/parser": "^5.62.0", 35 | "eslint": "^8.45.0", 36 | "homebridge": "^1.6.0", 37 | "nodemon": "^2.0.22", 38 | "rimraf": "^3.0.2", 39 | "ts-node": "^10.9.1", 40 | "typescript": "^4.9.5" 41 | } 42 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/eslint-recommended", 6 | "plugin:@typescript-eslint/recommended" // uses the recommended rules from the @typescript-eslint/eslint-plugin 7 | ], 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "sourceType": "module" 11 | }, 12 | "ignorePatterns": [ 13 | "dist" 14 | ], 15 | "rules": { 16 | "quotes": ["warn", "single"], 17 | "indent": ["warn", 2, { "SwitchCase": 1 }], 18 | "semi": ["off"], 19 | "comma-dangle": ["warn", "always-multiline"], 20 | "dot-notation": "off", 21 | "eqeqeq": "warn", 22 | "curly": ["warn", "all"], 23 | "brace-style": ["warn"], 24 | "prefer-arrow-callback": ["warn"], 25 | "max-len": ["warn", 140], 26 | "no-console": ["warn"], // use the provided Homebridge log method instead 27 | "no-non-null-assertion": ["off"], 28 | "comma-spacing": ["error"], 29 | "no-multi-spaces": ["warn", { "ignoreEOLComments": true }], 30 | "no-trailing-spaces": ["warn"], 31 | "lines-between-class-members": ["warn", "always", {"exceptAfterSingleLine": true}], 32 | "@typescript-eslint/explicit-function-return-type": "off", 33 | "@typescript-eslint/no-non-null-assertion": "off", 34 | "@typescript-eslint/explicit-module-boundary-types": "off", 35 | "@typescript-eslint/semi": ["warn"], 36 | "@typescript-eslint/member-delimiter-style": ["warn"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/litterRobot.ts: -------------------------------------------------------------------------------- 1 | import { LitterRobotPlatform } from './platform'; 2 | import Whisker from './api/Whisker'; 3 | import { Logger, PlatformConfig } from 'homebridge'; 4 | import { GlobeLightAccessory } from './accessories/globeLight'; 5 | import { OccupancySensorAccessory } from './accessories/occupancySensor'; 6 | import { DrawerLevelAccessory } from './accessories/drawerLevel'; 7 | import { Robot } from './api/Whisker.types'; 8 | 9 | export class LitterRobot { 10 | private globeLight: GlobeLightAccessory; 11 | private occupancySensor: OccupancySensorAccessory; 12 | private drawerLevel?: DrawerLevelAccessory; 13 | 14 | public uuid = { 15 | bot: this.platform.api.hap.uuid.generate(this.device.serial), 16 | globeLight: this.platform.api.hap.uuid.generate(this.device.serial + 'globeLight'), 17 | occupancySensor: this.platform.api.hap.uuid.generate(this.device.serial + 'occupancySensor'), 18 | drawerLevel: this.platform.api.hap.uuid.generate(this.device.serial + 'drawerLevel'), 19 | }; 20 | 21 | public serialNumber = this.device.serial; 22 | public name = this.device.name; 23 | 24 | constructor( 25 | private readonly account: Whisker, 26 | public readonly device: Robot, 27 | private readonly platform: LitterRobotPlatform, 28 | private readonly log: Logger, 29 | private readonly config: PlatformConfig, 30 | ) { 31 | this.log.info('Litter Robot:', device.name, device.serial); 32 | this.globeLight = new GlobeLightAccessory(this.platform, this.account, this); 33 | this.occupancySensor = new OccupancySensorAccessory(this.platform, this.account, this); 34 | if (!this.config.disableDrawerSensor) { 35 | this.drawerLevel = new DrawerLevelAccessory(this.platform, this.account, this); 36 | } 37 | } 38 | 39 | public update(device: Robot): void { 40 | this.globeLight?.update(device.isNightLightLEDOn); 41 | this.occupancySensor?.update(device.robotStatus); 42 | if (!this.config.disableDrawerSensor) { 43 | this.drawerLevel?.update(device.DFILevelPercent); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/accessories/drawerLevel.ts: -------------------------------------------------------------------------------- 1 | import { LitterRobotPlatform } from '../platform'; 2 | import { Service, PlatformAccessory } from 'homebridge'; 3 | import Whisker from '../api/Whisker'; 4 | import { LitterRobot } from '../litterRobot'; 5 | 6 | export class DrawerLevelAccessory { 7 | private readonly log; 8 | private service: Service; 9 | private name: string; 10 | private uuid: string; 11 | private accessory: PlatformAccessory; 12 | private state = { 13 | Level: 0, 14 | }; 15 | 16 | 17 | constructor( 18 | private readonly platform: LitterRobotPlatform, 19 | private readonly account: Whisker, 20 | private readonly LitterRobot: LitterRobot, 21 | ) { 22 | this.log = this.platform.log; 23 | this.name = this.LitterRobot.name + ' Drawer Level'; 24 | this.uuid = this.LitterRobot.uuid.drawerLevel; 25 | this.accessory = this.platform.getOrCreateAccessory(this.uuid, this.name); 26 | 27 | // create a new HumiditySensor service 28 | this.service = this.accessory.getService(this.platform.Service.HumiditySensor) || 29 | this.accessory.addService(this.platform.Service.HumiditySensor); 30 | 31 | 32 | // create handlers for required characteristics 33 | this.service.getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity) 34 | .onGet(this.handleCurrentRelativeHumidityGet.bind(this)); 35 | 36 | } 37 | 38 | /** 39 | * Handle requests to get the current value of the "Current Relative Humidity" characteristic 40 | */ 41 | handleCurrentRelativeHumidityGet() { 42 | this.log.debug('Triggered GET CurrentRelativeHumidity'); 43 | return this.state.Level;; 44 | } 45 | 46 | // update the state of the HumiditySensor on the platform 47 | update(level: number) { 48 | // correct for the fact that the litter robot reports 110% when full 49 | level = Math.round(level / 110 * 100); 50 | if (this.state.Level !== level) { 51 | this.platform.log.debug(`Updating ${this.name} -> `, level); 52 | this.state.Level = level; 53 | this.service.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, level); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/accessories/occupancySensor.ts: -------------------------------------------------------------------------------- 1 | import { Service, PlatformAccessory } from 'homebridge'; 2 | import { LitterRobot } from '../litterRobot'; 3 | 4 | import { LitterRobotPlatform } from '../platform'; 5 | import Whisker from '../api/Whisker'; 6 | 7 | export class OccupancySensorAccessory { 8 | private service: Service; 9 | private accessory: PlatformAccessory; 10 | private name: string; 11 | private uuid: string; 12 | private characteristic = this.platform.Characteristic.OccupancyDetected; 13 | 14 | private lookup = { 15 | 'ROBOT_CAT_DETECT_DELAY': this.characteristic.OCCUPANCY_DETECTED, 16 | 'ROBOT_IDLE': this.characteristic.OCCUPANCY_NOT_DETECTED, 17 | }; 18 | 19 | private state = { 20 | isOccupied: this.characteristic.OCCUPANCY_NOT_DETECTED, 21 | }; 22 | 23 | constructor( 24 | private readonly platform: LitterRobotPlatform, 25 | private readonly account: Whisker, 26 | private readonly LitterRobot: LitterRobot, 27 | ) { 28 | this.name = this.LitterRobot.name + ' Cat Sensor'; 29 | this.uuid = this.LitterRobot.uuid.occupancySensor; 30 | this.accessory = this.platform.getOrCreateAccessory(this.uuid, this.name); 31 | 32 | this.service = this.accessory.getService(this.platform.Service.OccupancySensor) || 33 | this.accessory.addService(this.platform.Service.OccupancySensor); 34 | 35 | // create handlers for required characteristics 36 | this.service.getCharacteristic(this.platform.Characteristic.OccupancyDetected) 37 | .onGet(this.handleOccupancyDetectedGet.bind(this)); 38 | 39 | } 40 | 41 | // update the state of the LightBulb on the platform 42 | update(catDetectValue: string) { 43 | const newValue = this.lookup[catDetectValue] || this.characteristic.OCCUPANCY_NOT_DETECTED; 44 | if (this.state.isOccupied !== newValue) { 45 | this.platform.log.debug(`Updating ${this.name} -> `, newValue); 46 | this.state.isOccupied = newValue; 47 | this.service.updateCharacteristic(this.platform.Characteristic.OccupancyDetected, newValue); 48 | } 49 | } 50 | 51 | /** 52 | * Handle requests to get the current value of the "Occupancy Detected" characteristic 53 | */ 54 | handleOccupancyDetectedGet() { 55 | this.platform.log.debug('Triggered GET OccupancyDetected'); 56 | return this.state.isOccupied; 57 | } 58 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore compiled code 2 | dist 3 | 4 | # ------------- Defaults ------------- # 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | .parcel-cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and not Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # Stores VSCode versions used for testing VSCode extensions 113 | .vscode-test 114 | 115 | # yarn v2 116 | 117 | .yarn/cache 118 | .yarn/unplugged 119 | .yarn/build-state.yml 120 | .pnp.* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignore source code 2 | src 3 | 4 | # ------------- Defaults ------------- # 5 | 6 | # gitHub actions 7 | .github 8 | 9 | # eslint 10 | .eslintrc 11 | 12 | # typescript 13 | tsconfig.json 14 | 15 | # vscode 16 | .vscode 17 | 18 | # nodemon 19 | nodemon.json 20 | 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | lerna-debug.log* 28 | 29 | # Diagnostic reports (https://nodejs.org/api/report.html) 30 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 31 | 32 | # Runtime data 33 | pids 34 | *.pid 35 | *.seed 36 | *.pid.lock 37 | 38 | # Directory for instrumented libs generated by jscoverage/JSCover 39 | lib-cov 40 | 41 | # Coverage directory used by tools like istanbul 42 | coverage 43 | *.lcov 44 | 45 | # nyc test coverage 46 | .nyc_output 47 | 48 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 49 | .grunt 50 | 51 | # Bower dependency directory (https://bower.io/) 52 | bower_components 53 | 54 | # node-waf configuration 55 | .lock-wscript 56 | 57 | # Compiled binary addons (https://nodejs.org/api/addons.html) 58 | build/Release 59 | 60 | # Dependency directories 61 | node_modules/ 62 | jspm_packages/ 63 | 64 | # Snowpack dependency directory (https://snowpack.dev/) 65 | web_modules/ 66 | 67 | # TypeScript cache 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | .npm 72 | 73 | # Optional eslint cache 74 | .eslintcache 75 | 76 | # Microbundle cache 77 | .rpt2_cache/ 78 | .rts2_cache_cjs/ 79 | .rts2_cache_es/ 80 | .rts2_cache_umd/ 81 | 82 | # Optional REPL history 83 | .node_repl_history 84 | 85 | # Output of 'npm pack' 86 | *.tgz 87 | 88 | # Yarn Integrity file 89 | .yarn-integrity 90 | 91 | # dotenv environment variables file 92 | .env 93 | .env.test 94 | 95 | # parcel-bundler cache (https://parceljs.org/) 96 | .cache 97 | .parcel-cache 98 | 99 | # Next.js build output 100 | .next 101 | 102 | # Nuxt.js build / generate output 103 | .nuxt 104 | 105 | # Gatsby files 106 | .cache/ 107 | # Comment in the public line in if your project uses Gatsby and not Next.js 108 | # https://nextjs.org/blog/next-9-1#public-directory-support 109 | # public 110 | 111 | # vuepress build output 112 | .vuepress/dist 113 | 114 | # Serverless directories 115 | .serverless/ 116 | 117 | # FuseBox cache 118 | .fusebox/ 119 | 120 | # DynamoDB Local files 121 | .dynamodb/ 122 | 123 | # TernJS port file 124 | .tern-port 125 | 126 | # Stores VSCode versions used for testing VSCode extensions 127 | .vscode-test 128 | 129 | # yarn v2 130 | 131 | .yarn/cache 132 | .yarn/unplugged 133 | .yarn/build-state.yml 134 | .pnp.* 135 | -------------------------------------------------------------------------------- /src/accessories/globeLight.ts: -------------------------------------------------------------------------------- 1 | import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; 2 | 3 | import { LitterRobotPlatform } from '../platform'; 4 | import Whisker from '../api/Whisker'; 5 | import { LitterRobot } from '../litterRobot'; 6 | 7 | /** 8 | * Platform Accessory 9 | * An instance of this class is created for each accessory your platform registers 10 | * Each accessory may expose multiple services of different service types. 11 | */ 12 | export class GlobeLightAccessory { 13 | private service: Service; 14 | private accessory: PlatformAccessory; 15 | private name: string; 16 | private uuid: string; 17 | private state = { 18 | On: true, 19 | }; 20 | 21 | constructor( 22 | private readonly platform: LitterRobotPlatform, 23 | private readonly account: Whisker, 24 | private readonly LitterRobot: LitterRobot, 25 | ) { 26 | this.name = this.LitterRobot.name + ' Globe Light'; 27 | this.uuid = this.LitterRobot.uuid.globeLight; 28 | this.accessory = this.platform.getOrCreateAccessory(this.uuid, this.name); 29 | 30 | // set accessory information 31 | this.accessory.getService(this.platform.Service.AccessoryInformation)! 32 | .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Whisker Inc.') 33 | .setCharacteristic(this.platform.Characteristic.Model, 'Litter Robot 4 Globe Light') 34 | .setCharacteristic(this.platform.Characteristic.SerialNumber, this.LitterRobot.serialNumber); 35 | 36 | // get the LightBulb service if it exists, otherwise create a new LightBulb service 37 | // you can create multiple services for each accessory 38 | this.service = this.accessory.getService(this.platform.Service.Lightbulb) || 39 | this.accessory.addService(this.platform.Service.Lightbulb); 40 | 41 | this.service.setCharacteristic(this.platform.Characteristic.Name, this.name); 42 | 43 | // register handlers for the On/Off Characteristic 44 | this.service.getCharacteristic(this.platform.Characteristic.On) 45 | .onSet(this.toggle.bind(this)) // SET - bind to the `setOn` method below 46 | .onGet(this.getStatus.bind(this)); // GET - bind to the `getOn` method below 47 | } 48 | 49 | // update the state of the LightBulb on the platform 50 | update(isOn: boolean) { 51 | if (this.state.On !== isOn) { 52 | this.platform.log.debug(`Updating ${this.name} -> `, isOn); 53 | this.state.On = isOn; 54 | this.service.updateCharacteristic(this.platform.Characteristic.On, isOn); 55 | } 56 | } 57 | 58 | /** 59 | * Handle "SET" requests from HomeKit 60 | * These are sent when the user changes the state of an accessory, for example, turning on a Light bulb. 61 | */ 62 | async toggle(value: CharacteristicValue) { 63 | const commandValue = value ? 'nightLightModeOn' : 'nightLightModeOff'; 64 | const command = JSON.stringify({ 65 | query: `mutation { 66 | sendLitterRobot4Command(input: {serial: "${this.LitterRobot.serialNumber}", command: "${commandValue}"}) 67 | }`, 68 | }); 69 | 70 | this.platform.log.debug('Toggle Globle Light -> ', value, command); 71 | 72 | this.account.sendCommand(command).then((response) => { 73 | this.platform.log.debug('Toggle Globle Light Cmd Resonse -> ', response.data); 74 | }); 75 | this.state.On = value as boolean; 76 | return value; 77 | } 78 | 79 | /** 80 | * Handle the "GET" requests from HomeKit 81 | * These are sent when HomeKit wants to know the current state of the accessory, for example, checking if a Light bulb is on. 82 | */ 83 | async getStatus(): Promise { 84 | this.platform.log.debug(`Getting ${this.name} Status...`); 85 | return this.state.On; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/api/Whisker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PlatformConfig, 3 | Logger, 4 | PlatformAccessory, 5 | API, 6 | } from 'homebridge'; 7 | 8 | import axios, { AxiosPromise } from 'axios'; 9 | import { parseJwt } from '../utils'; 10 | import { whiskerResponse } from './Whisker.types'; 11 | 12 | export default class Whisker { 13 | public accountId?: string; 14 | private token?: string; 15 | private refreshToken?: string; 16 | 17 | private readonly GCS_LOGIN_URL = 18 | 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=AIzaSyCv84jel7JkCQlsgqr_slXf3f3x-cMG15Q'; 19 | 20 | private readonly AWS_LOGIN_URL = 'https://42nk7qrhdg.execute-api.us-east-1.amazonaws.com/prod/login'; 21 | private readonly TOKEN_REFRESH_URL = 'https://securetoken.googleapis.com/v1/token?key=AIzaSyCv84jel7JkCQlsgqr_slXf3f3x-cMG15Q'; 22 | private readonly API_URL = 'https://lr4.iothings.site/graphql'; 23 | 24 | constructor( 25 | readonly config: PlatformConfig, 26 | public readonly log: Logger, 27 | public readonly accessories: PlatformAccessory[], 28 | public readonly api: API, 29 | ) { 30 | this.log.debug('Whisker Instance Created'); 31 | // refresh token every 55 minutes 32 | setInterval(() => { 33 | this.refreshJWTToken(); 34 | }, 3300000); 35 | } 36 | 37 | public async authenticate(): Promise { 38 | this.log.debug('Authenticating'); 39 | const data = JSON.stringify({ 40 | 'email': this.config.email, 41 | 'password': this.config.password, 42 | }); 43 | this.log.debug('Authenticate: Data -> ', data); 44 | 45 | const config = { 46 | method: 'post', 47 | maxBodyLength: Infinity, 48 | url: this.AWS_LOGIN_URL, 49 | headers: { 50 | 'x-api-key': 'w2tPFbjlP13GUmb8dMjUL5B2YyPVD3pJ7Ey6fz8v', 51 | 'Content-Type': 'application/json', 52 | 'Accept': '*/*', 53 | 'Accept-Encoding': 'gzip, deflate', 54 | }, 55 | data : data, 56 | }; 57 | 58 | await axios.request(config) 59 | .then(async (response) => { 60 | this.log.debug('Authenticate: Get Token Response -> ', JSON.stringify(response.data)); 61 | await this.verifyToken(response.data.token).then((token) => { 62 | this.token = `Bearer ${token}`; 63 | }); 64 | }) 65 | .catch((error) => { 66 | this.log.error(error); 67 | }); 68 | } 69 | 70 | private async verifyToken(token: string): Promise { 71 | const parsed: { claims: { mid: string } } = parseJwt(token); 72 | this.accountId = parsed.claims.mid; 73 | this.log.debug('UserId set -> ', this.accountId); 74 | this.log.debug('Verifying Token'); 75 | const data = JSON.stringify({ 76 | 'returnSecureToken': 'True', 77 | 'token': token}); 78 | 79 | const config = { 80 | method: 'post', 81 | maxBodyLength: Infinity, 82 | url: this.GCS_LOGIN_URL, 83 | headers: { 84 | 'x-ios-bundle-identifier': 'com.whisker.ios', 85 | 'Content-Type': 'application/json', 86 | }, 87 | data : data, 88 | }; 89 | 90 | return axios.request(config) 91 | .then((response) => { 92 | this.token = response.data.idToken; 93 | this.refreshToken = response.data.refreshToken; 94 | return response.data.idToken; 95 | }) 96 | .catch(() => { 97 | return 'error'; 98 | }); 99 | 100 | } 101 | 102 | private async refreshJWTToken(): Promise { 103 | this.log.debug('Refreshing Token'); 104 | const data = JSON.stringify({ 105 | 'grantType': 'refresh_token', 106 | 'refreshToken': this.refreshToken}); 107 | this.log.debug('Refresh Token Data -> ', data); 108 | const config = { 109 | method: 'post', 110 | maxBodyLength: Infinity, 111 | url: this.TOKEN_REFRESH_URL, 112 | headers: { 113 | 'x-ios-bundle-identifier': 'com.whisker.ios', 114 | 'Content-Type': 'application/json', 115 | }, 116 | data : data, 117 | }; 118 | 119 | axios.request(config) 120 | .then((response) => { 121 | this.log.debug(JSON.stringify(response.data)); 122 | this.token = response.data.access_token; 123 | this.refreshToken = response.data.refresh_token; 124 | }) 125 | .catch((error) => { 126 | this.log.debug(error); 127 | }); 128 | 129 | } 130 | 131 | public async sendCommand(command: string): AxiosPromise { 132 | const config = { 133 | method: 'post', 134 | maxBodyLength: Infinity, 135 | url: this.API_URL, 136 | headers: { 137 | 'authorization': this.token, 138 | 'Content-Type': 'application/json', 139 | }, 140 | data: command, 141 | }; 142 | 143 | return axios.request(config); 144 | } 145 | } -------------------------------------------------------------------------------- /src/platform.ts: -------------------------------------------------------------------------------- 1 | import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; 2 | 3 | import Whisker from './api/Whisker'; 4 | import { LitterRobot } from './litterRobot'; 5 | import { PLUGIN_NAME, PLATFORM_NAME } from './settings'; 6 | import { Robot } from './api/Whisker.types'; 7 | 8 | /** 9 | * HomebridgePlatform 10 | * This class is the main constructor for your plugin, this is where you should 11 | * parse the user config and discover/register accessories with Homebridge. 12 | */ 13 | export class LitterRobotPlatform implements DynamicPlatformPlugin { 14 | public readonly Service: typeof Service = this.api.hap.Service; 15 | public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; 16 | 17 | // this is used to track restored cached accessories 18 | public readonly accessories: PlatformAccessory[] = []; 19 | public litterRobots: LitterRobot[] = []; 20 | constructor( 21 | public readonly log: Logger, 22 | public readonly config: PlatformConfig, 23 | public readonly api: API, 24 | ) { 25 | this.log.debug('Finished initializing platform:', this.config.name); 26 | 27 | const account = new Whisker(this.config, this.log, this.accessories, this.api); 28 | 29 | 30 | // When this event is fired it means Homebridge has restored all cached accessories from disk. 31 | // Dynamic Platform plugins should only register new accessories after this event was fired, 32 | // in order to ensure they weren't added to homebridge already. This event can also be used 33 | // to start discovery of new accessories. 34 | this.api.on('didFinishLaunching', () => { 35 | log.debug('Executed didFinishLaunching callback'); 36 | // run the method to discover / register your devices as accessories 37 | account.authenticate().then(() => { 38 | this.log.debug('Authenticated now discovering devices'); 39 | this.discoverDevices(account); 40 | this.pollForUpdates(account, 100000); 41 | }); 42 | }); 43 | } 44 | 45 | getOrCreateAccessory(uuid: string, name: string) { 46 | const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); 47 | if (existingAccessory) { 48 | const skipDrawerLevel = this.config.disableDrawerSensor; 49 | const isDrawerLevel = existingAccessory.services[1].constructor.name === 'HumiditySensor'; 50 | if (skipDrawerLevel && isDrawerLevel) { 51 | this.log.info('Skipping DrawerLevel:', name); 52 | this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]); 53 | return existingAccessory; 54 | } 55 | this.log.info('Restoring existing accessory:', name); 56 | return existingAccessory; 57 | } else { 58 | this.log.info('Adding new accessory:', name); 59 | const accessory = new this.api.platformAccessory(name, uuid); 60 | this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); 61 | return accessory; 62 | } 63 | } 64 | 65 | /** 66 | * This function is invoked when homebridge restores cached accessories from disk at startup. 67 | * It should be used to setup event handlers for characteristics and update respective values. 68 | */ 69 | configureAccessory(accessory: PlatformAccessory) { 70 | this.log.info('Loading accessory from cache:', accessory.displayName); 71 | 72 | // add the restored accessory to the accessories cache so we can track if it has already been registered 73 | this.accessories.push(accessory); 74 | } 75 | 76 | /** 77 | * This is an example method showing how to register discovered accessories. 78 | * Accessories must only be registered once, previously created accessories 79 | * must not be registered again to prevent "duplicate UUID" errors. 80 | */ 81 | async discoverDevices(account: Whisker) { 82 | const data = JSON.stringify({ 83 | query: `{ 84 | query: getLitterRobot4ByUser(userId: "${account.accountId}") { 85 | serial 86 | name 87 | isNightLightLEDOn 88 | } 89 | 90 | }`, 91 | }); 92 | account.sendCommand(data).then((response) => { 93 | this.log.debug('Discovered devices:', JSON.stringify(response.data.data.query)); 94 | let devices = response.data.data.query; 95 | devices ||= []; 96 | 97 | // loop over the discovered devices and register each one 98 | for (const device of devices) { 99 | this.log.debug('Discovered device:', device.name, device.serial); 100 | this.litterRobots.push(new LitterRobot(account, device, this, this.log, this.config)); 101 | } 102 | }); 103 | } 104 | 105 | // Poll for updates 106 | pollForUpdates(account: Whisker, interval: number) { 107 | const command = JSON.stringify({ 108 | query: `{ 109 | query: getLitterRobot4ByUser(userId: "${account.accountId}") { 110 | serial 111 | name 112 | isNightLightLEDOn 113 | robotStatus 114 | catDetect 115 | DFILevelPercent 116 | } 117 | }`, 118 | }); 119 | account.sendCommand(command).then((response) => { 120 | const data = response.data.data.query; 121 | 122 | data.forEach((device: Robot) => { 123 | const litterRobot = this.litterRobots.find((bot) => bot.serialNumber === device.serial); 124 | if (litterRobot) { 125 | litterRobot.update(device); 126 | } 127 | }); 128 | 129 | setTimeout(() => { 130 | this.pollForUpdates(account, interval); 131 | }, interval); 132 | }); 133 | } 134 | } 135 | 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS --------------------------------------------------------------------------------