Technical documentation for contributing to and extending the OpenGrowBox Home Assistant GUI.
- Project Structure
- Development Setup
- Component Architecture
- State Management
- Communication
- Styling
- Testing
- Building & Deployment
- Contributing
ogb-ha-gui/
├── src/
│ ├── Components/ # React components
│ │ ├── Cards/ # Display cards
│ │ │ ├── SliderCards/ # Sensor cards
│ │ │ ├── ControlCards/ # Device controls
│ │ │ └── ...
│ │ ├── Context/ # React Context providers
│ │ ├── Dashboard/ # Dashboard components
│ │ ├── GrowBook/ # Grow tracking
│ │ ├── Navigation/ # Navigation components
│ │ ├── Settings/ # Settings pages
│ │ └── Common/ # Shared components
│ ├── Pages/ # Route pages
│ ├── hooks/ # Custom React hooks
│ ├── utils/ # Utility functions
│ ├── misc/ # Misc helper files
│ ├── App.jsx # Main App component
│ ├── main.jsx # Entry point
│ └── config.js # App configuration
├── public/ # Static assets
├── docs/ # Documentation
├── package.json # Dependencies
├── vite.config.ts # Vite config
└── tsconfig.json # TypeScript config
Components/Cards/:
SliderCards/: Sensor display cards (Temp, Hum, VPD, CO2, etc.)ControlCards/: Device control (Switch, Slider, Select, DeviceCard, etc.)
Components/Context/:
HomeAssistantContext.jsx: WebSocket connection and HA dataGlobalContext.jsx: Shared state across appOGBPremiumContext.jsx: Premium features and authenticationMediumContext.jsx: Medium/plant data management
Pages/:
Home.jsx: Main dashboardDashboard.jsx: Detailed metricsGrowBook.jsx: Grow tracking and logsSettings.jsx: ConfigurationInterface.jsx: Layout wrapper
- Node.js 18+ and npm
- Home Assistant instance (for PROD testing)
- Git
# Clone repository
git clone <repository-url>
cd ogb-ha-gui
# Install dependencies
npm install
# Start development server
npm run dev
# Open browser to http://localhost:3004# Start dev server
npm run dev
# Run type checking
npm run typecheck
# Run linter
npm run lint
# Build for production
npm run build
# Preview production build
npm run previewCreate .env file in root:
# No environment variables required for basic development
# For PROD build, values come from Home AssistantPages are top-level components corresponding to routes:
// Home.jsx
export default function Home() {
return (
<PageLayout>
<DashboardStats />
<ControlCollection />
<DeviceCard />
</PageLayout>
);
}Reusable UI cards for displaying data:
Slider Cards (Display sensors):
<TempCard />
<HumCard />
<VPDCard />
<CO2Card />Control Cards (Control devices):
<SwitchCard entity_id="switch.example" />
<SliderCard entity_id="number.example" />
<SelectCard entity_id="select.example" />Most cards follow this pattern:
const MyCard = ({ entity_id, title, ...props }) => {
const { entities, callService } = useHomeAssistant();
const entity = entities[entity_id];
return (
<CardContainer>
<Header>{title}</Header>
<Content>{entity.state}</Content>
</CardContainer>
);
};State is managed through React Context providers:
HomeAssistantContext
- WebSocket connection
- HA entities
- Services
- Current room
GlobalContext
- Shared application state
- HASS object (PROD only)
OGBPremiumContext
- Authentication
- Premium features
- Backend communication
MediumContext
- Plant/medium data
- WebSocket subscriptions
import { useHomeAssistant } from '../Context/HomeAssistantContext';
function MyComponent() {
const {
entities, // All HA entities
currentRoom, // Current selected room
connection, // WebSocket connection
callService // Call HA service
} = useHomeAssistant();
const entity = entities['sensor.example'];
// ...
}Use useState for component-local state:
const [isExpanded, setIsExpanded] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
// Side effects
}, [dependencies]);The app communicates with Home Assistant via WebSocket through HomeAssistantContext:
// Subscribe to events
connection.subscribeEvents((event) => {
console.log('Event received:', event);
}, 'event_type');
// Call service
await callService('domain', 'service', {
entity_id: 'switch.example',
service_data: { /* options */ }
});Common Event Types:
state_changed - Entity state changes
{
type: "state_changed",
entity_id: "sensor.temperature",
new_state: { state: "25", attributes: {...} }
}LogForClient - Grow log events
{
type: "LogForClient",
data: {
room: "flower tent",
message: "VPD adjusted",
timestamp: 1234567890
}
}MediumPlantsUpdate - Medium/plant updates
{
type: "MediumPlantsUpdate",
data: {
Name: "flower tent",
plants: [/* plant objects */]
}
}Call Home Assistant services:
// Turn on a switch
await callService('switch', 'turn_on', {
entity_id: 'switch.example'
});
// Set a number input
await callService('input_number', 'set_value', {
entity_id: 'input_number.example',
value: 50
});
// Select an option
await callService('input_select', 'select_option', {
entity_id: 'input_select.example',
option: 'option_name'
});The project uses styled-components for styling:
import styled from 'styled-components';
const CardContainer = styled.div`
background: var(--main-bg-color, #1a1a1a);
padding: 1rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
&:hover {
border-color: var(--primary-accent);
}
`;Theme colors are defined as CSS variables:
// src/utils/themeColors.js
export const themeColors = {
mainBgColor: '#0f0f0f',
primaryAccent: '#0b95ea',
mainTextColor: '#ffffff',
// ...
};Access in styled components:
color: var(--primary-accent, #0b95ea);Use media queries:
const ResponsiveContainer = styled.div`
display: flex;
gap: 1rem;
@media (max-width: 768px) {
flex-direction: column;
}
`;Test files are in src/test/__tests__/:
env-validation.test.jsinput-validation.test.jssafe-json-parsing.test.jssecure-token-storage.test.js
# Run all tests
npm test
# Run with coverage
npm test -- --coverage
# Run in watch mode
npm test -- --watchimport { describe, it, expect } from 'vitest';
import { myFunction } from '../utils/myFile';
describe('myFunction', () => {
it('should return correct value', () => {
expect(myFunction('input')).toBe('output');
});
it('should handle errors', () => {
expect(() => myFunction(null)).toThrow();
});
});npm run dev
# Runs on http://localhost:3004npm run build
# Creates dist/ folder with optimized assetsThe build creates:
dist/index.html- Entry HTMLdist/assets/- CSS, JS, and other assets- Files are hashed for caching
- Build the app:
npm run build - Copy contents of
dist/to your Home Assistantwww/folder - Add as custom panel in Home Assistant configuration
- Restart Home Assistant
Option 1: HA Panel (Recommended)
- Built into Home Assistant
- Use Home Assistant panel
Option 2: External Hosting
- Host on any web server
- Connect via WebSocket
- Requires CORS configuration
Option 3: Docker
- Use provided Dockerfile
- Containerized deployment
- Use functional components with hooks
- Prefer styled-components over CSS files
- Follow existing naming conventions
- Add comments for complex logic
- Use TypeScript where possible (JSX files are JS, but TSConfig exists)
Follow conventional commits:
feat: add new sensor card for CO2
fix: correct temperature display in metric card
docs: update user documentation
style: improve card layout styling
refactor: simplify state management
test: add tests for utility functions
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Update documentation
- Submit pull request with clear description
- Code follows project style
- Tests added/updated
- Documentation updated
- No console warnings/errors
- Responsive design tested
- Accessibility considered
- Create
src/Components/Cards/SliderCards/NewSensor.jsx:
import { useEffect, useState } from 'react';
import styled from 'styled-components';
import { useHomeAssistant } from '../../Context/HomeAssistantContext';
const NewSensor = () => {
const { entities } = useHomeAssistant();
const [value, setValue] = useState('');
useEffect(() => {
const entity = entities['sensor.new_sensor'];
if (entity) {
setValue(entity.state);
}
}, [entities]);
return (
<CardContainer>
<Header>New Sensor</Header>
<Value>{value}</Value>
</CardContainer>
);
};
export default NewSensor;- Import and use in
DashboardSlider.jsx - Add to slider menu
Similar to sensor cards, but with controls:
import { useState } from 'react';
import { useHomeAssistant } from '../../Context/HomeAssistantContext';
import SliderCard from './SliderCard';
const NewControl = () => {
const { entities, callService } = useHomeAssistant();
const [value, setValue] = useState(0);
const handleChange = async (newValue) => {
setValue(newValue);
await callService('input_number', 'set_value', {
entity_id: 'input_number.new_control',
value: newValue
});
};
return (
<SliderCard
entity_id="input_number.new_control"
value={value}
onChange={handleChange}
min={0}
max={100}
/>
);
};- Create
src/Pages/NewPage.jsx - Add route in
App.jsx - Add navigation item in
BottomBar.jsx
Open browser DevTools (F12) to see:
- Console errors/warnings
- Network requests
- React component tree
- State changes
WebSocket not connecting:
- Check Home Assistant is running
- Verify token is valid
- Check network connection
Entities not updating:
- Verify entity IDs match Home Assistant
- Check WebSocket subscription
- Ensure event listener is set up
Styling not applying:
- Check styled-components syntax
- Verify CSS variable names
- Clear browser cache
- Memoize expensive computations:
const filteredData = useMemo(() => {
return data.filter(/* filter logic */);
}, [data]);- Avoid unnecessary re-renders:
const MemoizedComponent = memo(MyComponent);- Debounce expensive operations:
import { debounce } from 'lodash';
const debouncedHandler = debounce(handler, 300);- Optimize WebSocket subscriptions:
- Only subscribe to needed events
- Clean up listeners on unmount
- Never log tokens to console
- Store tokens securely (use
secureTokenStorage.js) - Don't commit tokens to git
- Validate tokens before use
- Sanitize all user inputs
- Use parameterized queries (if applicable)
- Validate entity IDs before use
- Escape HTML when rendering user content
Happy Coding! 🚀