Skip to content

Commit f0f6ece

Browse files
authored
Fix HTTP 422 error handling and improve status code messaging (#163)
1 parent b526a55 commit f0f6ece

File tree

5 files changed

+397
-6
lines changed

5 files changed

+397
-6
lines changed

.copilot/instructions.md

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# Copilot Instructions for Homebridge August Plugin
2+
3+
## Repository Overview
4+
5+
This is a Homebridge plugin that provides HomeKit integration for August and Yale smart locks. The plugin is written in TypeScript with ES modules and follows Homebridge platform patterns.
6+
7+
**Branch Strategy**: This repository uses a beta-first development workflow where all changes must target beta branches before being merged to main. Version bumps are determined by issue labels (patch/minor/major) that must be set before assigning work to Copilot.
8+
9+
## Architecture
10+
11+
- **Plugin Type**: Homebridge dynamic platform plugin
12+
- **Language**: TypeScript with ES modules (ES2022)
13+
- **Runtime**: Node.js 20+ or 22+
14+
- **Build System**: TypeScript compiler (tsc) with bundler module resolution
15+
- **Testing**: Vitest with coverage support
16+
- **Linting**: ESLint with Antfu config and custom rules
17+
- **Documentation**: TypeDoc with modern theme
18+
- **External API**: august-yale package for August/Yale API communication
19+
20+
## Key Files and Directories
21+
22+
- `src/` - TypeScript source code
23+
- `index.ts` - Plugin entry point that registers the platform
24+
- `platform.ts` - Main platform class (AugustPlatform)
25+
- `settings.ts` - Configuration interfaces and constants
26+
- `devices/` - Device-specific implementations
27+
- `lock.ts` - Lock mechanism implementation
28+
- `device.ts` - Base device class
29+
- `homebridge-ui/` - Custom UI for Homebridge Config UI X
30+
- `*.test.ts` - Test files (Vitest)
31+
- `dist/` - Compiled JavaScript output (generated, excluded from git)
32+
- `config.schema.json` - Homebridge configuration schema with custom UI
33+
- `package.json` - Dependencies and npm scripts
34+
- `tsconfig.json` - TypeScript configuration (ES2022, bundler resolution)
35+
- `eslint.config.js` - ESLint configuration with Antfu base and custom rules
36+
37+
## Development Workflow
38+
39+
### Branch Strategy and PR Workflow
40+
41+
**IMPORTANT**: All pull requests must target a beta branch first, never directly to the main branch.
42+
43+
#### Beta Branch Requirements
44+
- All PRs must be directed to a branch that starts with "beta-"
45+
- Beta branches should be named `beta-X.Y.Z` based on the expected version bump
46+
- If no appropriate beta branch exists, create one based on the next possible version:
47+
- **patch** releases (bug fixes): `beta-X.Y.Z+1` (e.g., current 3.0.2 → beta-3.0.3)
48+
- **minor** releases (new features): `beta-X.Y+1.0` (e.g., current 3.0.2 → beta-3.1.0)
49+
- **major** releases (breaking changes): `beta-X+1.0.0` (e.g., current 3.0.2 → beta-4.0.0)
50+
51+
#### Required Labels
52+
Before assigning any issue to Copilot, the following labels **must** be set to determine the version bump:
53+
- `patch` - for bug fixes and minor improvements
54+
- `minor` - for new features and enhancements
55+
- `major` - for breaking changes
56+
57+
#### Workflow Steps
58+
1. Ensure proper label (patch/minor/major) is set on the issue
59+
2. Identify or create appropriate beta branch based on the label
60+
3. Target all development work and PRs to the beta branch
61+
4. Once beta testing is complete, merge beta branch to main for release
62+
63+
### Building
64+
```bash
65+
npm run build # Full build: clean + compile + copy UI files
66+
npm run clean # Remove dist directory
67+
npm run watch # Build + link + watch with nodemon
68+
```
69+
70+
### Testing
71+
```bash
72+
npm test # Run Vitest tests once
73+
npm run test:watch # Run tests in watch mode
74+
npm run test-coverage # Run tests with coverage report
75+
```
76+
77+
### Linting
78+
```bash
79+
npm run lint # Check for ESLint issues
80+
npm run lint:fix # Auto-fix ESLint issues
81+
```
82+
83+
### Documentation
84+
```bash
85+
npm run docs # Generate TypeDoc documentation
86+
npm run docs:lint # Validate documentation (treat warnings as errors)
87+
npm run docs:theme # Generate docs with default-modern theme
88+
```
89+
90+
### Development Setup
91+
```bash
92+
npm run watch # Start development with hot reload
93+
npm run plugin-ui # Copy UI files to dist
94+
```
95+
96+
## Coding Standards
97+
98+
1. **TypeScript**:
99+
- ES2022 target with bundler module resolution
100+
- Strict mode enabled (except noImplicitAny: false)
101+
- Use ES modules with `.js` file extensions in imports
102+
- Generate declaration files and source maps
103+
104+
2. **ESLint**:
105+
- Antfu ESLint config as base
106+
- Custom rules for import ordering (perfectionist)
107+
- JSDoc alignment enforcement
108+
- Multi-line curly braces only
109+
- Consistent quote props
110+
- Test-specific rules (no-only-tests)
111+
112+
3. **Code Style**:
113+
- Use 1TBS brace style with single line allowed
114+
- Sort imports by type (builtin, external, internal, relative)
115+
- Sort named imports and exports
116+
- Use proper JSDoc formatting and alignment
117+
118+
4. **Homebridge Patterns**:
119+
- Implement DynamicPlatformPlugin interface
120+
- Use proper platform registration in index.ts
121+
- Follow accessory lifecycle management
122+
- Implement proper logging with Homebridge logger
123+
- Use PlatformAccessory for device management
124+
125+
5. **Error Handling**:
126+
- Proper try-catch blocks for async operations
127+
- Meaningful error messages with context
128+
- Graceful degradation for API failures
129+
130+
## Homebridge Specifics
131+
132+
- **Platform Type**: Dynamic platform plugin (not accessory plugin)
133+
- **Platform Name**: "August" (defined in settings.ts)
134+
- **Plugin Name**: "homebridge-august" (must match package.json name)
135+
- **Configuration Schema**: Uses config.schema.json with custom UI
136+
- **Custom UI**: Located in src/homebridge-ui/ with custom configuration interface
137+
- **Platform Class**: AugustPlatform extends DynamicPlatformPlugin
138+
- **Device Management**: Uses PlatformAccessory for caching and state management
139+
- **HAP Services**: Implement LockMechanism service for smart locks
140+
- **Authentication**: Handle August API authentication with validation codes
141+
- **Rate Limiting**: Implement proper refresh/update/push rates for API calls
142+
143+
## August/Yale Integration
144+
145+
- **API Package**: Uses `august-yale` npm package for API communication
146+
- **Authentication Flow**:
147+
1. User provides augustId (email/phone) and password
148+
2. System sends validation code to user
149+
3. User enters validation code to complete authentication
150+
4. API credentials are stored and used for subsequent requests
151+
- **Device Types**: Support multiple lock types (August Smart Lock, Yale Assure Lock variants)
152+
- **State Synchronization**: Keep HomeKit state in sync with August/Yale API
153+
- **Polling**: Implement configurable refresh rates for device state updates
154+
- **Configuration**: Use credentials interface for API authentication settings
155+
156+
## When Making Changes
157+
158+
**CRITICAL**: Before starting any work, ensure the issue has the appropriate label (patch/minor/major) and target the correct beta branch.
159+
160+
0. **Branch Targeting**:
161+
- Never target PRs directly to main branch
162+
- Always target a beta branch (beta-X.Y.Z format)
163+
- Create beta branch if none exists for the expected version
164+
- Use issue labels to determine version bump:
165+
- `patch`: Bug fixes → beta-X.Y.Z+1
166+
- `minor`: New features → beta-X.Y+1.0
167+
- `major`: Breaking changes → beta-X+1.0.0
168+
169+
1. **Dependencies**:
170+
- Only add well-maintained dependencies
171+
- Keep runtime dependencies minimal
172+
- Update package.json and package-lock.json properly
173+
- Consider bundle size impact
174+
175+
2. **Breaking Changes**:
176+
- Follow semantic versioning
177+
- Update CHANGELOG.md appropriately
178+
- Consider migration paths for users
179+
- Test with multiple Homebridge versions
180+
- **Must** use `major` label and target appropriate beta-X+1.0.0 branch
181+
182+
3. **Configuration**:
183+
- Update config.schema.json for new options
184+
- Maintain backward compatibility
185+
- Add proper validation and defaults
186+
- Update TypeScript interfaces in settings.ts
187+
188+
4. **Documentation**:
189+
- Update README.md for user-facing changes
190+
- Add TSDoc comments for new APIs
191+
- Update TypeDoc comments for generated docs
192+
- Consider updating examples
193+
194+
5. **Testing**:
195+
- Add Vitest tests for new features
196+
- Mock external dependencies (August API)
197+
- Test error scenarios and edge cases
198+
- Maintain test coverage
199+
200+
6. **Compatibility**:
201+
- Support Node.js 20+ and 22+
202+
- Support Homebridge 1.9.0+ and 2.0.0+
203+
- Test with Config UI X integration
204+
- Verify ES module compatibility
205+
206+
## Common Patterns
207+
208+
- **RxJS**: Use reactive programming patterns where appropriate
209+
- **Async/Await**: Prefer async/await over Promise chains
210+
- **ES Modules**: Use ES module syntax with `.js` extensions in imports
211+
- **Interface Design**: Define clear TypeScript interfaces in settings.ts
212+
- **Error Handling**: Implement comprehensive error handling with meaningful messages
213+
- **Logging**: Use this.log with appropriate log levels (info, warn, error, debug)
214+
- **Device Lifecycle**: Implement proper setup/cleanup in platform methods
215+
- **Configuration Validation**: Validate user configuration and provide helpful error messages
216+
- **API Rate Limiting**: Respect August API rate limits with configurable intervals
217+
- **State Management**: Maintain device state consistency between API and HomeKit
218+
219+
## Important Constants and Settings
220+
221+
- **PLATFORM_NAME**: "August" (used in Homebridge registration)
222+
- **PLUGIN_NAME**: "homebridge-august" (must match package.json)
223+
- **Plugin Type**: "platform" (not accessory)
224+
- **Custom UI**: Enabled with path "./dist/homebridge-ui"
225+
- **Configuration**: Uses credentials and options interfaces
226+
- **Device Types**: Support for various August and Yale lock models
227+
228+
## Testing Guidelines
229+
230+
- **Framework**: Use Vitest for testing with TypeScript support
231+
- **Test Files**: Name test files with `.test.ts` extension in src/
232+
- **Coverage**: Aim for good test coverage, use `npm run test-coverage`
233+
- **Mocking**: Mock external dependencies, especially:
234+
- August/Yale API calls
235+
- Homebridge API interactions
236+
- File system operations
237+
- **Test Categories**:
238+
- Unit tests for individual functions and classes
239+
- Integration tests for platform initialization
240+
- Configuration validation tests
241+
- Error handling tests
242+
- **Test Data**: Use realistic test data that matches August API responses
243+
- **Async Testing**: Properly test async operations and promises
244+
- **CI/CD**: Ensure tests run in continuous integration pipeline
245+
246+
## Files to Avoid Modifying
247+
248+
- `dist/` directory (generated by TypeScript compilation)
249+
- `node_modules/` directory (managed by npm)
250+
- `docs/` directory (generated by TypeDoc)
251+
- `.git/` directory and git metadata
252+
- Files listed in `.gitignore`
253+
- `package-lock.json` (only modify through npm commands)
254+
255+
## Security Considerations
256+
257+
- **Credentials**: Never log or expose user credentials
258+
- **API Keys**: Handle August API credentials securely
259+
- **Validation**: Validate all user inputs
260+
- **Error Messages**: Don't expose sensitive information in error messages
261+
- **Storage**: Store credentials securely using Homebridge storage mechanisms

src/devices/device.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
3+
import { deviceBase } from './device.js'
4+
5+
describe('deviceBase statusCode', () => {
6+
// Test the statusCode method directly by mocking only what we need
7+
const createMockDevice = () => {
8+
// Create a minimal mock that satisfies TypeScript
9+
const mockDevice = {
10+
debugLog: vi.fn().mockImplementation(async () => {}),
11+
debugErrorLog: vi.fn().mockImplementation(async () => {}),
12+
}
13+
// Bind the statusCode method to our mock device
14+
const boundStatusCode = deviceBase.prototype.statusCode.bind(mockDevice)
15+
return { ...mockDevice, statusCode: boundStatusCode }
16+
}
17+
18+
it('should handle 422 status code with appropriate message', async () => {
19+
const device = createMockDevice()
20+
const error = { message: 'PUT failed with: 422' }
21+
22+
await device.statusCode('pushChanges', error)
23+
24+
expect(device.debugLog).toHaveBeenCalledWith(
25+
'Unprocessable Entity - The request was well-formed but could not be processed. This may indicate the lock is in an invalid state or the operation is not allowed at this time, statusCode: PUT failed with: 422',
26+
)
27+
expect(device.debugErrorLog).not.toHaveBeenCalled()
28+
})
29+
30+
it('should handle unknown status codes', async () => {
31+
const device = createMockDevice()
32+
const error = { message: 'PUT failed with: 500' }
33+
34+
await device.statusCode('pushChanges', error)
35+
36+
expect(device.debugLog).toHaveBeenCalledWith(
37+
'Unknown statusCode: PUT failed with: 500, Submit Bugs Here: https://tinyurl.com/AugustYaleBug',
38+
)
39+
expect(device.debugErrorLog).toHaveBeenCalledWith('failed pushChanges, Error: [object Object]')
40+
})
41+
42+
it('should handle 200 status code successfully', async () => {
43+
const device = createMockDevice()
44+
const error = { message: '200 OK' }
45+
46+
await device.statusCode('pushChanges', error)
47+
48+
expect(device.debugLog).toHaveBeenCalledWith('Request successful, statusCode: 200 OK')
49+
expect(device.debugErrorLog).not.toHaveBeenCalled()
50+
})
51+
52+
it('should handle 429 status code for rate limiting', async () => {
53+
const device = createMockDevice()
54+
const error = { message: 'PUT failed with: 429' }
55+
56+
await device.statusCode('pushChanges', error)
57+
58+
expect(device.debugLog).toHaveBeenCalledWith(
59+
'Too Many Requests, exceeded the number of requests allowed for a given time window, statusCode: PUT failed with: 429',
60+
)
61+
expect(device.debugErrorLog).not.toHaveBeenCalled()
62+
})
63+
64+
it('should extract status code from different message formats', async () => {
65+
const device = createMockDevice()
66+
67+
// Test extraction from "422"
68+
const error1 = { message: '422' }
69+
await device.statusCode('test', error1)
70+
expect(device.debugLog).toHaveBeenCalledWith(
71+
'Unprocessable Entity - The request was well-formed but could not be processed. This may indicate the lock is in an invalid state or the operation is not allowed at this time, statusCode: 422',
72+
)
73+
74+
// Reset mock
75+
device.debugLog.mockClear()
76+
device.debugErrorLog.mockClear()
77+
78+
// Test extraction from "API call failed: 422"
79+
const error2 = { message: 'API call failed: 422' }
80+
await device.statusCode('test', error2)
81+
expect(device.debugLog).toHaveBeenCalledWith(
82+
'Unprocessable Entity - The request was well-formed but could not be processed. This may indicate the lock is in an invalid state or the operation is not allowed at this time, statusCode: API call failed: 422',
83+
)
84+
})
85+
})

src/devices/device.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,16 +157,22 @@ export abstract class deviceBase {
157157

158158
async statusCode(action: string, error: { message: string }): Promise<void> {
159159
const statusCodeString = error.message // Convert statusCode to a string
160+
161+
// Extract numeric status code from error message (e.g., "PUT failed with: 422" -> "422")
162+
const statusCodeMatch = statusCodeString.match(/\b(\d{3})\b/)
163+
const statusCode = statusCodeMatch ? statusCodeMatch[1] : statusCodeString.slice(0, 3)
164+
160165
const logMap = {
161166
100: `Command successfully sent, statusCode: ${statusCodeString}`,
162167
200: `Request successful, statusCode: ${statusCodeString}`,
163168
400: `Bad Request, statusCode: ${statusCodeString}`,
169+
422: `Unprocessable Entity - The request was well-formed but could not be processed. This may indicate the lock is in an invalid state or the operation is not allowed at this time, statusCode: ${statusCodeString}`,
164170
429: `Too Many Requests, exceeded the number of requests allowed for a given time window, statusCode: ${statusCodeString}`,
165171
}
166-
const logMessage = logMap[statusCodeString.slice(0, 3)]
172+
const logMessage = logMap[statusCode]
167173
?? `Unknown statusCode: ${statusCodeString}, Submit Bugs Here: https://tinyurl.com/AugustYaleBug`
168174
await this.debugLog(logMessage)
169-
if (!logMap[statusCodeString.slice(0, 3)]) {
175+
if (!logMap[statusCode]) {
170176
await this.debugErrorLog(`failed ${action}, Error: ${error}`)
171177
}
172178
}

0 commit comments

Comments
 (0)